https://0xfaner.top/posts/atcoder-dp/

做题并不是一口气全做完的,所以前后可能有代码风格不一样的地方

写在前面

深感自己 DP 很弱的 Murabito 刷了点 DP 题,比赛地址戳这里

后记:刷完后感觉自己又行了

A - Frog 1

题意

给定 AtCoder Educational DP Contest 刷题记录 - 图1 个石头,第 i 个石头的高度为 AtCoder Educational DP Contest 刷题记录 - 图2。现在要求小青蛙从 1 号石头跳到 n 号石头,每次小青蛙可以选择从 i 号石头跳到 i+1 或 i+2 号石头,代价是起跳点与落点的高度差的绝对值。询问你最小代价。

解法

令 f[i] 表示小青蛙跳到第 i 号石头时的最小代价。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图3#card=math&code=O%28n%29&id=QLTL3)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. int main() {
  5. ios_base::sync_with_stdio(false), cin.tie(0);
  6. int n;
  7. cin >> n;
  8. vector<int> dp(n, 0), h(n);
  9. for (auto &x : h) cin >> x;
  10. for (int i = 1; i < n; ++i) {
  11. dp[i] = min(dp[i - 1] + abs(h[i] - h[i - 1]),
  12. i > 1 ? dp[i - 2] + abs(h[i] - h[i - 2]) : INT_MAX);
  13. }
  14. cout << dp[n - 1] << "\n";
  15. return 0;
  16. }

B - Frog 2

题意

给定 n 个石头与 k 的操作范围,第 i 个石头的高度为 hi。现在要求小青蛙从 1 号石头跳到 n 号石头,每次小青蛙可以选择从 i 号石头跳到 i+s ( 1≤s≤k )号石头,代价是起跳点与落点的高度差的绝对值。询问你最小代价。

解法

令 f[i] 表示小青蛙跳到第 i 号石头时的最小代价。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图4#card=math&code=O%28nk%29&id=eDfkA)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. int main() {
  5. ios_base::sync_with_stdio(false), cin.tie(0);
  6. int n, k;
  7. cin >> n >> k;
  8. vector<int> dp(n, 0), h(n);
  9. for (auto &x : h) cin >> x;
  10. for (int i = 1; i < n; ++i) {
  11. dp[i] = INT_MAX;
  12. for (int j = 1; j <= k && (i - j >= 0); ++j)
  13. dp[i] = min(dp[i], abs(h[i] - h[i - j]) + dp[i - j]);
  14. }
  15. cout << dp[n - 1] << "\n";
  16. return 0;
  17. }

C - Vacation

题意

给定太郎的暑假时长为 n 天,每天他可以进行三种活动中的一种,每种活动给他带来的愉悦值各不相同。

如果当天进行过某一种活动,第二天即不能进行另一种活动,询问太郎能获得的最大愉悦值。

解法

AtCoder Educational DP Contest 刷题记录 - 图5 分别记录在第 i 天选做第 j 件娱乐活动的最大愉悦值。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图6#card=math&code=O%28n%29&id=oaWqr)

代码

  1. // RioTian 21/03/16#include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. const int N = 1e5 + 10;
  5. int dp[N][3], a[N][3], n;
  6. int main() {
  7. ios_base::sync_with_stdio(false), cin.tie(0);
  8. cin >> n;
  9. for (int k = 1; k <= n; ++k)
  10. for (int i = 0; i < 3; ++i) cin >> a[k][i];
  11. for (int k = 1; k <= n; ++k)
  12. for (int i = 0; i < 3; ++i)
  13. for (int j = 0; j < 3; ++j)
  14. if (i != j) dp[k][i] = max(dp[k][i], dp[k - 1][j] + a[k][j]);
  15. cout << max(dp[n][0], max(dp[n][1], dp[n][2]));
  16. return 0;
  17. }

D - Knapsack 1

题意

给定太郎拥有的 n 个物品,第 i 件物品大小为 wi 权重为 vi。

给定太郎的背包的最大容量 m,询问太郎能取得的最大权重和。

解法

01 背包求解。

f[i] 表示装总大小不超过 i 的物品后的最大权重和。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图7#card=math&code=O%28nm%29&id=KP9l3)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. const int N = 100010;
  5. ll dp[N];
  6. int main() {
  7. ios_base::sync_with_stdio(false), cin.tie(0);
  8. int n, m;
  9. cin >> n >> m;
  10. for (int i = 1, w, v; i <= n; ++i) {
  11. cin >> w >> v;
  12. for (int j = m; j >= w; j--) dp[j] = max(dp[j], dp[j - w] + v);
  13. }
  14. cout << dp[m];
  15. return 0;
  16. }

