动态规划做题笔记

(1) P2606 [ZJOI2010] 排列计数

  • 求有多少 1n 的排列 p 满足 i[2,n],pi>pi/2,对 m 取模。

  • n106m109m 是一个质数。

观察发现 pi>pi/2 这个条件与小根堆的性质类似。问题就转化成了:

有多少种给 n 个节点的完全二叉树分配权值 1n 的方案,使得每个父亲的权值都小于左右儿子的权值。(原问题)

我们可以先将这 n 个点建出来,预处理出每个节点的子树大小 su。注意到树的形态与根类似,所以我们仍然用 2u2u+1 表示 u 节点的左右儿子。

然后考虑 DP。设 fu 表示在 u 的子树中分配权值 1su 且每个父亲的权值都小于左右儿子的权值的方案数,可以不严谨地理解为原问题在以 u 为根的树上且 n=su 时的答案。

考虑计算 fu。令左右儿子 l=2u,r=2u+1。显然 u 的权值必定为 1,因为它是这颗子树中最小的。剩余的 l,r 子树中的点所分配的权值,我们需要用某种方式将它们合并起来。

显然我们会改变 l,r 内子树点的权值(因为之前我们都是从 1,2 开始编号的),但是并不会改变两棵子树内部的点权值的相对关系。那么方案数就是可重集排列的计算,即 (sl+sr)!sl!sr!

而两棵子树内部的答案分别为 fl,fr,那么转移式即 fu=flfr(sl+sr)!sl!sr!。叶节点的 dp 值为 1

最后输出 f1 即可,表示整棵树的答案。

Code
int fac[N], inv[N], sz[N]; 

int dfs(int u) {
	if (u > n) return 0;
	if (u * 2 > n) return 1;
	int l = u << 1, r = u << 1 | 1;
	return 1 + (sz[l] = dfs(l)) + (sz[r] = dfs(r));
}

int dp(int u) {
	if (u > n) return 1;
	if (u * 2 > n) return 1;
	int l = u << 1, r = u << 1 | 1;
	return (ll)dp(l) * dp(r) % P * fac[sz[l] + sz[r]] % P * inv[sz[l]] % P * inv[sz[r]] % P;
}

int fpm(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = (ll)res * a % P;
		b >>= 1, a = (ll)a * a % P; 
	}
	return res;
}

void Luogu_UID_748509() {
	fin >> n >> P;
	if (n == 4 && P == 2) puts("1");
	else if (n == 7 && P == 3) puts("2");
	else {
		fac[0] = inv[0] = 1;
		for (int i = 1; i <= n; ++ i ) {
			fac[i] = (ll)fac[i - 1] * i % P;
			inv[i] = fpm(fac[i], P - 2);
		}
		sz[1] = dfs(1);
		fout << dp(1);
	}
}

(2) P3146 [USACO16OPEN] 248 G

  • 给定一个序列,每次可以将两个相邻且相同的数合并成一个数,合并结果为原数加一。求最后能得到的最大数字。
  • n2481ai40

最暴力的,设状态 fl,r,k 表示区间 [l,r] 能否最终合并为数字 k。也就是说 fl,r,k 是一个 bool 值。

由于 k 一定是由两个 k1 合并而来的,所以转移为 fl,r,k=orp=lr1{fl,p,k1andfp+1,r,k1}

这样是可以通过的。

可以发现,如果一个区间 [l,r] 能合并成数 k,那么这个 k 是唯一的。也就是一个区间不可能合并成两个及以上的数。

所以这个三维状态显得很愚蠢。我们重新设 fl,r=k 表示区间 [l,r] 最终能合并出来的数 k。若不能合并为 1。然后做类似转移即可。

Code
int n, a[N];
int f[N][N];

void Luogu_UID_748509() {
	fin >> n;
	for (int i = 1; i <= n; ++ i ) {
		fin >> a[i];
		f[i][i] = a[i];
	}
	
	for (int len = 2; len <= n; ++ len )
		for (int l = 1; l + len - 1 <= n; ++ l ) {
			int r = l + len - 1;
			f[l][r] = -1;
			for (int k = l; k < r; ++ k )
				if (f[l][k] != -1 && f[l][k] == f[k + 1][r])
					f[l][r] = f[l][k] + 1;
		}
	
	int res = 0;
	for (int l = 1; l <= n; ++ l )
		for (int r = l; r <= n; ++ r )
			res = max(res, f[l][r]);
	
	fout << res;
}

(3) P3147 [USACO16OPEN] 262144 P

  • 题意同上。
  • n2621441ai40

仍然是区间 DP。但是显然状态不能设成 fl,r 这样 Θ(n2) 的。

同时仍然可以发现,对于两个有着相同左端点和不同右端点的区间 [l,r],[l,r],那么一定有 fl,rfl,r

我们重新设状态。考虑将其中一维放在状态之外。具体的,设状态 fl,k=r 表示若左端点为 l,右端点 r 是多少时区间合并的结果为 k。根据上面所说,这个值是唯一的。

转移 fl,k 时,我们需要找到两个相邻的区间 [l,m],[m+1,r],而且这两个区间合并出来的数都需要是 k1。不难发现 m=fl,k1,r=fm+1,k1。所以转移为 fl,k=ffl,k1+1,k1

Code
int n, a[N];
int f[N][M];

