P1666:前缀单词题解

思路

这道题是一道动态规划的题目。我们首先考虑预处理出一个数组,来存放两两单词是否安全。之后我们再利用这个信息来进行DP。而有一个值得注意的要点是:我们在动态规划时要消除后效性,否则就会出现错误。在本题中,我们需要对字符串进行排序,因为排序后不安全的单词只存在于在第\(i\)个单词的前面,这样我们用一维\(dp\)即可。考虑方程:\(dp[j] += dp[i], j \in [i, n], possible[i][j] == true\)即可。

Continue reading →

P1966:火柴排队题解

思路

我们首先把\(a,b\)输入进一个数据结构,并且对这两个序列以高度为关键字排序。我们先来写出一组样例排序后的样子(数组下标为原本的位置):\[\{1_3,2_1,3_2,4_4\}\]与\[\{1_3,2_2,3_1,4_4\}\]我们观察发现,显然我们可以进行离散处理。所以令\(mapping[]\)为离散数组,在两组排序后进行\(mapping[a[i].index] = b[i].index\),便可以完成离散化。问题便转换为b数组要完成多少次变换才能与a相同(因为我们已经进行了离散化处理,\(mapping[]\)便可以在\(n\)范围内)。我们可以想到使用计算逆序对个数来解决这个问题。问题迎刃而解。

Continue reading →

数学期望和其线性性质

背景

自从NOIp 2018初赛第一次知道有数学期望这个东西起,我就一直很难理解。我一直没有去仔细的了解数学期望,而今天打了一场比赛,被逼着去了解了,现在我来写点东西记录一下这个概念。

概念介绍

\(数学期望\)是一个跟概率相关的概念。他的定义是:

数学期望是一个有限的事件的结果乘上一个事件的概率。

我们可以通过举例子来解释这个概念。比如取一个随机数(1-10之间),那么取到5的数学期望就是\(\frac {1} {2}\)。一般,我们用\(\mathbb{E}\)来表示这个值。

性质介绍

在本篇文章中,我只介绍一个性质。未来如果我学到了一些关于数学期望的新东西,那么我会把这些内容写在这篇文章里的。

数学期望在OI中比较重要的性质就是其线性性。简单来讲,如果有两个事件的数学期望为\(\mathbb{E}(a)\)和\(\mathbb{E}(b)\),那么这两个事件叠加起来就是\(\mathbb{E}(a) + \mathbb{E}(b) = \mathbb{E}(a+b)\)。这个性质在OI中比较重要,一些题目会利用这个性质让解题者使用递推的方式解出答案。接下来我们看一道题目:

例题

链接:T2062:随机数生成器

这道题目是我在华南师范大学附属中学举办的比赛中遇到的题目。虽然我这一题并没有AC,但是不妨碍我们来解释数学期望的一些性质和递推得解的方法。

我们来思考这样一个方程,设置\(\mathbb{E}(i)\)为输入为\(i\)的数学期望,那么,根据数学期望的线性性质,我们可以很容易推出以下方程:\[\mathbb{E}(i) =\frac {\mathbb{E}(1) + 1}{i} + \frac {\mathbb{E}(2) + 1}{i} + \frac {\mathbb{E}(3) + 1}{i} + \frac {\mathbb{E}(4) + 1}{i} + \dots + \frac {\mathbb{E}(i) + 1}{i}\]

而在本题中,可知\(\mathbb{E}(1) = 0\)。那么我们来变换一下式子,变成:\[\mathbb{E}(i) = \frac {\mathbb{E}(1) + \mathbb{E}(2) + \dots + \mathbb{E}(i – 1) + i}{n – 1}\]这样我们就可以递推出式子。

数学期望是OI中常见的概念,掌握它很有帮助。建议读者可以自己去阅读相关的资料,我在这里便不再描述。

P2915:「USACO08NOV」奶牛混合起来题解

思路

这道题观察数据范围和题干,可以得知这是一道状压dp的题目。我们先来设计状态:在本题中,可以类比互不侵犯的那一题(互不侵犯题解),设计成当前状态和上一状态相关的元素。可以想出,dp[当前牛的存在情况][当前加入的牛的编号] += dp[上一次加入后的状态][上一次加入的牛的编号];