E - Knapsack 2

题意

给定太郎拥有的 n 个物品,第 i 件物品大小为 AtCoder Educational DP Contest 刷题记录 - 图8 权重为 AtCoder Educational DP Contest 刷题记录 - 图9

给定太郎的背包的最大容量 m,询问太郎能取得的最大权重和。

解法

发现权值不超过 100,令 f[i] 表示装填价值为 i 的物品的最小权值。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图10#card=math&code=O%28nv%29&id=FkLtd)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. const int N = 100010;
  5. ll dp[N];
  6. int n, m;
  7. int main() {
  8. ios_base::sync_with_stdio(false), cin.tie(0);
  9. memset(dp, 0x3f, sizeof dp);
  10. dp[0] = 0;
  11. cin >> n >> m;
  12. for (int i = 1, w, v; i <= n; ++i) {
  13. cin >> w >> v;
  14. for (int j = N - 1; j >= v; --j) dp[j] = min(dp[j], dp[j - v] + w);
  15. }
  16. int add = N - 1;
  17. while (dp[add] > m) add--;
  18. cout << add << "\n";
  19. return 0;
  20. }

F - LCS

题意

给定两个字符串 s 和 t,询问它们的任意一个最长公共子串。

解法

AtCoder Educational DP Contest 刷题记录 - 图11 表示 s 的长度为 i 的前缀和 t 的长度为 j 的前缀的最长公共子串。

递归输出即可。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图12#card=math&code=O%28n%29&id=j04yq)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. #define N 3002
  4. char s[N], t[N];
  5. int dp[N][N], n, m;
  6. void print(int x, int y) {
  7. if (!x || !y) return;
  8. if (s[x] == t[y]) {
  9. print(x - 1, y - 1);
  10. putchar(s[x]);
  11. } else {
  12. (dp[x][y] == dp[x][y - 1] ? y : x)--;
  13. print(x, y);
  14. }
  15. }
  16. int main() {
  17. scanf("%s%s", s + 1, t + 1);
  18. int n = strlen(s + 1), m = strlen(t + 1);
  19. for (int i = 1; i <= n; i++)
  20. for (int j = 1; j <= m; j++) {
  21. if (s[i] == t[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
  22. dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i][j - 1]));
  23. }
  24. print(n, m);
  25. return 0;
  26. }

G - Longest Path

题意

给定一个 DAG,求其最长路径。

解法

按照 DFS 序 DP,f[i] 表示从 i 号点出发的最长路径上点的数量。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图13#card=math&code=O%28n%29&id=zvgSi)

代码

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. using ll = long long;
  4. const int N = 1e5 + 10;
  5. const int M = 1e5 + 5;
  6. int head[N], nx[M], e[M];
  7. int f[N], v[N], n, m;
  8. int dfs(int x) {
  9. if (f[x]) return f[x];
  10. for (int i = head[x]; i; i = nx[i]) f[x] = max(f[x], dfs(e[i]));
  11. return ++f[x];
  12. }
  13. int main() {
  14. ios_base::sync_with_stdio(false), cin.tie(0);
  15. cin >> n >> m;
  16. for (int i = 1, Num = 0, x, y; i <= m; ++i) {
  17. cin >> x >> y;
  18. nx[++Num] = head[x], head[x] = Num, e[Num] = y;
  19. }
  20. int ans = 0;
  21. for (int i = 1; i <= n; ++i) ans = max(ans, dfs(i));
  22. cout << ans - 1 << "\n";
  23. return 0;
  24. }

H - Grid 1

题意

给定一个 AtCoder Educational DP Contest 刷题记录 - 图14 的网格,太郎起始在 AtCoder Educational DP Contest 刷题记录 - 图15#card=math&code=%281%2C1%29&id=q03Dy),最终要到达 AtCoder Educational DP Contest 刷题记录 - 图16#card=math&code=%28n%2Cm%29&id=mtwCm)。每次太郎可以从 AtCoder Educational DP Contest 刷题记录 - 图17#card=math&code=%28i%2Cj%29&id=xwqbz) 到达 AtCoder Educational DP Contest 刷题记录 - 图18#card=math&code=%28i%2Cj%2B1%29&id=i6dEn) 或 AtCoder Educational DP Contest 刷题记录 - 图19#card=math&code=%28i%2B1%2Cj%29&id=Ix03L),其中部分格子无法达到。