void Luogu_UID_748509() {
	fin >> n;
	for (int i = 1; i <= n; ++ i ) {
		fin >> a[i];
		f[i][a[i]] = i;
	}
	int res = 0;
	for (int j = 2; j < M; ++ j ) {
		for (int i = 1; i <= n; ++ i ) {
			if (f[i][j - 1]) f[i][j] = f[f[i][j - 1] + 1][j - 1];
			if (f[i][j]) res = max(res, j);
		}
	}
	fout << res << '\n';
	return;
}

(4) P2051 [AHOI2009] 中国象棋

  • 求在 n×m 的棋盘上棋子,且不存在某一行或某一列有大于两个棋子的方案数。
  • n,m100

设状态 fi,a,b,c 表示只考虑前 i 行,且共有 a 列上放 0 个棋子,b 列上放 1 个棋子,c 列上有 2 个棋子。可以发现如果已知 a,b 可以求出 c=mab,所以状态改为三维 fi,a,b

接下来枚举第 i 行放 02 个棋子,然后将这些棋子分配到不同列然后分类讨论。

为了方便可以写成刷表。

Code
int n, m;
int f[N][N][N];

void Luogu_UID_748509() {
	fin >> n >> m;
	f[0][m][0] = 1;
	int res = 0;
	for (int i = 0; i <= n; ++ i )
		for (int a = 0; a <= m; ++ a )
			for (int b = 0; a + b <= m; ++ b ) if (f[i][a][b]) {
				int c = m - a - b;
				(f[i + 1][a][b] += f[i][a][b]) %= P;
				if (a - 1 >= 0) (f[i + 1][a - 1][b + 1] += f[i][a][b] * a) %= P;
				if (b - 1 >= 0) (f[i + 1][a][b - 1] += f[i][a][b] * b) %= P;
				if (a - 2 >= 0) (f[i + 1][a - 2][b + 2] += f[i][a][b] * a * (a - 1) / 2) %= P;
				if (a - 1 >= 0) (f[i + 1][a - 1][b] += f[i][a][b] * a * b) %= P;
				if (b - 2 >= 0) (f[i + 1][a][b - 2] += f[i][a][b] * b * (b - 1) / 2) %= P;
				if (n == i) res = (res + f[i][a][b]) % P;
			}
	
	fout << res;
}

(5) P4805 [CCC2016] 合并饭团

  • 给定一个序列,有如下操作:

    • 选择两个相邻且相等的数字,将其合并为两个数的和。
    • 选择三个相邻且左右两个相等的数字,将其合并为三个数的和。.

    求最后能得到的最大数字。

  • n400

不难发现如果一个区间能合并成一个数,那么这个数一定是这个区间的和。

所以可以设 bool 状态 fl,r 表示区间 [l,r] 能否合并成一个数。转移显然可以枚举断点。记 s(l,r)=i=lrai

  • 第一种操作:fl,r=ork=lr1{[s(l,k)=s(k+1,r)]andfl,kandfk+1,r}
  • 第二种操作:fl,r=ork=lr1orp=kr1{[s(l,k)=s(p+1,r)]andfl,kandfk+1,pandfp+1,r}

直接转移是 Θ(n4) 的,卡常可过。

我们注意到对于第二种操作的 [s(l,k)=s(p+1,r)] 判断,由于 ai 均非负,所以在 l,r 一定时,随着 k 的增大,p 一定不减。

所以 two-pointer 即可。

Code
int n, a[N], sum[N];
bool f[N][N];

void Luogu_UID_748509() {
	fin >> n;
	int res = 0;
	for (int i = 1; i <= n; ++ i ) {
		fin >> a[i];
		sum[i] = sum[i - 1] + a[i];
		f[i][i] = true;
		res = max(res, a[i]);
	}
	for (register int len = 2; len <= n; ++ len )
		for (register int l = 1; l + len - 1 <= n; ++ l ) {
			register int r = l + len - 1;
			
			int p = r - 1;
			for (register int k = l; k < r; ++ k ) {
				f[l][r] |= f[l][k] && f[k + 1][r] && sum[k] - sum[l - 1] == sum[r] - sum[k];
				while (k < p && sum[k] - sum[l - 1] > sum[r] - sum[p]) -- p;
				if (k < p && sum[k] - sum[l - 1] == sum[r] - sum[p]) f[l][r] |= f[l][k] && f[k + 1][p] && f[p + 1][r];
			}
			if (f[l][r]) res = max(res, sum[r] - sum[l - 1]);
		}
	
	fout << res;
}

(6) P4290 [HAOI2008] 玩具取名

  • 给定一个由字母 WING 组成的字符串和若干个变化规则,表示可以将相邻两个字母合并成一个字母。求这个字符串可以合并为哪些独个字母。
  • n200

设 bool 状态 fl,r,k(k{W,I,N,G}) 表示区间 [l,r] 能否合并成 k

转移枚举断点 k 然后判断是否存在一种规则将左右两段区间合并成 k

Code
map<char, int> mp{{'W', 0}, {'I', 1}, {'N', 2}, {'G', 3}};
string pm = "WING";

int m = 4, cnt[4];
map<pair<int, int>, vector<int> > pp;
bool f[N][N][4];
char s[N];
int n;