基本上完成了思考,现在我们先来搞定预处理。我们把只有cowId在队列里的方案数初始化为1。

for (int cowId = 0; cowId < n; cowId++)
        dp[1 << cowId][cowId] = 1;

之后我们就可以进行dp了。详细见代码:

for (int lastState = 0; lastState < (1 << n); lastState++)
    for (int lastCow = 0; lastCow < n; lastCow++)
        if (lastState & (1 << lastCow))
            for (int nowCow = 0; nowCow < n; nowCow++)
                if (!(lastState & (1 << nowCow)) && abs(seq[nowCow] - seq[lastCow]) > k)
                    nowState = lastState | (1 << nowCow),
                    dp[nowState][nowCow] += dp[lastState][lastCow];

最后方案数叠加,在满员的状态下枚举最后一头牛的id。

ll ans = 0;
for (int i = 0; i < n; i++)
    ans += dp[(1 << n) - 1][i];
cout << ans;

代码

// P2915.cpp
#include <iostream>
#include <cmath>
using namespace std;

#define ll unsigned long long
const int maxn = 16;
ll dp[1 << maxn][maxn];
int n, k, nowState;
int seq[maxn];

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i++)
        cin >> seq[i];
    for (int cowId = 0; cowId < n; cowId++)
        dp[1 << cowId][cowId] = 1;
    for (int lastState = 0; lastState < (1 << n); lastState++)
        for (int lastCow = 0; lastCow < n; lastCow++)
            if (lastState & (1 << lastCow))
                for (int nowCow = 0; nowCow < n; nowCow++)
                    if (!(lastState & (1 << nowCow)) && abs(seq[nowCow] - seq[lastCow]) > k)
                        nowState = lastState | (1 << nowCow),
                        dp[nowState][nowCow] += dp[lastState][lastCow];
    ll ans = 0;
    for (int i = 0; i < n; i++)
        ans += dp[(1 << n) - 1][i];
    cout << ans;
    return 0;
}

十月份暨AC100题总结

这个月我达成了比较多的目标,比如说题目的AC数量有着肉眼可见的提升,学习了一些算法:刚学的状压dp,区间dp的熟练,数论的一些玄学骚操作,机房大佬教我的LCA和倍增RMQ算法,树状数组的编写(还没搞定区间的最值),图论的前向星存图(基础现在来学,我还是太菜了),最小生成树(然而Prim还没学我很慌),DAG上的dp和树形dp也练了一些水题,并查集也拓展了一些其他的写法,还有一些STL的更优替代,bitset等等。

然而我发现很多算法都只学了皮毛,即使在我学过的范畴之内,也有很多题目中我都很难想出正解。这个时候蒟蒻的思维能力就体现了出来。不过经过一个月的训练,思维能力也还是有了长足的进步,反应能力也没有之前那么不堪。AC速度比我想象中要高很多,但是由于11月份联赛结束之后要回归文化课,所以并不会期望11月份的算法学习能有多快。

12月份和1月份将是我完成所有中阶知识的deadline,我希望在2月份到来的寒假集训我可以保持良好的状态,以便于准备下个学期停课去外校集训的基础。当然文化课也要在寒假做一些基本的认知,题还是要做的,只不过很难像17班的优秀dalao,我也只能说我很惭愧。

这个月的NOIp 2018,希望我有个好结果,也真的希望机房的dalao可以轻松通过。总而言之,NOIp 2018 ++rp。

P1896:「SCOI2005」互不侵犯题解

思路

这是一道状压dp的题目,在此之前我没有学过状压dp,今天上午在dalao lornd的讲解下我理解了状压dp。然而我的能力有限,不能广义上来总结状压dp这个骚操作。状压dp的精髓就在于把多个状态合并为二进制数,然后再进行子集生成。我们这里设计一个状态,这个状态包含了当行棋子的摆放方式。比如,01110代表中间三个位置放置了棋子,为了简便,我们把这个状态可以直接压缩成14这个十进制的数字。这就是状态压缩