询问太郎的移动方案数。

解法

AtCoder Educational DP Contest 刷题记录 - 图20 表示从 AtCoder Educational DP Contest 刷题记录 - 图21#card=math&code=%281%2C1%29&id=wE2tF) 出发到达 AtCoder Educational DP Contest 刷题记录 - 图22#card=math&code=%28i%2Cj%29&id=XBS7t) 的移动方案。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图23#card=math&code=O%28nm%29&id=mGVbe)

代码

  1. const int N = 1e3 + 10;
  2. const int mod = 1e9 + 7;
  3. vector<string> vs(N);
  4. int dp[N][N] = {0};
  5. void solve() {
  6. int n, m;
  7. cin >> n >> m;
  8. for (int i = 0; i < n; ++i) cin >> vs[i];
  9. dp[0][0] = 1;
  10. for (int i = 0; i < n; ++i)
  11. for (int j = 0; j < m; ++j) {
  12. if (i == 0 and j == 0) continue;
  13. if (i - 1 >= 0 && vs[i - 1][j] == '.') dp[i][j] = (dp[i][j] + dp[i - 1][j]) % mod;
  14. if (j - 1 >= 0 && vs[i][j - 1] == '.') dp[i][j] = (dp[i][j] + dp[i][j - 1]) % mod;
  15. }
  16. cout << dp[n - 1][m - 1] << "\n";
  17. }

I - Coins

题意

给定 AtCoder Educational DP Contest 刷题记录 - 图24 个硬币,扔第 AtCoder Educational DP Contest 刷题记录 - 图25 个硬币时,它正面朝上的概率为 AtCoder Educational DP Contest 刷题记录 - 图26

询问对每一个硬币扔一次,正面朝上的数量多余反面朝上的硬币的数量的概率。

保证 AtCoder Educational DP Contest 刷题记录 - 图27 为奇数。

解法

定义 AtCoder Educational DP Contest 刷题记录 - 图28 表示前 i 个硬币中 j 个正面朝上的概率。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图29#card=math&code=O%28n%5E2%29&id=Dw6yQ)

代码

  1. #include <bits/stdc++.h>using namespace std;#define N 3001double f[N][N] = {1}, p[N];int n;int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> p[i]; f[i][0] = f[i - 1][0] * (1 - p[i]); for (int j = 1; j <= i; j++) f[i][j] = f[i - 1][j - 1] * p[i] + f[i - 1][j] * (1 - p[i]); } double ans = 0; for (int i = n / 2 + 1; i <= n; i++) ans += f[n][i]; cout << fixed << setprecision(10) << ans << "\n"; return 0;}

J - Sushi

题意

给定 AtCoder Educational DP Contest 刷题记录 - 图30 盘寿司,每盘里有 AtCoder Educational DP Contest 刷题记录 - 图31AtCoder Educational DP Contest 刷题记录 - 图32 个寿司。每次太郎随机选中一盘寿司,若该盘中有寿司,则吃掉一个。

询问你太郎的期望选择次数。

解法

看起来很像概率DP,但记忆化搜索即可。

AtCoder Educational DP Contest 刷题记录 - 图33 表示 x 盘 1 个寿司,y 盘 2 个寿司,z 盘 3 个寿司时的期望选择次数。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图34#card=math&code=O%28n%5E3%29&id=bzp5Q)

代码

  1. const int N = 310;int num[4], n;double p[N][N][N];double P(int x, int y, int z) { if (p[x][y][z] >= 0) return p[x][y][z]; double sum = (double)n / (x + y + z); if (x) sum += P(x - 1, y, z) * x / (x + y + z); if (y) sum += P(x + 1, y - 1, z) * y / (x + y + z); if (z) sum += P(x, y + 1, z - 1) * z / (x + y + z); return p[x][y][z] = sum;}void solve() { for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j) for (int k = 0; k < N; ++k) p[i][j][k] = -1; cin >> n; p[0][0][0] = 0; for (int i = 1, x; i <= n; ++i) cin >> x, num[x]++; cout << fixed << setprecision(10) << P(num[1], num[2], num[3]);}

K - Stones

题意

