【复习】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;
}
posted @ 2021-10-18 14:13  mango09  阅读(37)  评论(0编辑  收藏  举报
-->