强烈建议读者学习完位运算的基本知识之后再来阅读状态压缩的文章,这样理解起来才能事半功倍。

之后我们搞懂了如何压缩状态之后,我们来进行dp。首先我们先要来预处理,预处理的几个目标是:计算单行的状态,计算两行之间的状态,计算每个状态的棋子数量

计算单行和棋子的数量的状态比较简单,在[0,2^32)之间枚举出状态,判定是否有两个1相邻。如果有的话,那么在状态表中设置成false,表示这个状态不能被利用,即不能被纳入计算。具体代码如下:

for (int i = 0; i < MAX; i++)
    {
        // check if there are pairs of 1;
        if (i & (i >> 1))
            continue;
        // checked and set it true;
        sigLine[i] = true;
        // get the number of 1 within the state and record them;
        for (int j = 1; j <= i; j <<= 1)
            if (j & i)
                king[i]++;
    }

其中我们设置MAX = 1 << n,即状态的上限。至于为什么小于MAX而不是小于等于MAX,读者可以思考这两者是否有区别。

之后我们要计算两行之间的状态,我们开一个数组叫\(doubleLine[1<<maxn][1<<maxn]\),其中如果\(doubleLine[x][y]==true\),说明第一行的状态为x且第二行状态为y是可以存在的,可以被纳入计算的。这样的话,不难想出用暴力的方法来进行预处理:

// the first line;
    for (int i = 0; i < MAX; i++)
        if (sigLine[i])
            // the second line;
            for (int j = 0; j < MAX; j++)
                // check if these two are valid.
                if (sigLine[j] && !(i & j) && !(i & (j << 1)) && !(i & (j >> 1)))
                    // set!
                    doubleLine[i][j] = true;

我们预处理工作差不多了,接下来我们来进行动态规划的递推编写了。首先我们先来设计方程,不难想出方程:

\(dp[前n行数][累计w个棋子][本行的状态] = 当前状态数\)

\(dp[i+1][w+king[next]][nextLine] = dp[i][w][currentLine]\)

最后,我们把在n行上、棋子数量为k的所有状态枚举一边,便可以累计得出答案。特别注意的是,这道题目需要long long的数据范围,小心溢出。

代码

// P1896.cpp
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int maxn = 11;
#define ll long long
int MAX;
int n, k;
bool sigLine[1 << maxn];
bool doubleLine[1 << maxn][1 << maxn];
int king[1 << maxn];
ll dp[maxn][100][1 << maxn];

int main()
{
    cin >> n >> k;
    MAX = 1 << n;
    for (int i = 0; i < MAX; i++)
    {
        // check if there are pairs of 1;
        if (i & (i >> 1))
            continue;
        // checked and set it true;
        sigLine[i] = true;
        // get the number of 1 within the state and record them;
        for (int j = 1; j <= i; j <<= 1)
            if (j & i)
                king[i]++;
    }
    // the first line;
    for (int i = 0; i < MAX; i++)
        if (sigLine[i])
            // the second line;
            for (int j = 0; j < MAX; j++)
                // check if these two are valid.
                if (sigLine[j] && !(i & j) && !(i & (j << 1)) && !(i & (j >> 1)))
                    // set!
                    doubleLine[i][j] = true;
    for (int i = 0; i <= MAX; i++)
        if (sigLine[i])
            dp[1][king[i]][i] = 1;
    for (int i = 1; i < n; i++)
        for (int poss = 0; poss < MAX; poss++)
            if (sigLine[poss])
                for (int nextLine = 0; nextLine < MAX; nextLine++)
                    if (sigLine[nextLine] && doubleLine[poss][nextLine])
                        for (int w = king[poss]; w + king[nextLine] <= k; w++)
                            dp[i + 1][w + king[nextLine]][nextLine] += dp[i][w][poss];
    ll ans = 0;
    for (int i = 0; i < MAX; i++)
        ans += dp[n][k][i];
    cout << ans;
    return 0;
}