给定一个集合 AtCoder Educational DP Contest 刷题记录 - 图35 并给定一个石堆,包含 AtCoder Educational DP Contest 刷题记录 - 图36 个石子。

太郎和次郎轮流进行博弈,太郎先行。每次他们可以选择集合 AtCoder Educational DP Contest 刷题记录 - 图37 中的一个数字 AtCoder Educational DP Contest 刷题记录 - 图38,并从该石堆中取走 AtCoder Educational DP Contest 刷题记录 - 图39 个石子。

无法取走石子的人为输家,询问你赢家是谁。

解法

AtCoder Educational DP Contest 刷题记录 - 图40 表示当前剩余 AtCoder Educational DP Contest 刷题记录 - 图41 个石子时先手的胜败状态。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图42#card=math&code=O%28k%29&id=UvZ1H)

代码

这道题很奇怪,在 1 100000 1 这组数据竟然会输出不出来,但又过了,懵逼ing.

  1. const int N = 110, M = 100000 + 10;int a[N], dp[M] = {-1}, n, k;int check(int num) { if (dp[num]) return dp[num]; bool flag = false; for (int i = 1; i <= n; i++) if (a[i] <= num && check(num - a[i]) == -1) flag = true; return dp[num] = (flag ? 1 : -1);}void solve() { cin >> n >> k; for (int i = 1; i <= n; ++i) cin >> a[i]; cout << (check(k) == 1 ? "First" : "Second");}

L - Deque

题意

给定一个序列 AtCoder Educational DP Contest 刷题记录 - 图43,太郎和次郎轮流进行博弈,太郎先行。

每次可以从序列的两端中任取一端,删去那一段的第一个元素,并获取等量的分数。

太郎和次郎都希望能最大化自己与对面的得分差,询问你最终太郎与次郎的得分差。

解法

AtCoder Educational DP Contest 刷题记录 - 图44 表示剩余区间为 AtCoder Educational DP Contest 刷题记录 - 图45 时的最大得分差。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图46#card=math&code=O%28n%5E2%29&id=rpftT)

代码

using ll    = long long;const int N = 3e3 + 10;int n, a[N];ll dp[N][N];void solve() {    cin >> n;    for (int i = 1; i <= n; ++i) cin >> a[i];    bool f = true;    for (int i = 1; i <= n; ++i) cin >> a[i], dp[i][i] = a[i];    for (int len = 1; len <= n; ++len)        for (int i = 1; i + len <= n; ++i) {            int j    = i + len;            dp[i][j] = max(a[i] - dp[i + 1][j], a[j] - dp[i][j - 1]);        }    cout << dp[1][n];}

M - Candies

题意

给定 n 个小朋友的需求,第 i 个小朋友的需求为 ai,表示他可以接受 [0,ai] 个糖果。

给定 m 个糖果,要求你把这些糖果一个不剩的分配出去,询问你有多少种分配方案。

解法

AtCoder Educational DP Contest 刷题记录 - 图47 表示分配完前 i 个小朋友后还剩 j 个糖果的方案。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图48#card=math&code=O%28n%5E2%29&id=HILIJ)

代码

const int mod = 1e9 + 7, N = 110, M = 1e5 + 10;int f[N][M], s[N][M];void solve() {    int n, k;    cin >> n >> k;    f[0][k] = s[0][k] = 1;    for (int i = 1, x; i <= n; ++i) {        cin >> x;        for (int j = 1; j <= k; ++j)            f[i][j] = (s[i - 1][min(k, j + x)] - s[i - 1][j - 1] + mod) % mod;        s[i][0] = f[i][0] = s[i - 1][x];        for (int j = 1; j <= k; ++j) s[i][j] = (s[i][j - 1] + f[i][j]) % mod;    }    cout << f[n][0] << "\n";}

N - Slimes

区间DP

题意

给定 n 个史莱姆,每个史莱姆的初始大小为 AtCoder Educational DP Contest 刷题记录 - 图49

这 n 个史莱姆站成一排。每次可以将相邻的两个史莱姆合并为一个,新的史莱姆大小为原来两个史莱姆的大小之和。合并的代价是新史莱姆的大小,询问将这 n 个史莱姆合并为 1 个的最小代价。

解法

AtCoder Educational DP Contest 刷题记录 - 图50 表示将区间 [l,r] 的史莱姆合并为 1 个史莱姆的最小代价。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图51#card=math&code=O%28n%5E3%29&id=ajmRU)

代码