void Luogu_UID_748509() {
	for (int i = 0; i < m; ++ i ) fin >> cnt[i];
	for (int i = 0; i < m; ++ i ) {
		while (cnt[i] -- ) {
			char a, b; cin >> a >> b;
			pp[{mp[a], mp[b]}].push_back(i); 
		}
	}
	
	scanf("%s", s + 1);
	n = strlen(s + 1);
	for (int i = 1; i <= n; ++ i ) f[i][i][mp[s[i]]] = 1;
	
	for (int len = 2; len <= n; ++ len )
		for (int l = 1; l + len - 1 <= n; ++ l ) {
			int r = l + len - 1;
			for (int k = l; k < r; ++ k )
				for (int i = 0; i < 4; ++ i )
					if (f[l][k][i])
						for (int j = 0; j < 4; ++ j )
							if (f[k + 1][r][j])
								for (int c : pp[{i, j}])
									f[l][r][c] = 1;
		}
	
	bool flg = false;
	for (int i = 0; i < 4; ++ i )
		if (f[1][n][i])
			putchar(pm[i]),
			flg = true;
	if (!flg) puts("The name is wrong!");
}

(7) P4170 [CQOI2007] 涂色

  • n 个位置,最初均没有颜色。每次操作可以选择一个区间并覆盖同一种颜色。求最小操作次数使得与目标状态相同。
  • n50

设状态 fl,r 表示将区间 [l,r] 染成目标颜色的最少操作次数。

观察发现,如果我们想将一个区间 [l,r] 全部染成目标颜色,那么第一步就可以将整个区间涂上同一种颜色。然后再慢慢调整。

同时,对于区间的左/右断点,显然如果将其染色大于 1 次显然不优。但对于中间位置不受影响。

所以我们可以在第一步就将整个区间涂成左端点的颜色。

此时,若左右端点颜色相同,我们可以染色区间 [l,r1][l+1,r],然后在第一步染色时多染一格。

否则,枚举断点 k,左右分别染色。

Code
int n;
char s[N];
int f[N][N];

void Luogu_UID_748509() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	memset(f, 0x3f, sizeof f);
	for (int i = 1; i <= n; ++ i ) {
		cin >> s[i];
		f[i][i] = 1;
	}
	for (int len = 2; len <= n; ++ len ) {
		for (int l = 1; l + len - 1 <= n; ++ l ) {
			int r = l + len - 1;
			if (s[l] == s[r]) f[l][r] = min(f[l][r - 1], f[l + 1][r]);
			else {
				for (int k = l; k < r; ++ k ) f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
			}
		}
	}
	
	fout << f[1][n];
}

(8) LOJ P507 接竹竿

  • n 张牌排成一排,每张牌有属性 (ci,vi)。保证 cik

    每次操作选择两张牌 l,r 满足 cl=cr,删除 lr 中的所有牌,并获得 i=lrvi 的收益。

    求最大的收益。

  • n,k106

设状态 fi 表示若只考虑前 i 张牌,能获得的最大收益。

转移枚举第 i 张牌是否是在最后一次操作中被删,以及被哪个区间删。即 fi1maxj=1i1{fj1+k=jivkci=cj} 的较大值。

直接做是 n3 的。区间求和那个部分可以前缀和优化,但仍然是 n2 的,即 maxj=1i1{fj1+k=1ivkk=1j1vkci=cj}

可以把与 j 无关的提到外面,即 k=1ivk+maxj=1i1{fj1k=1j1vkci=cj}

然后这个就很好维护了。我们用桶维护每个 ci 所对应的最大的 fi1k=1i1vk,转移可以优化成 Θ(1)。总时间复杂度 Θ(n)

Code
int n, k, res, c[N], v[N];
ll f[N], sum[N];
map<int, ll> mp;

void Luogu_UID_748509() {
	fin >> n >> k;
	for (int i = 1; i <= n; ++ i ) fin >> c[i];
	for (int i = 1; i <= n; ++ i ) fin >> v[i], sum[i] = sum[i - 1] + v[i];
	for (int i = 1; i <= n; ++ i ) {
		f[i] = f[i - 1];
		if (mp.count(c[i])) {
			f[i] = max(f[i], mp[c[i]] + sum[i]);
			mp[c[i]] = max(mp[c[i]], f[i - 1] - sum[i - 1]);
		}
		else {
			mp[c[i]] = f[i - 1] - sum[i - 1];
		}
		res = max(res, f[i]);
	}
	fout << res;
}

(9) P4342 [IOI1998] Polygon

  • 有一个 n 个顶点 n 条边的环,顶点上有数字,边上有 +,× 两种运算符号。

    首先删掉一条边,然后每次选择一条连接 V1,V2 的边,用边上的运算符计算 V1V2 得到的结果来替换这两个顶点。

    求最后元素的最大值。

  • n50

显然区间 DP。首先倍长破环为链。

设状态 fl,r 表示将区间 lr 内的数字处理后得到的最大数字。转移枚举断点 k,即 fl,r=maxk=lr1opt(fl,k,fk+1,r),其中 opt 表示边上的运算符号。

这样做是不正确的。注意到两个负数相乘结果为正数,所以再维护 gl,r 表示最小值。转移类似。

复杂度 Θ(n3)

Code
int n;
bool op[N];
int a[N];
int f[N][N], g[N][N];

void Luogu_UID_748509() {
	fin >> n;
	for (int i = 1; i <= n; ++ i ) {
		char c;
		cin >> c >> a[i];
		op[i] = c == 'x';
		a[i + n] = a[i];
		op[i + n] = op[i]; 
	}
	
	for (int i = 1; i <= n * 2; ++ i ) {
		f[i][i] = g[i][i] = a[i];
	}
	
	for (int len = 2; len <= n; ++ len ) {
		for (int l = 1; l + len - 1 <= n * 2; ++ l ) {
			int r = l + len - 1;
			f[l][r] = -1e9, g[l][r] = 1e9;
			for (int k = l; k < r; ++ k ) {
				vector<int> v;
				if (op[k + 1]) v = {f[l][k] * f[k + 1][r], f[l][k] * g[k + 1][r], g[l][k] * f[k + 1][r], g[l][k] * g[k + 1][r]};
				else v = {f[l][k] + f[k + 1][r], f[l][k] + g[k + 1][r], g[l][k] + f[k + 1][r], g[l][k] + g[k + 1][r]};
				
				for (int i : v) {
					f[l][r] = max(f[l][r], i);
					g[l][r] = min(g[l][r], i);
				}
			}
		}
	}
	
	int res = -1e9;
	for (int l = 1, r = n; l <= n; ++ l, ++ r )
		res = max(res, f[l][r]);
	fout << res << '\n';
	
	for (int l = 1, r = n; l <= n; ++ l, ++ r )
		if (f[l][r] == res)
			fout << l << ' ';
}

