十月份暨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;
}

P2014:选课题解

思路

一开始我还以为是拓扑排序,后面写代码的时候仔细思考了这道题,应该是树形dp。

主要的思路就是做一个编号为0的虚拟节点(这个点权重(学分)为0),然后建一棵树。构造dp方程\(F[u][size] = F[son][subsize] + F[u][size – subsize]\)。其中这里需要注意的是,这个本质上是一个树形的01背包,所以在生成dp表的时候需要反着推。(坑了我一个小时,哎我还是太菜了)输出的时候因为有点0的存在所以背包容量会比之前的大一个单位。

还需要注意的是,在我的代码中,我用了一个dfss来计算每个子背包的容量,不知道有没有必要,希望大佬能指出是否有这个必要。

代码

// P2014.cpp
#include <iostream>
#include <queue>
using namespace std;

const int maxn = 2100;
struct edge
{
    int to, next;
} edges[maxn << 1];
int deg[maxn];
int indeg[maxn];
int head[maxn];
int weight[maxn];
int current = 0;

int n, m;

void addpath(int src, int dst)
{
    edges[current].to = dst;
    edges[current].next = head[src];
    head[src] = current++;
}

int F[maxn][maxn];
int sizes[maxn];

void dfss(int u)
{
    sizes[u] = 1;
    for (int i = head[u]; i != -1; i = edges[i].next)
    {
        dfss(edges[i].to);
        sizes[u] += sizes[edges[i].to];
    }
}

void dfs(int u)
{
    for (int i = 1; i <= m; i++)
        F[u][i] = weight[u];
    for (int i = head[u]; i != -1; i = edges[i].next)
    {
        dfs(edges[i].to);
        int jto = edges[i].to;
        for (int len = m + 1; len >= 2; len--)
            for (int lens = 0; lens < len; lens++)
                F[u][len] = max(F[u][len], F[u][len - lens] + F[jto][lens]);
    }
}

int main()
{
    fill(head, head + maxn, -1);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        int k, s;
        cin >> k >> s;
        weight[i] = s;
        addpath(k, i), indeg[i]++, deg[k]++;
    }
    dfss(0);
    dfs(0);
    cout << F[0][m + 1];
    return 0;
}

P1373:小a和uim之大逃离题解

思路

这道题是一道比较典型的dp求方案数。这是我第一次做dp方案数的题目,这篇题解就作为我dp方案数专题的第一篇文章吧。

首先思考本题的dp方程时,需要用方案数dp的方式去思考。方案数是叠加的,所以每次转移时的运算就是相加而不是取最大最小值。这样的话,我们可以来真正的来思考本题方程。首先方程中需要有点的位置,亦需要小a和uim的状态。我们如何把魔液这个元素考虑进来呢?我们增加一个维度,来记录俩人魔液罐子的差值(当然,为负数的时候我们就可以加一个k进行取模运算,保证在正数范围内可以访问)。所以,dp[i][j][h][0/1]可以表示为走到(i,j)时两人魔罐插值为h时的方案数量,其中0代表小a走,1代表uim走。所以方程如下:

F[i][j][h][0] = (F[i][j][h][0] + F[i – 1][j][(h – map[i][j] + k) % k][1]) % P;
F[i][j][h][0] = (F[i][j][h][0] + F[i][j – 1][(h – map[i][j] + k) % k][1]) % P;
F[i][j][h][1] = (F[i][j][h][1] + F[i – 1][j][(h + map[i][j]) % k][0]) % P;
F[i][j][h][1] = (F[i][j][h][1] + F[i][j – 1][(h + map[i][j]) % k][0]) % P;

代码如下:

// P1373.cpp
#include <iostream>

using namespace std;
// Variables;
const int maxn = 802;
const int INF = 0x7fffffff;
const int P = 1000000007;
int n, m, k;
int map[maxn][maxn];
int F[maxn][maxn][20][2];
// Start to dp;
void dp()
{
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            for (int h = 0; h <= k; h++)
            {
                F[i][j][h][0] = (F[i][j][h][0] + F[i - 1][j][(h - map[i][j] + k) % k][1]) % P;
                F[i][j][h][0] = (F[i][j][h][0] + F[i][j - 1][(h - map[i][j] + k) % k][1]) % P;
                F[i][j][h][1] = (F[i][j][h][1] + F[i - 1][j][(h + map[i][j]) % k][0]) % P;
                F[i][j][h][1] = (F[i][j][h][1] + F[i][j - 1][(h + map[i][j]) % k][0]) % P;
            }
}