using ll    = long long;const int N = 410;ll f[N][N], s[N][N];int a[N], n;void solve() {    memset(f, 0x3f, sizeof(f));    cin >> n;    vector<ll> a(n + 1);    for (int i = 1; i <= n; ++i) {        cin >> a[i];        f[i][i] = 0, s[i][i] = a[i];    }    for (int i = 1; i <= n; i++)        for (int j = i + 1; j <= n; j++) s[i][j] = s[i][j - 1] + a[j];    for (int len = 1; len < n; ++len)        for (int i = 1; i + len <= n; ++i) {            int j = i + len;            for (int k = i; k < j; ++k)                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[i][j]);        }    cout << f[1][n] << "\n";}

另外一种写法

void solve() {    int n;    cin >> n;    vector<ll> v(n + 1), sum(n + 1);    vector<vector<ll>> dp(n + 1, vector<ll>(n + 1));    for (int i = 1; i <= n; i++) cin >> v[i], sum[i] = sum[i - 1] + v[i];    for (int i = 1; i < n; i++)        for (int j = 1; j + i <= n; j++) {            dp[j][j + i] = 1e18;            for (int k = j; k < j + i; k++)                dp[j][j + i] = min(dp[j][j + i], dp[j][k] + dp[k + 1][j + i]);            dp[j][j + i] += sum[j + i] - sum[j - 1];        }    cout << dp[1][n] << '\n';}

O - Matching

题意

给定 n 男 n 女,以及任意一对男女之间是否匹配。

询问有多少种方法将这男女完全配对。

解法

令 num(i) 表示 i 的二进制展开中 1 的数量。

f[i] 表示前 num(i) 个男生与状态为 i 的女生配对的方案数。

时间复杂度 AtCoder Educational DP Contest 刷题记录 - 图52#card=math&code=O%282n%C3%97n%29&id=p2fSV)

代码

#include <bits/stdc++.h>using namespace std;#define mod 1000000007#define N 22int a[N][N], f[1 << N] = {1}, n;int main() {    scanf("%d", &n);    for (int i = 1; i <= n; i++)        for (int j = 1; j <= n; j++) scanf("%d", &a[i][j]);    for (int s = 0; s < (1 << n); s++) {        int i = __builtin_popcount(s);        for (int j = 1; j <= n; j++)            if (s & (1 << (j - 1)) && a[i][j])                f[s] = (f[s] + f[s ^ (1 << (j - 1))]) % mod;    }    printf("%d\n", f[(1 << n) - 1]);    return 0;}

P - Independent Set

题意

给定一颗树,可以把树上的每一个节点染色为黑色或白色,但不允许两个相邻的节点同时为黑色。

询问染色方案数。

解法

f1[i] 表示 i 为白色时以 i 为根节点的子树的染色方案。

f2[i] 表示 i 为黑色时以 i 为根节点的子树的染色方案。

时间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;#define mod 1000000007#define ll long long#define N 100001#define M 200001int hd[N], nx[M], e[M], n;ll f1[N], f2[N];void dfs(int x, int fa) {    f1[x] = f2[x] = 1;    for (int i = hd[x]; i; i = nx[i])        if (e[i] != fa) {            dfs(e[i], x);            f1[x] = f1[x] * (f1[e[i]] + f2[e[i]]) % mod;            f2[x] = f2[x] * f1[e[i]] % mod;        }}int main() {    scanf("%d", &n);    for (int i = 1, Num = 0, x, y; i < n; i++) {        scanf("%d%d", &x, &y);        nx[++Num] = hd[x], hd[x] = Num, e[Num] = y;        nx[++Num] = hd[y], hd[y] = Num, e[Num] = x;    }    dfs(1, 0);    printf("%lld\n", (f1[1] + f2[1]) % mod);    return 0;}

R - Walk

题意

给定一个 n 个节点的有向图的邻接矩阵,求该有向图中长度为 k 的路径长。

解法

答案即为该邻接矩阵的 k 次幂的行列式。

使用矩阵快速幂即可,时间复杂度 O(n2log⁡k)

代码