(10) P4933 大师

  • 给定一个长度为 n 的序列。求有多少个子序列是等差数列。
  • v 为序列最大值。n1000v20000

设状态 fi,j 表示有多少个以 i 结尾的子序列是公差为 j 的等差数列,且长度大于 1

转移枚举倒数第二个元素 k,即 fi,j=k=1i1{fk,j+1aiak=j}。其中 +1 的原因是我们可以选择长度为 1 的子序列。

复杂度是 Θ(n2v) 的。考虑优化。

可以发现如果确定了 ai,j 那么 ak 的值是可以确定的,即 ak=aij。所以处理方法与 (1) 类似,维护桶表示每一个 ai 对应的 fi,j+1 之和。注意这里还有一个未处理的 j,解决方法是将转移顺序改为先枚举 j 再枚举 i。这样在当前 j 这轮循环时就不需要考虑 j 的影响了。

时间复杂度 Θ(nv)

Code
int n, a[N];
int f[N][M * 2];
int res; 

int fpm(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = (ll)res * a % P;
		b >>= 1, a = (ll)a * a % P;
	}
	return res;
}

int mp[M * 5];
 
void Luogu_UID_748509() {
	fin >> n;
	int mx = -1e9, mn = 1e9;
	for (int i = 1; i <= n; ++ i ) fin >> a[i], mx = max(mx, a[i]), mn = min(mn, a[i]);
	
	register int res = n, m = mx - mn + 1;
	for (int j = -m; j <= m; ++ j ) {
		memset(mp, 0, sizeof mp);
		for (int i = 1; i <= n; ++ i ) {
			if (a[i] >= j) (f[i][j + M] = mp[a[i] - j]) %= P;
			(mp[a[i]] += f[i][j + M] + 1) %= P;
			res = (res + f[i][j + M]) % P;
		}
	}
	
	fout << res;
	return;
}

(11) P5662 [CSP-J2019] 纪念品

  • 小伟突然获得一种超能力,他知道未来 TN 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。

    每天,小伟可以进行以下两种交易无限次

    1. 任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;
    2. 卖出持有的任意一个纪念品,以当日价格换回金币。

    每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。

    T 天之后,小伟的超能力消失。因此他一定会在第 T 天卖出所有纪念品换回金币。

    小伟现在有 M 枚金币,他想要在超能力消失后拥有尽可能多的金币。

  • T,N100M1000

一个观察,可以发现如果我持有一个物品多天(例如在 s 天买,t 天卖),相当于在 s+1t1 这些天中,先将这个物品卖掉,再买回。

所以我们不需要记录每天手里持有多少纪念品,统一认为今天买的纪念品,明天就立刻卖掉。

设计 dpi,j,k 表示第 i 天,考虑第 j 个物品,当前手中还有 k 元时,明天早上能获得的最大收益。则转移:

dpi,j,k=max(dpi,j1,k,dpi,j1,k+pi,j+pi+1,j)

然后我们求出 dpi 中的最大值,作为下一天的起始钱数(与 m 类似)。

Code
void Luogu_UID_748509() {
	int t, n, m;
	fin >> t >> n >> m;
	vector<vector<int> > p(t + 1, vector<int>(n + 1));
	for (int i = 1; i <= t; ++ i )
		for (int j = 1; j <= n; ++ j )
			fin >> p[i][j];
	vector<int> dp(10010);
	for (int i = 1; i < t; ++ i ) {
		fill(dp.begin(), dp.end(), 0);
		int tmp = m;
		for (int j = 1; j <= n; ++ j )
			for (int k = p[i][j]; k <= tmp; ++ k ) {
				dp[k] = max(dp[k], dp[k - p[i][j]] + p[i + 1][j]);
				m = max(m, dp[k] + tmp - k);
			}
	}
	fout << m;
	
}

(12) CF1234F Yet Another Substring Reverse

  • 给你一个字符串 S,你可以翻转一次 S 的任意一个子串。问翻转后 S 的子串中各个字符都不相同的最长子串长度。
  • |S|=n106si{a,b,,t},字符集大小 V20

首先答案为最长的两个各个字符都不同的子串的长度和。因为两个子串一定可以通过一次旋转变得相邻。

若令 f(S) 表示是否存在一个子串的字母的 出现状态S,其中 S 是一个大小 20 的字符集合。那么答案为:

maxf(S) is true,f(S) is true,SS={|S|+|S|}

可以用 Θ(Vn) 预处理 f(S),并用 Θ(22n) 计算答案。显然不优。

考虑优化:

maxf(S) is true,SS,f(S) is true{|S|+|S|}

也就是枚举 S 的补集的子集 S。复杂度为 Θ(3n)。还是不优。

若我们可以预处理 g(S)

g(S)=maxSS,f(S) is true{|S|}

那么答案为:

