【复习】CSP2021-DP
疯狂A题训练——DP基础篇
1. CF414B Mashmokh and ACM
\(\text{Solution}\)
设 \(dp_{i,j}\) 表示长度为 \(i\) 的数列,最后一个数为 \(j\) 的数列个数。
则 $$dp_{i,j\times k}=\sum\limits_{j\times k\le n}dp_{i-1,j}$$
答案为 \(\sum\limits_{i=1}^n dp_{k,i}\)。
边界为 \(dp_{1,i}=1\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2005;
const int MOD = 1e9 + 7;
int dp[MAXN][MAXN];
int main()
{
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
{
dp[1][i] = 1;
}
for (int i = 2; i <= k; i++)
{
for (int j = 1; j <= n; j++)
{
for (int k = 1; k * j <= n; k++)
{
dp[i][j * k] = (dp[i][j * k] + dp[i - 1][j]) % MOD;
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
ans = (ans + dp[k][i]) % MOD;
}
printf("%d\n", ans);
return 0;
}
2. P1586 四方定理
\(\text{Solution}\)
转化为完全背包。
第一层
for (int i = 1; i * i <= MAXN; i++)
\(i\) 相当于枚举是哪个物品。
第二层
for (int j = i * i; j <= MAXN; j++)
\(j\) 是总重量。
第三层枚举用了多少个平方数。
答案为 \(\sum\limits_{i=1}^4dp_{n,i}\)。
边界为 \(dp_{0,0}=1\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 32768;
int dp[MAXN + 5][5];
int main()
{
dp[0][0] = 1;
for (int i = 1; i * i <= MAXN; i++)
{
for (int j = i * i; j <= MAXN; j++)
{
for (int k = 1; k <= 4; k++)
{
dp[j][k] += dp[j - i * i][k - 1];
}
}
}
int t;
scanf("%d", &t);
while (t--)
{
int n;
scanf("%d", &n);
int ans = 0;
for (int i = 1; i <= 4; i++)
{
ans += dp[n][i];
}
printf("%d\n", ans);
}
return 0;
}
3. P2426 删数
\(\text{Solution}\)
其实顺序没有影响,所以把前面的全删完就剩下后面的了。全部从前往后删。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 105;
int a[MAXN], dp[MAXN];
int dis(int x, int y)
{
return abs(a[x] - a[y]) * (y - x + 1);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", a + i);
}
for (int i = 1; i <= n; i++)
{
dp[i] = dp[i - 1] + a[i];
for (int j = 0; j < i - 1; j++)
{
dp[i] = max(dp[i], dp[j] + dis(j + 1, i));
}
}
printf("%d\n", dp[n]);
return 0;
}
4. P1040 [NOIP2003 提高组] 加分二叉树
\(\text{Solution}\)
区间 \(\rm dp\)。
初始默认没有左子树(右子树答案相等)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
#define int long long
using namespace std;
const int MAXN = 35;
int a[MAXN], dp[MAXN][MAXN], rt[MAXN][MAXN];
void output(int l, int r)
{
if (l > r)
{
return;
}
printf("%lld ", rt[l][r]);
output(l, rt[l][r] - 1);
output(rt[l][r] + 1, r);
}
signed main()
{
int n;
scanf("%lld", &n);
for (int i = 1; i <= n; i++)
{
scanf("%lld", a + i);
dp[i][i] = a[i];
rt[i][i] = i;
}
for (int len = 2; len <= n; len++)
{
for (int i = 1; i + len - 1 <= n; i++)
{
int j = i + len - 1;
dp[i][j] = dp[i + 1][j] + a[i]; //默认没有左子树
rt[i][j] = i; //默认以 i 为根
for (int k = i + 1; k < j; k++)
{
if (dp[i][j] < dp[i][k - 1] * dp[k + 1][j] + a[k])
{
dp[i][j] = dp[i][k - 1] * dp[k + 1][j] + a[k];
rt[i][j] = k;
}
}
}
}
printf("%lld\n", dp[1][n]);
output(1, n);
return 0;
}
5. P1122 最大子树和
\(\text{Solution}\)
注意到要取 \(subtree(u)\) 内的就必须取 \(u\),故设 \(dp_u\) 为 \(subtree(u)\) 中必须取 \(u\) 能取到的最大值。
初始值为 \(dp_u=a_u\)。
\(\forall v\in son(u),dp_u=\max(dp_u,dp_u+dp_v)\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 16005;
int cnt;
int head[MAXN];
struct edge
{
int to, nxt;
}e[MAXN << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
int a[MAXN], dp[MAXN];
void dfs(int u, int fa)
{
dp[u] = a[u];
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (v == fa)
{
continue;
}
dfs(v, u);
dp[u] = max(dp[u], dp[u] + dp[v]);
}
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", a + i);
}
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 0);
int ans = -0x7fffffff;
for (int i = 1; i <= n; i++)
{
ans = max(ans, dp[i]);
}
printf("%d\n", ans);
return 0;
}
6. P1351 [NOIP2014 提高组] 联合权值
\(\text{Solution}\)
直接枚举点对为 \(\mathcal{O}(n^2)\)。
考虑一个节点 \(u\),在它的父亲和儿子们中任选两个,则这两个的距离为 \(2\)。
然后根据乘法分配律计算 \(sum\),贪心地计算 \(max\)。
因为是有序点对所以记得 \(sum\gets sum\times2\)。
注意只有 \(sum\) 要取模。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2e5 + 5;
const int MOD = 10007;
int cnt;
int head[MAXN];
struct edge
{
int to, nxt;
}e[MAXN << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
int w[MAXN];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
scanf("%d", w + i);
}
int maxx = 0, sum = 0;
for (int u = 1; u <= n; u++)
{
int nowm = 0, pre = 0;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
sum = (sum + pre * w[v]) % MOD;
pre = (pre + w[v]) % MOD;
maxx = max(maxx, nowm * w[v]);
nowm = max(nowm, w[v]);
}
}
printf("%d %d\n", maxx, (sum << 1) % MOD);
return 0;
}
7. P1387 最大正方形
\(\text{Solution}\)
设 \(dp_{i,j}\) 为以 \((i,j)\) 为右下角所能构成的最大正方形的边长。
对于右下角为 \((i,j)\) 来说,它一定是右下角为 \((i-1,j),(i,j-1),(i-1,j-1)\) 的交集加上自己
所以有 $$dp_{i,j}=\begin{cases}\min(dp_{i-1,j},dp_{i,j-1},dp_{i-1,j-1})+1&a_{i,j}=1\0&a_{i,j}=0\end{cases}$$
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 105;
int dp[MAXN][MAXN];
int main()
{
int n, m, ans = 0;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int x;
scanf("%d", &x);
if (x)
{
dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
ans = max(ans, dp[i][j]);
}
}
}
printf("%d\n", ans);
return 0;
}
8. P1681 最大正方形II
\(\text{Solution}\)
预处理: \(a_{i,j}\gets a_{i,j}\operatorname{xor}((i\operatorname{xor}j)\operatorname{and}1)\)
相当于行列奇偶性不同的,值都被取反了,那么就变成了最大相同正方形。
注意一个点也可以作为满足条件的正方形,所以要将 \(ans\) 和 \(dp\) 初始化为 \(1\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1505;
int a[MAXN][MAXN], dp[MAXN][MAXN];
int main()
{
int n, m, ans = 1;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
scanf("%d", a[i] + j);
a[i][j] ^= ((i ^ j) & 1);
dp[i][j] = 1;
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if (a[i][j] == a[i - 1][j] && a[i][j] == a[i][j - 1] && a[i][j] == a[i - 1][j - 1])
{
dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
ans = max(ans, dp[i][j]);
}
}
}
printf("%d\n", ans);
return 0;
}
9. P2513 [HAOI2009]逆序对数列
\(\text{Solution}\)
设 \(dp_{i,j}\) 表示 \(1\sim i\) 的排列中逆序对个数为 \(j\) 的排列个数。
假设已经放好了前 \((i-1)\) 个数,现在要把 \(i\) 插♂进去,那么最多会使逆序对个数增加 \((i-1)\)。
所以状态转移方程就是 $$\begin{aligned}dp_{i,j}&=\sum\limits_{l=0}{\min(j,i-1)}dp_{i-1,j-l}\&=\sum\limits_{l=\max(0,j-i+1)}jdp_{i-1,l}\end{aligned}$$
写出来就是
for (int i = 2; i <= n; i++)
{
for (int j = 0; j <= k; j++)
{
for (int l = max(0, j - i + 1); l <= j; l++)
{
dp[i][j] = (dp[i][j] + dp[i - 1][l]) % MOD;
}
}
}
这个是 \(\mathcal{O}(nk^2)\) 的,你可以那它去做 P1521 求逆序对。
对于本题,观察到上面那个式子其实只有 \(dp_{i-1,?}\) 这一层转移过来,所以可以用前缀和优化。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1005;
const int MOD = 1e4;
int dp[MAXN][MAXN];
int main()
{
int n, k;
scanf("%d%d", &n, &k);
dp[1][0] = 1;
for (int i = 2; i <= n; i++)
{
int sum = 0;
for (int j = 0; j <= k; j++)
{
sum = (sum + dp[i - 1][j]) % MOD; //第 (i - 1) 层的前缀和
dp[i][j] = sum;
if (j - i + 1 >= 0)
{
sum = ((sum - dp[i - 1][j - i + 1]) % MOD + MOD) % MOD; //多出范围的部分要从前缀和中减去
}
}
}
printf("%d\n", dp[n][k]);
return 0;
}
10. [P1107 [BJWC2008]雷涛的小猫](P1107 [BJWC2008]雷涛的小猫)
\(\text{Solution}\)
设 \(dp_{i,j}\) 为当前到第 \(i\) 棵树且当前高度为 \(j\) 能吃到的最多柿子数。
那么显然有 $$dp_{i,j}=\max(dp_{i,j+1},\max\limits_{k=1}^n{dp_{k,j+delta}})+c_{i,j}$$,其中 \(c_{i,j}\) 为第 \(i\) 棵树 \(j\) 高度上的柿子数量。
因为 \(delta\ge1\),所以一定有 \(dp_{i,j+1}\ge dp_{i,j+delta}\),所以 \(k=i\) 时转移也没有关系。
这个是 \(\mathcal{O}(n^2h)\) 的。
发现后面那一半全是 \(dp_{?,j+delta}\),所以可以用 \(maxx_i\) 保存 \(\max\limits_{k=1}^n\{dp_{k,i}\}\)。
这样就能做到 \(\mathcal{O}(nh)\) 了。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2005;
int c[MAXN][MAXN], dp[MAXN][MAXN], maxx[MAXN];
int main()
{
int n, h, delta;
scanf("%d%d%d", &n, &h, &delta);
for (int i = 1; i <= n; i++)
{
int t;
scanf("%d", &t);
while (t--)
{
int x;
scanf("%d", &x);
c[i][x]++;
}
}
for (int i = h; i >= 0; i--)
{
for (int j = 1; j <= n; j++)
{
dp[j][i] = max(dp[j][i + 1], maxx[i + delta]) + c[j][i];
maxx[i] = max(maxx[i], dp[j][i]);
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
ans = max(ans, dp[i][0]);
}
printf("%d\n", ans);
return 0;
}
11. P4290 [HAOI2008]玩具取名
\(\text{Solution}\)
区间 \(\rm dp\)。
\(\text{W}\to1,\text{I}\to2,\text{N}\to3,\text{G}\to4\)。
设 \(dp_{i,j,k}(k\in\{1,2,3,4\})\) 表示区间 \(i\sim j\) 内的字母能否组成成 \(k\)。
在输入的时候记录一下 \(vis\) 数组,\(vis_{a,b,c}\) 表示 \(a\) 能否被 \(b,c\) 替代。
然后枚举左端点 \(i\)、右端点 \(j\) 和中间断点 \(k\),那么如果 \(i\sim k\) 能组成 \(b\),\((k+1)\sim j\) 能组成 \(c\),\(a\) 又能由 \(b,c\) 替代,那么区间 \(i\sim j\) 就能组成 \(a\)。
则转移就是
if (dp[i][k][b] && dp[k + 1][j][c] && vis[a][b][c])
{
dp[i][j][a] = true;
}
读入字符串 \(S\) 后对于每一位 \(S_i\) 都令 \(dp_{i,i,S_i}\gets true\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 205;
int val(char c)
{
switch (c)
{
case 'W':
return 1;
case 'I':
return 2;
case 'N':
return 3;
case 'G':
return 4;
}
}
char c(int val)
{
switch (val)
{
case 1:
return 'W';
case 2:
return 'I';
case 3:
return 'N';
case 4:
return 'G';
}
}
int cnt[5];
bool vis[5][5][5], dp[MAXN][MAXN][5];
int main()
{
for (int i = 1; i <= 4; i++)
{
scanf("%d", cnt + i);
}
for (int i = 1; i <= 4; i++)
{
for (int j = 1; j <= cnt[i]; j++)
{
char s[5];
scanf("%s", s);
vis[i][val(s[0])][val(s[1])] = true;
}
}
char s[MAXN];
scanf("%s", s + 1);
int Len = strlen(s + 1);
for (int i = 1; i <= Len; i++)
{
dp[i][i][val(s[i])] = true;
}
for (int len = 2; len <= Len; len++)
{
for (int i = 1; i + len - 1 <= Len; i++)
{
int j = i + len - 1;
for (int k = i; k < j; k++)
{
for (int a = 1; a <= 4; a++)
{
for (int b = 1; b <= 4; b++)
{
for (int c = 1; c <= 4; c++)
{
if (dp[i][k][b] && dp[k + 1][j][c] && vis[a][b][c])
{
dp[i][j][a] = true;
}
}
}
}
}
}
}
bool flag = true;
for (int i = 1; i <= 4; i++)
{
if (dp[1][Len][i])
{
flag = false;
printf("%c", c(i));
}
}
if (flag)
{
puts("The name is wrong!");
}
return 0;
}
12. P5020 [NOIP2018 提高组] 货币系统
\(\text{Solution}\)
显然大货币是不可能凑出小货币的,所以将 \(a\) 数组从小到大排序。
设 \(dp_i\) 表示面额为 \(i\) 的货币能否被凑出。
\(i\) 从 \(1\) 遍历到 \(n\),若 \(dp_{a_i}=true\),说明 \(a_i\) 可以由前面的凑出,那么它就不需要。
然后 \(j\) 从 \(a_i\) 遍历到 \(a_n\),\(j\) 能由 \((j-a_i)\) 和 \(a_i\) 凑出,即 $$dp_j\gets dp_j\operatorname{or}dp_{j-a_i}$$。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 25005;
int a[MAXN];
bool dp[MAXN];
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
memset(dp, false, sizeof(dp));
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", a + i);
}
sort(a + 1, a + n + 1);
int ans = n;
dp[0] = true;
for (int i = 1; i <= n; i++)
{
if (dp[a[i]])
{
ans--;
continue;
}
for (int j = a[i]; j <= a[n]; j++)
{
dp[j] = dp[j] || dp[j - a[i]];
}
}
printf("%d\n", ans);
}
return 0;
}
13. P1510 精卫填海
\(\text{Solution}\)
简单的 \(01\) 背包。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1e4 + 5;
int a[MAXN], b[MAXN], dp[MAXN];
int main()
{
int v, n, c;
scanf("%d%d%d", &v, &n, &c);
for (int i = 1; i <= n; i++)
{
scanf("%d%d", a + i, b + i);
}
for (int i = 1; i <= n; i++)
{
for (int j = c; j >= b[i]; j--)
{
dp[j] = max(dp[j], dp[j - b[i]] + a[i]);
}
}
for (int i = 0; i <= c; i++)
{
if (dp[i] >= v) //遇到的第一个满足的会使 (c - i) 最大,直接输出
{
printf("%d\n", c - i);
return 0;
}
}
puts("Impossible");
return 0;
}
14. P2563 [AHOI2001]质数和分解
\(\text{Solution}\)
质数筛 \(+\) 完全背包。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 200;
int p[MAXN + 5];
bool vis[MAXN + 5];
void pre()
{
for (int i = 2; i <= MAXN; i++)
{
if (!vis[i])
{
p[++p[0]] = i;
}
for (int j = 1; j <= p[0] && i * p[j] <= MAXN; j++)
{
vis[i * p[j]] = true;
if (i % p[j] == 0)
{
break;
}
}
}
}
int dp[MAXN];
int main()
{
pre();
dp[0] = 1;
for (int i = 1; i <= p[0]; i++)
{
for (int j = p[i]; j <= MAXN; j++)
{
dp[j] += dp[j - p[i]];
}
}
int n;
while (~scanf("%d", &n))
{
printf("%d\n", dp[n]);
}
return 0;
}
15. P2017 [USACO09DEC]Dizzy Cows G
\(\text{Solution}\)
先输入有向边,用 \(\rm bfs\) 跑拓扑排序。读入无向边 \((x,y)\),如果 \(x\) 的 \(\rm bfs\) 序比 \(y\) 的 \(\rm bfs\) 序大,那么 \(x\) 一定没有有向边指向 \(y\)(题目保证无环),所以可以从 \(y\) 连一条有向边到 \(x\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int MAXN = 1e5 + 5;
int cnt;
int head[MAXN];
struct edge
{
int to, nxt;
}e[MAXN];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
int n, p1, p2, Time;
int in[MAXN], bfn[MAXN];
void topo()
{
queue<int> q;
for (int i = 1; i <= n; i++)
{
if (!in[i])
{
q.push(i);
}
}
while (!q.empty())
{
int u = q.front();
q.pop();
bfn[u] = ++Time;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!--in[v])
{
q.push(v);
}
}
}
}
int main()
{
scanf("%d%d%d", &n, &p1, &p2);
for (int i = 1; i <= p1; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
in[v]++;
}
topo();
for (int i = 1; i <= p2; i++)
{
int u, v;
scanf("%d%d", &u, &v);
if (bfn[u] < bfn[v])
{
printf("%d %d\n", u, v);
}
else
{
printf("%d %d\n", v, u);
}
}
return 0;
}