int main()
{
    // Input;
    cin >> n >> m >> k, ++k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            // Initialize;
            cin >> map[i][j], F[i][j][map[i][j]][0] = 1;
    dp();
    long long ans = 0;
    // Calculate the answer;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            ans += F[i][j][0][1], ans %= P;
    // Output;
    cout << ans;
    return 0;
}

P1352:没有上司的舞会题解

思路

这是一道非常经典的树形DP,题中直接讲到这是树形结构。建树之后,状态转移方程不难想出:

  • 对于每一个点,F[u][0]代表不选这个点的最优解,F[u][1]代表选这个点的最优解。
  • 对于运行时的每个点,都有F[u][1] = happiness[u]。
  • 对于一个点的孩子遍历,都有F[u][0] += max(F[v][1], F[v][0]),F[u][1] += F[v][0]。

由此,我们可以轻松的写出下列的代码。

代码

// P1352.cpp
#include <iostream>

using namespace std;
// Data Structure;
const int maxn = 6100;
int n;
int R[maxn];
// Graph stuff;
// Notice: the edge space should be doubled;
struct edge
{
    int to, next;
} edges[maxn << 1];
int head[maxn];
int current = 0;
int F[maxn][2];
int deg[maxn];
// DFS func;
void DFS(int curt)
{
    // Initalize the DP Table;
    F[curt][1] = R[curt];
    // Find the subnodes;
    for (int i = head[curt]; i != -1; i = edges[i].next)
    {
        // Call the subtree to be ready;
        DFS(edges[i].to);
        // Deciding;
        F[curt][1] += F[edges[i].to][0];
        F[curt][0] += max(F[edges[i].to][0], F[edges[i].to][1]);
    }
}

void add_path(int src, int dst)
{
    edges[current].to = dst;
    edges[current].next = head[src];
    head[src] = current++;
}

int main()
{
    // Initialize;
    cin >> n;
    fill(head, head + maxn, -1);
    for (int i = 1; i <= n; i++)
        cin >> R[i];
    int a, b;
    for (int i = 1; i <= n - 1; i++)
    {
        cin >> a >> b, add_path(b, a);
        deg[a]++;
    }
    cin >> a >> b;
    // Find the root node by the degrees saved before;
    int root = 0;
    for (int i = 1; i <= n; i++)
        if (deg[i] == 0)
        {
            root = i;
            break;
        }
    // Start to dp;
    DFS(root);
    // Output;
    cout << max(F[root][0], F[root][1]);
    return 0;
}

P1841:「JSOI2007」重要的城市题解

思路

这道题是lornd巨佬给我写的。由于我太菜了,只能放弃看题解。看完题解之后,发现解法甚是巧妙,便来分享一则。

原理就是在无向图floyed求最短路的时候,每发现一次新的路径就要更新一次重要点。策略如下:

  1. 如果原路径比现在的路径要长,那么更新路径,并且在cities[i][j]中记录点k。
  2. 如果原路径和现在的路径相等,那么重要点不唯一,那么把cities[i][j]记录为-1。

最终在cities里面搜索不为-1的元素,排序输出即可。

代码

// P1841.cpp
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;
#define ll long long
ll mat[300][300];
int dp[300][300];
bool ans[500];
const ll INF = (ll)(0x7fffffff) / 3;
int n, m;

void floyed()
{
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            if (i != k)
                for (int j = 1; j <= n; j++)
                    if (i != j && j != k)
                        if (mat[i][k] + mat[k][j] < mat[i][j])
                            mat[i][j] = mat[i][k] + mat[k][j], dp[i][j] = k;
                        else if (mat[i][k] + mat[k][j] == mat[i][j])
                            dp[i][j] = -1;
}

void addpath(int src, int dst, ll w)
{
    mat[src][dst] = mat[dst][src] = w;
}

int main()
{
    fill(mat[0], mat[0] + 300 * 300, INF);
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        ll a, b, c;
        cin >> a >> b >> c;
        addpath(a, b, c);
    }
    floyed();
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (dp[i][j] != -1)
                ans[dp[i][j]] = true;
    bool flag = true;
    for (int i = 1; i <= n; i++)
        if (ans[i])
            cout << i << " ", flag = false;
    if (flag)
        cout << "No important cities.";
    return 0;
}