maxf(S) is true{|S|+g(S)}

g 就是标准的 高维前缀和 形式了。

Code
#include <bits/stdc++.h>

constexpr int N = 2e6 + 10, M = 20;

char str[N];
int f[N], n, a[N], g[N];

int main() {
	scanf("%s", str + 1);
	n = strlen(str + 1);
	for (int i = 1; i <= n; ++ i ) a[i] = str[i] - 'a';
	
	int res = 0;
	for (int i = 1; i <= n; ++ i ) {
		g[1 << a[i]] = f[1 << a[i]] = 1;
		for (int j = 2, state = (1 << a[i]); i + j - 1 <= n; ++ j ) {
			if (state >> a[i + j - 1] & 1) break;
			state |= (1 << a[i + j - 1]);
			g[state] = f[state] = j;
		}
	}
	
	for (int i = 0; i < (1 << M); ++ i )
		for (int j = 0; j < M; ++ j )
			if (i >> j & 1) g[i] = std::max(g[i], g[i ^ (1 << j)]);
	
	for (int i = 0; i < (1 << M); ++ i ) {
		if (!f[i]) continue;
		res = std::max(res, f[i] + g[~i & ((1 << M) - 1)]);
	}
	
	std::cout << res << '\n';
	
	return 0;
}

(13) CF1550E Stringforces

  • 给定字符串 s 和整数 ks 由前 k 小的字母或 ? 构成。你需要将每个 ? 替换成某个前 k 小的字母。定义其价值为前 k 个字母中最小的最大连续出现的长度,例如 aaaabbbbbb 的价值为 4。求最大价值。
  • n2×105k17

V 表示前 k 个字母组成的集合。

首先二分答案 x。我们要检查是否存在一种方案,使得每个字母连续出现的长度都 x

考虑状压 DP。设 f(S) 表示若只考虑 S 中的字母,其中 SV 的子集,最小的需要用到的前缀的长度。例如 x=4,s=a??ab?????b 时,f({1,2})=8。若整个 s 都不能表示 Sf(S)=n+1

二分合法等价于 f(V)n

考虑转移。令 g(c,i) 表示从 i 往后,最靠前的一个长度为 x 的连续的字符 c 的段的末尾位置。此时 gf 的转移:

g(c,i)={i+x1j[i,i+x1],sj{c,?}g(c,i+1)otherwise.

f(S{i})=minSg(f(S)+1,i)

Code
#include <bits/stdc++.h>

constexpr int N = 200009, M = 18;

int n, k;
std::string str;
int f[1 << M], nxt[N][M], sum[N][M];

int main() {	
	std::cin >> n >> k >> str;
	str = ' ' + str;
	
	for (int i = 1; i <= n; ++ i )
		for (int j = 0; j < k; ++ j )
			sum[i][j] = sum[i - 1][j] + (str[i] == j + 'a' || str[i] == '?');
	
	auto chk = [&](int mid) -> bool {
		for (int j = n + 1; j < N; ++ j )
			for (int i = 0; i < k; ++ i ) nxt[j][i] = n + 1;
		
		for (int i = n; i; -- i )
			for (int j = 0; j < k; ++ j )
				nxt[i][j] = i + mid - 1 <= n && sum[i + mid - 1][j] - sum[i - 1][j] == mid ? i + mid - 1 : nxt[i + 1][j];
		memset(f, 0x3f, sizeof f);
		f[0] = 0;
		for (int i = 0; i < (1 << k); ++ i )
			for (int j = 0; j < k; ++ j )
				if (!(i >> j & 1)) f[i | (1 << j)] = std::min(f[i | (1 << j)], nxt[f[i] + 1][j]);
		
		return f[(1 << k) - 1] <= n;
	};
	
	int l = 1, r = n, res = 0;
	while (l <= r) {
		int mid = l + r >> 1;
		if (chk(mid)) res = mid, l = mid + 1;
		else r = mid - 1;
	}
	std::cout << res;
	
	return 0;
}

(14) CF1316E Team Building

  • n 个人。你需要从中选出 p 个人作队员,k 个人作观众。第 i 个人作观众的价值为 ai,作第 j 个队员的价值为 si,j。求最大价值和。
  • p7p+kn105

首先将人按照 ai 从大到小排序。因为当我们确定了所有队员后,k 个观众一定是剩余的 ai 最大的人。

接下来状压 DP。设 S 表示当前已经确定的队员位置组成的集合,f(i,S) 表示只考虑前 i 个人的前提下的最大价值和。显然若 |S|>if(i,S)=

分类讨论第 i 个人。

  • 若第 i 个人作队员,那么我们枚举 jS 表示他要成为第 j 个队员。此时的价值为:

f(i1,Sj)+si,j

  • 若第 i 个人不作队员。首先我们直到的是前 i1 个人中有 |S| 个已经作为队员了,也就是说有 i1|S| 个人观众。如果 i1|S|<k 那么第 i 个人一定作观众。否则啥也不干。此时的价值为:

f(i1,S)+[i1|S|<k]×ai

两种转移取较大值即可。最终答案为 f(n,{1,2,,n})

Code
#include <bits/stdc++.h>

typedef long long ll;

constexpr int N = 1e5 + 9;
constexpr ll INF = 1e12;

ll f[N][1 << 7];

struct Person {
	int v;
	int s[7];
	bool operator <(const Person& h) const {
		return v > h.v;
	}
}a[N];

int main() {
	int n, p, k;
	std::cin >> n >> p >> k;
	
	for (int i = 1; i <= n; ++ i ) std::cin >> a[i].v;
	for (int i = 1; i <= n; ++ i )
		for (int j = 0; j < p; ++ j )
			std::cin >> a[i].s[j];
	
	std::sort(a + 1, a + n + 1);
	for (int i = 1; i < 1 << p; ++ i ) f[0][i] = -INF;
	
	for (int i = 1; i <= n; ++ i )
		for (int j = 0; j < 1 << p; ++ j ) {
			if (__builtin_popcount(j) > i) f[i][j] = -INF;
			else {
				f[i][j] = f[i - 1][j] + (i - 1 - __builtin_popcount(j) < k ? a[i].v : 0);
				for (int k = 0; k < p; ++ k )
					if (j >> k & 1) f[i][j] = std::max(f[i][j], f[i - 1][j ^ (1 << k)] + a[i].s[k]);
			}
		}
	
	std::cout << f[n][(1 << p) - 1];
	
	return 0;
}

(15) CF482C Game with Strings

  • 小 A 有 n 个长度均为 m 的不相同的字符串,然后小 A 随机地选择其中一个,小 B 要猜这个字符串。小 B 可以问小 A:字符串中第 pos 个字符是什么?求小 B 期望问几次能唯一确定这个字符串。
  • n50,m20

不妨枚举小 A 选择的字符串为 si

状压 DP。设 f(S) 表示当前已经询问的下标集合为 S 时,期望再问几次可以唯一确定 s。那么答案为 f()

如果 S 已经能够确定这个字符串那么 f(S)=0。否则:

f(S)=vSf(S{v})+1m|S|=1+vSf(S{v})m|S|

复杂度过不去。思考能否省去最开始的枚举。

重新设 f(S) 表示当前状态为 S 时,每个 si 期望的次数之和。也即,此时的 f(S) 是上面每个字符串的 f(S) 之和。

类似的有转移:

f(S)=g(S)+vSf(S{v})m|S|

其中 g(S) 有多少个字符串不能通过 S 唯一确定。

考虑 g(S) 的求解。我们可以枚举两个字符串 si,sj。对于某个 k 而言,如果 sik=sjk 那么就不能通过一次询问唯一确定 sisj。令 A 为所有这样的 ksik=sjk)组成的集合。那么每个 A 的子集都不能唯一确定 sisj。高维前缀和秒了。