#include <bits/stdc++.h>using namespace std;#define mod 1000000007#define ll long longll n, k;struct Matrix {    ll mat[50][50];    void clear() { memset(mat, 0, sizeof(mat)); }    void reset(int n) {        clear();        for (int i = 0; i < n; i++) mat[i][i] = 1;    }} a;Matrix MatrixMul(Matrix x, Matrix y) {    Matrix a;    a.clear();    for (int i = 0; i < n; i++)        for (int k = 0; k < n; k++)            for (int j = 0; j < n; j++)                a.mat[i][j] = (a.mat[i][j] + x.mat[i][k] * y.mat[k][j]) % mod;    return a;}ll MatrixQpow(Matrix a, ll P) {    Matrix s;    s.reset(n);    while (P) {        if (P & 1) s = MatrixMul(s, a);        a = MatrixMul(a, a), P >>= 1;    }    ll sum = 0;    for (int i = 0; i < n; i++)        for (int j = 0; j < n; j++) sum = (sum + s.mat[i][j]) % mod;    return sum;}int main() {    scanf("%lld%lld", &n, &k);    for (int i = 0; i < n; i++)        for (int j = 0; j < n; j++) scanf("%lld", &a.mat[i][j]);    printf("%lld\n", MatrixQpow(a, k));    return 0;}

S - Digit Sum

题意

给定一个数字 n 与数字 d,询问你 1 到 n 中多少数的数位和为 d 的倍数。

解法

简单数位 DP 即可,f[i][j] 表示 i 位数中,前缀对 d 取模为 j 的情况下的数位和为 d 的倍数的数的数量。

令 m 表示 n 的位数,时间复杂度 O(md)

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define mod 1000000007#define N 10001int a[N], d;ll f[N][101];ll dfs(int len, int pre, int top) {    if (!len) return pre == 0;    if (!top && f[len][pre] != -1) return f[len][pre];    int up = top ? a[len] : 9;    ll num = 0;    for (int i = 0; i <= up; i++)        num = (num + dfs(len - 1, (pre + i) % d, top && (i == up))) % mod;    if (!top) f[len][pre] = num;    return num;}char c[N];int main() {    memset(f, -1, sizeof(f));    scanf("%s%d", c + 1, &d);    int n = strlen(c + 1);    for (int i = n, j = 1; i; i--, j++) a[j] = c[i] - '0';    cout << (dfs(n, 0, 1) - 1 + mod) % mod << endl;    return 0;}

T - Permutation

题意

给定一个长度为 n−1 的仅包含 <> 的字符串,第 i 位字符表示 a[i] 与 a[i+1] 的大小关系。

询问能构造多少长度为 n 的,满足该字符串的要求,且恰好包含 1 到 n 每个数一次的序列。

解法

f[i][j] 表示安排完前 i 个数且第 i 个数为 j 的方案数。

时间复杂度 O(n2)

代码

#include <bits/stdc++.h>#define ll long long#define mod 1000000007#define N 3002int f[N][N], n, ans;char c[N];int main() {    scanf("%d%s", &n, c + 1);    f[1][1] = 1;    for (int i = 2; i <= n; i++) {        if (c[i - 1] == '<')            for (int j = 2; j <= i; j++)                f[i][j] = (f[i][j - 1] + f[i - 1][j - 1]) % mod;        else            for (int j = i - 1; j; j--)                f[i][j] = (f[i][j + 1] + f[i - 1][j]) % mod;    }    for (int i = 1; i <= n; ++i) ans = (ans + f[n][i]) % mod;    printf("%d\n", ans);    return 0;}

U - Grouping

题意

给定 n 个兔子之间的匹配程度,太郎想要把这 n 只兔子分组,对于任意两只分到同一组的兔子,太郎将会获得它们两对应的匹配程度的得分。

询问太郎的最大得分。

解法

令 sum[i] 表示状态为 i 的兔子分为一组时的得分。

f[i] 表示将状态为 i 的兔子分好组的最大得分。

时间复杂度 O(2n×n)

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define M 65536#define N 16int a[N][N], v[N], n;ll sum[M], f[M];int main() {    scanf("%d", &n);    for (int i = 0; i < n; i++)        for (int j = 0; j < n; j++) scanf("%d", &a[i][j]);    for (int t = 0; t < (1 << n); t++) {        int num = 0;        for (int i = 0; i < n; i++)            if (t >> i & 1ll) v[num++] = i;        ll s = 0;        for (int i = 0; i < num; i++)            for (int j = i + 1; j < num; j++) s += a[v[i]][v[j]];        f[t] = sum[t] = s;    }    for (int i = 0; i < (1 << n); i++)        for (int j = i; j; j = (j - 1) & i) f[i] = max(f[i], f[i ^ j] + sum[j]);    printf("%lld\n", f[(1 << n) - 1]);    return 0;}