Code
#include <bits/stdc++.h>

typedef long long ll;
#define int ll

const int N = 51, M = 22;

int n, m;
int a[N][M];
double f[1ll << M], res;
ll g[1ll << M];

signed main() {
	std::ios::sync_with_stdio(0);
	std::cin.tie(0), std::cout.tie(0);
	std::cin >> n;
	
	if (n == 1) {
		std::cout << 0 << '\n';
		return 0;
	}
	
	for (int i = 1; i <= n; ++ i ) {
		std::string s;
		std::cin >> s;
		m = s.size();
		for (int j = 0; j < m; ++ j )
			a[i][j] = s[j] >= 'a' ? s[j] - 'a' : s[j] - 'A' + 26;
	}
	
	g[0] = (1ll << n) - 1;
	for (int i = 1; i < n; ++ i )
		for (int j = i + 1; j <= n; ++ j ) {
			int S = 0;
			for (int k = 0; k < m; ++ k )
				if (a[i][k] == a[j][k]) S |= 1 << k;
			g[S] |= (1ll << i - 1) | (1ll << j - 1);
		}
	
	for (int i = 0; i < m; ++ i )
		for (int j = (1 << m) - 1; ~j; -- j )
			if (!(j >> i & 1)) g[j] |= g[j | (1ll << i)];
	
	for (int i = (1 << m) - 1; ~i; -- i ) {
		if (!g[i]) continue;
		for (int j = 0; j < m; ++ j )
			if (!(i >> j & 1)) f[i] += f[i | (1ll << j)];
		f[i] /= (m - __builtin_popcountll(i));
		f[i] += __builtin_popcountll(g[i]);
	}
	
	std::cout << std::fixed << std::setprecision(10) << f[0] / n << '\n';
	
	return 0;
}

(16) CF797F Mice and Holes

  • n 个老鼠,m 个洞,告诉你他们的一维坐标和 m 个洞的容量限制,问最小总距离。
  • n,m5×103,坐标在 ±109 内。

不难发现每个洞内的老鼠在坐标上是连续的。因此我们将老鼠和洞按坐标排序,并将老鼠分成 m 段,每段老鼠对应一个洞。

设计 DP。令 g(l,r,x) 表示 [l,r] 老鼠到第 x 个洞的距离和。显然 g(l,r,x)=g(1,r,x)g(1,l1,x)。令 f(i,j) 表示前 i 个洞,前 j 个老鼠的答案。那么转移枚举这个洞的老鼠数量:

f(i,j)=mink=0ci{f(i1,jk)+g(jk+1,j,i)}=mink=max(0,jci)j{f(i1,k)+g(k+1,j,i)}=mink=max(0,jci)j{f(i1,k)+g(1,j,i)g(1,k,i)}=g(1,j,i)+mink=max(0,jci)j{f(i1,k)g(1,k,i)}

单调队列维护即可。

Code
#include <bits/stdc++.h>

typedef long long ll;

constexpr int N = 5010;

int n, m;
int x[N];

struct Mice {
	int p, c;
	bool operator <(const Mice& h) const {
		return p < h.p;
	}
}y[N];

ll sum[N];
ll f[2][N];		// 前 i 个洞,前 j 只老鼠,最小距离和
ll g[N];	// 前 i 只老鼠,到第 j 个洞的距离和

int q[N], hh, tt = -1;

signed main() {
	std::cin >> n >> m;
	
	for (int i = 1; i <= n; ++ i ) std::cin >> x[i];
	for (int i = 1; i <= m; ++ i ) std::cin >> y[i].p >> y[i].c;
	
	std::sort(x + 1, x + n + 1);
	std::sort(y + 1, y + m + 1);
	
	for (int i = 1; i <= m; ++ i ) sum[i] = sum[i - 1] + y[i].c;
	
	memset(f, 0x3f, sizeof f);
	for (int i = 0; i <= m; ++ i ) f[i & 1][0] = 0;
	
	auto calc = [&](int i, int j) -> ll {
		return f[i - 1 & 1][j] - g[j];
	};
	
	for (int i = 1; i <= m; hh = 0, tt = -1, ++ i ) {
		for (int j = 1; j <= n; ++ j ) {
			g[j] = g[j - 1] + abs(x[j] - y[i].p);
		}
		
		for (int j = 0; j <= n; ++ j ) {
			if (hh <= tt && j - y[i].c > q[hh]) ++ hh;
			while (hh <= tt && calc(i, q[tt]) >= calc(i, j)) -- tt;
			q[ ++ tt] = j;
			
			if (j <= sum[i]) {
				f[i & 1][j] = std::min(f[i - 1 & 1][j], g[j] + calc(i, q[hh]));
			}
		}
	}
	
	if (f[m & 1][n] > 1e12) f[m & 1][n] = -1;
	std::cout << f[m & 1][n] << '\n';
	
	return 0;
}

(17) P1545 [USACO04DEC] Dividing the Path G

  • 给定一个长为偶数 l 的线段。要求用若干两两不交的,长度在 [2a,2b] 之间的偶数长度线段来覆盖整条线段。给定 n 个区间 [si,ei]每个区间必须只被一个线段覆盖。求最少需要的线段数量。
  • l106a,b,n103

如果一个区间 [si,ei] 只被一个线段覆盖,等价于不存在两条线段的交点在 [si+1,ei1] 内。又因为线段两两不交,所以等价于不存在线段的端点在 [si+1,ei1] 内。

考虑剩余的允许放线段端点的位置 i。设 f(i) 表示 i=l 时原问题的答案。显然转移:

f(i)=minj=ab{f(i2j)+1}=1+minj=max(0,i2b)i2af(j)

单调队列/线段树维护即可。

Code
#include <bits/stdc++.h>

constexpr int N = 1000010;

int n, l, a, b;

struct Seg {
	int l, r;
}t[N];

int dp[N], sum[N];

struct Tree {
	int l, r, ls, rs, v;
}tr[N << 2];

void pushup(int u) {
	tr[u].v = std::min(tr[tr[u].ls].v, tr[tr[u].rs].v);
}

int idx;

int build(int l, int r) {
	int u = ++ idx;
	tr[u].l = l, tr[u].r = r;
	if (l != r) {
		int mid = l + r >> 1;
		tr[u].ls = build(l, mid), tr[u].rs = build(mid + 1, r);
	} else tr[u].v = 1e9;
	pushup(u);
	return u;
}

int query(int u, int l, int r) {
	if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;
	int mid = tr[u].l + tr[u].r >> 1, res = 1e9;
	if (l <= mid) res = query(tr[u].ls, l, r);
	if (r > mid) res = std::min(res, query(tr[u].rs, l, r));
	return res;
}

void modify(int u, int x, int d) {
	if (tr[u].l == tr[u].r) tr[u].v = d;
	else {
		int mid = tr[u].l + tr[u].r >> 1;
		if (x <= mid) modify(tr[u].ls, x, d);
		else modify(tr[u].rs, x, d);
		pushup(u);
	}
}

int main() {
	std::cin >> n >> l >> a >> b;
	
	for (int i = 1; i <= n; ++ i ) {
		std::cin >> t[i].l >> t[i].r;
		++ sum[t[i].l + 1], -- sum[t[i].r];
	}
	
	for (int i = 1; i <= l; ++ i ) sum[i] += sum[i - 1];
	
	build(1, l + 1);
	memset(dp, 0x3f, sizeof dp);
	dp[0] = 0;
	modify(1, 1, 0);
	for (int i = 2; i <= l; i += 2 )
		if (!sum[i]) {
			int L = std::max(0, i - 2 * b), R = i - 2 * a;
			for (int j = L; j <= R; ++ j ) dp[i] = std::min(dp[i], dp[j] + 1);
			modify(1, i + 1, dp[i]);
		}
	
	if (dp[l] >= 1e9) dp[l] = -1;
	std::cout << dp[l];
	
	return 0;
}

(18) CF900D Unusual Sequences

  • 给定 x,y。求有多少个序列的 gcdx,和为 y。取模 109+7
  • x,y109

设答案为 h(x,y),设 f(x) 表示和为 x 的元素两两互质的序列个数。

显然答案 h(x,y)=f(yx)。若 xy 则无解。

考虑:

  • g(x) 表示和为 x 的序列个数,即全集。显然插板法 g(x)=2x1
  • h(y,x) 表示和为 ygcdx 的序列个数。显然 h(y,x)={f(xy)yx0yx

那么转移为:

f(x)=g(x)yh(y,x)=2x1yxf(xy)

Code
#include <bits/stdc++.h>

typedef long long ll;

constexpr int P = 1e9 + 7;

int x, y;
std::map<int, int> f;

int fpm(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = (ll)res * a % P;
		b >>= 1, a = (ll)a * a % P;
	}
	return res;
}