V - Subtree

题意

给定一颗 n 个节点的树,将其每个节点染成黑色或白色,要求任意两个黑色节点之间的路径上无白色节点。

询问对于每一个节点,若它为黑色,则有多少染色方案数。

解法

考虑到模数可能为合数,因此无法用乘法逆元,对每一个节点单独计算它的子树的前缀乘积与后缀乘积。

s1[i] 表示 i 号节点为黑色时,它的子树节点的染色方案。

s2[i] 表示 i 号节点为黑色时,非它的子树节点的染色方案。

第一次用vector写树形 DP,学到许多

时间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define N 100001vector<int> G[N];vector<ll> a[N], b[N];ll f[N], len[N], m;ll s1[N], s2[N] = {0, 1};void dfs1(int x) {    G[x].erase(remove(G[x].begin(), G[x].end(), f[x]), G[x].end());    len[x] = G[x].size() - 1;    a[x].resize(len[x] + 1), b[x].resize(len[x] + 1);    ll sa = 1, sb = 1;    for (int i = 1; i <= len[x]; i++) {        f[G[x][i]] = x;        dfs1(G[x][i]);        a[x][i] = sa = sa * (s1[G[x][i]] + 1) % m;    }    for (int i = len[x]; i; i--) b[x][i] = sb = sb * (s1[G[x][i]] + 1) % m;    s1[x] = sa;}void dfs2(int x) {    for (int i = 1; i <= len[x]; i++) {        s2[G[x][i]] = s2[x];        if (i > 1) s2[G[x][i]] = s2[G[x][i]] * a[x][i - 1] % m;        if (i < len[x]) s2[G[x][i]] = s2[G[x][i]] * b[x][i + 1] % m;        s2[G[x][i]]++;        dfs2(G[x][i]);    }}int main() {    int n;    scanf("%d%lld", &n, &m);    for (int i = 1; i <= n; i++) G[i].push_back(-1);    for (int i = 1, x, y; i < n; i++) {        scanf("%d%d", &x, &y);        G[x].push_back(y), G[y].push_back(x);    }    dfs1(1), dfs2(1);    for (int i = 1; i <= n; i++) printf("%lld\n", s1[i] * s2[i] % m);    return 0;}

W - Intervals

题意

给定 m 个区间 [1,n] 的子区间。要求你构造出一个长度为 n 且只包含 01 的字符串,使得得分尽可能大。询问你最大得分。

得分计算方式为:若该字符串中第 i 位为 1 ,那么得分加上所有包括 i 的区间对应的权值。

解法

令 f[i] 表示安排完前 i 位,且第 i 位为 1 时的最大值。线段树优化转移。

后来发现无需存储 f[i],只需区间加值与维护全局最值的线段树即可。

时间复杂度 O(nlog⁡n)

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define N 200001ll s[N << 2], t[N << 2];vector<pair<int, int> > a[N];void add(int x, int L, int R, int l, int r, ll c) {    if (l <= L && R <= r) return (void)(s[x] += c, t[x] += c);    int mid = (L + R) >> 1;    if (l <= mid) add(x << 1, L, mid, l, r, c);    if (r > mid) add(x << 1 | 1, mid + 1, R, l, r, c);    s[x] = max(s[x << 1], s[x << 1 | 1]) + t[x];}int n, m;int main() {    scanf("%d%d", &n, &m);    for (int i = 1, x, y, z; i <= m; i++) {        scanf("%d%d%d", &x, &y, &z);        a[y].emplace_back(x, z);    }    for (int i = 1; i <= n; i++) {        add(1, 1, n, i, i, s[1]);        for (auto &it : a[i]) add(1, 1, n, it.first, i, it.second);    }    printf("%lld\n", max(s[1], 0ll));    return 0;}

X - Tower

题意

给定 n 个方块,每个方块有三个属性:重量 w 、强度 s 与价值 v。

太郎想要选择价值总和尽可能大的一些方块摞在一起,但是每个方块上方的方块的总质量不能超过该方块的强度。

询问太郎能取得的最大价值。

解法

容易发现从上向下放更容易考虑,于是首先应该放承重能力差且重量小的方块在上面,但是这种关系仅是一种偏序关系,需要拓展为全序关系。

假定现在已经堆叠了重量为 W 的方块,在下面要放下 i 与 j 两个方块,且最佳策略是 i 在上。那么有