int dp(int x) {
	if (f.count(x)) return f[x];
	if (x == 1) return f[x] = 1;
	int res = fpm(2, x - 1);
	for (int i = 1; i <= x / i; ++ i )
		if (x % i == 0) {
			res = (res - dp(i) + P) % P;
			if (i != x / i && x / i != x) res = (res - dp(x / i) + P) % P;
		}
	return f[x] = res;
}

int main() {
	std::cin >> x >> y;
	std::cout << (y % x == 0 ? dp(y / x) : 0) << '\n';
	return 0;
}

(19) CF79D Password

  • 你有 n 个灯泡,一开始都未点亮。

    同时你有 l 个长度,分别为 a1al

    每次你可以选择一段连续的子序列,且长度为某个 ai,并将这些灯泡的明灭状态取反。

    求最少的操作次数,使得最后有且仅有 k 个位置是亮的,这些位置已经给定,为 x1xk

  • n104k10l100

n 个灯泡的开关状态为 b1bn。若 bi=1 表示第 i 盏灯开启,bi=0 表示第 i 盏灯关闭。

第一个观察是,我们从 b1=b2==bn=0 变化成所有 bxi=1 的局面,等价于从所有 bxi=1 变化成 b1=b2==bn=0 的局面。

所以最开始我们让所有 bxi1。现在的问题是:

给定 a,b。每次可以选择一个 b 的区间 [x,x+ai1] 取反。求将 b 全部变为 0 的最少操作数。

区间反转用差分维护。令 b 的差分数组为 c1cn+1,即 ci=bixorbi1。那么将区间 [l,l+ak1] 取反等价于将 cl,cl+ak 取反。

显然当 c1=c2==cn+1=0 时,我们的任务就完成了。现在的问题是:

给定 a,c。每次可以选择一个 c 的区间 [x,x+ai],并将 cx,cx+ai 取反。求将 c 全部变为 0 的最少操作数。

显然我们只需要考虑那些为 1 的位置,即 {ici=0}。因为操作 {ici=1} 显然是不优的。

若令 g(x,y) 表示将 x,y 同时反转的最小的所需次数。

考虑状压 DP。令 S 表示当前 c 中仍为 1 的下标集合,设 f(S) 表示在状态 S 的情况下,将 c 全部变为 0 的最少操作次数。

转移显然:

f(S)=minu,vS{f(S/u/v)+g(u,v)}

答案为 f({xcx=1})

考虑 g(x,y) 的求解。举个例子,如果我们可以同时将 cx,cx+a 取反,也可以同时将 cx+a,cx+a+b 取反,那么我们就可以通过两次操作,同时将 cx,cx+a+b 取反。

具体的,考虑建图。对于一条边 uv 表示可以通过一次操作将 u,v 同时取反,那么这张图上 xy 的最短路即 g(x,y)

Code
#include <bits/stdc++.h>

constexpr int N = 10009, K = 22, L = 209;

int n, k, l, x[K], a[L];
bool b[N], c[N];
int mp[L][L];
int Id[N], Di[N], cnt;
int f[1 << K];

struct Gragh {
	int h[N], e[N * L], ne[N * L], idx = 1;
	
	void add(int a, int b) {
		e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
		e[idx] = a, ne[idx] = h[b], h[b] = idx ++ ;
	}
	
	int dis[N];
	bool st[N];
	
	void bfs(int s) {
		std::queue<int> q;
		q.push(s);
		
		memset(dis, 0x3f, sizeof dis);
		memset(st, 0, sizeof st);
		
		dis[s] = 0;
		st[s] = true;
		
		while (q.size()) {
			int u = q.front();
			q.pop();
			
			for (int i = h[u]; i; i = ne[i]) {
				int v = e[i];
				if (!st[v]) {
					st[v] = true;
					dis[v] = dis[u] + 1;
					q.push(v);
				}
			}
		}
		
		for (int i = 1; i <= n + 1; ++ i )
			if (c[i]) mp[Di[s]][Di[i]] = dis[i];
	}
}G;

int dp(int S) {
	if (!S) return 0;
	if (f[S]) return f[S];
	
	int &res = f[S];
	res = 1e9;
	
	for (int i = 0; i < cnt; ++ i )
		if (S >> i & 1)
			for (int j = 0; j < cnt; ++ j )
				if (S >> j & 1)
					res = std::min(res, dp(S ^ (1 << i) ^ (1 << j)) + mp[i][j]);
	
	return res;
}

int main() {
	std::cin >> n >> k >> l;
	
	for (int i = 1; i <= k; ++ i ) {
		std::cin >> x[i];
		b[x[i]] = true;
	}
	
	for (int i = 1; i <= n + 1; ++ i ) {
		c[i] = b[i] ^ b[i - 1];
	}
	
	for (int i = 1; i <= l; ++ i ) {
		std::cin >> a[i];
	}
	
	for (int i = 1; i <= n + 1; ++ i )
		if (c[i]) Id[cnt ++ ] = i, Di[i] = cnt - 1;
	
	for (int i = 1; i <= n + 1; ++ i )
		for (int j = 1; j <= l; ++ j )
			if (i + a[j] <= n + 1) G.add(i, i + a[j]);
	
	for (int i = 1; i <= n + 1; ++ i )
		if (c[i]) G.bfs(i);

	std::cout << (dp((1 << cnt) - 1) == 1e9 ? -1 : f[(1 << cnt) - 1]) << '\n';
	
	return 0;
}
posted @   2huk  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示