si<W+wj

sj≥W+wi

化简得

si+wi<sj+wj

这样就确定了对于任意两个物品的上下位置的全序关系了。

问题化简为简单的背包,时间复杂度 O(n(w+s+log⁡n))

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define M 20001#define N 1001struct faner {    int w, s, v;} a[N];ll f[M], ans;bool cmp(faner a, faner b) { return a.s + a.w < b.s + b.w; }int n;int main() {    scanf("%d", &n);    for (int i = 1; i <= n; i++) scanf("%d%d%d", &a[i].w, &a[i].s, &a[i].v);    sort(a + 1, a + n + 1, cmp);    for (int i = 1; i <= n; i++)        for (int j = M - 1; j >= a[i].w; j--)            if (a[i].s + a[i].w >= j) f[j] = max(f[j], f[j - a[i].w] + a[i].v);    for (int i = 1; i < M; i++) ans = max(ans, f[i]);    printf("%lld\n", ans);    return 0;}

Y - Grid 2

题意

给定一个网格的高度 n 与宽度 m,与网格中的 k 个障碍。太郎起始在 (1,1),最终要到达 (n,m)。每次太郎可以从 (i,j) 到达 (i,j+1) 或 (i+1,j)。其中障碍格子无法达到。

询问太郎的移动方案数。

解法

容斥原理,用无障碍情况下的方案数减去经过障碍的方案数即可。

时间复杂度 O(n+m+k2)

代码

#include <bits/stdc++.h>using namespace std;#define mod 1000000007#define ll long long#define N 200001int n, m, num;ll f[N] = {1}, fac[N] = {1}, ifac[N] = {1}, inv[N] = {1, 1};ll Calc(int n, int k) { return fac[n] * ifac[k] % mod * ifac[n - k] % mod; }struct faner {    int x, y;} a[N] = {1, 1};bool cmp(faner a, faner b) { return a.x == b.x ? a.y < b.y : a.x < b.x; }int main() {    scanf("%d%d", &n, &m);    for (int i = 2; i <= n + m; i++)        inv[i] = (mod - mod / i) * inv[mod % i] % mod;    for (int i = 1; i <= n + m; i++)        fac[i] = fac[i - 1] * i % mod, ifac[i] = ifac[i - 1] * inv[i] % mod;    scanf("%d", &num);    for (int i = 1; i <= num; i++) scanf("%d%d", &a[i].x, &a[i].y);    sort(a + 1, a + 1 + num, cmp);    a[++num] = {n, m};    for (int i = 0; i < num; i++) {        for (int j = i + 1; j <= num; j++) {            if (a[i].y > a[j].y) continue;            ll sum = Calc(a[j].x - a[i].x + a[j].y - a[i].y, a[j].x - a[i].x);            f[j] = (mod + f[j] - f[i] * sum % mod) % mod;        }    }    printf("%lld\n", (mod - f[num]) % mod);    return 0;}

Z - Frog 3

题意

给定 n 个石头,第 i 个石头的高度为 hi。现在要求小青蛙从 1 号石头跳到 n 号石头,每次小青蛙可以选择从 i 号石头跳到 i+s ( 1≤s≤n−i )号石头,代价是起跳点与落点的高度差的平方加上一个常数 C。询问你最小代价。

解法

可以发现当高差过大时,采取中间跳板会使策略更优,高差小时则应当直接跳。

于是维护一个单调队列,保存多个跳板,每次取最优策略即可。

时间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;#define ll long long#define N 200001ll q[N] = {0, 1}, f[N], h[N], n, c;double slope(int i, int j) {    return (f[i] - f[j] + h[i] * h[i] - h[j] * h[j]) / 2.0 / (h[i] - h[j]);}main() {    scanf("%lld%lld", &n, &c);    for (int i = 1; i <= n; i++) scanf("%lld", &h[i]);    int l = 1, r = 1;    for (int i = 2; i <= n; i++) {        while (l < r && slope(q[l], q[l + 1]) <= h[i]) l++;        f[i] = f[q[l]] + (h[i] - h[q[l]]) * (h[i] - h[q[l]]) + c;        while (l < r && slope(q[r - 1], q[r]) >= slope(q[r], i)) r--;        q[++r] = i;    }    printf("%lld\n", f[n]);    return 0;}

评价

AtCoder的这套 DP 题知识点涵盖广泛,题面描述清晰,题目质量很高,很值得入门选手尝试。