动态规划做题笔记

\(\color{#3498D8}(1)\) P2606 [ZJOI2010] 排列计数

  • 求有多少 \(1 \sim n\) 的排列 \(p\) 满足 \(\forall i \in[2, n], p_i > p_{\lfloor i/2 \rfloor}\),对 \(m\) 取模。

  • \(n \le 10^6\)\(m \le 10^9\)\(m\) 是一个质数。

观察发现 \(p_i > p_{\lfloor i/2 \rfloor}\) 这个条件与小根堆的性质类似。问题就转化成了:

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

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

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

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

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

而两棵子树内部的答案分别为 \(f_l, f_r\),那么转移式即 \(f_u = f_l \cdot f_r \cdot \dfrac{(s_l + s_r)!}{s_l! \cdot s_r!}\)。叶节点的 dp 值为 \(1\)

最后输出 \(f_1\) 即可,表示整棵树的答案。

$\color{blue}\text{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);
	}
}

\(\color{#52A41A}(2)\) P3146 [USACO16OPEN] 248 G

  • 给定一个序列,每次可以将两个相邻且相同的数合并成一个数,合并结果为原数加一。求最后能得到的最大数字。
  • \(n \le 248\)\(1 \le a_i \le 40\)

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

由于 \(k\) 一定是由两个 \(k - 1\) 合并而来的,所以转移为 \(f_{l, r, k} = \operatorname{or}_{p=l}^{r-1} \{f_{l, p, k - 1} \operatorname{and} f_{p + 1, r, k - 1}\}\)

这样是可以通过的。

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

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

$\color{blue}\text{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;
}

\(\color{#3498D8}(3)\) P3147 [USACO16OPEN] 262144 P

  • 题意同上。
  • \(n \le 262144\)\(1 \le a_i \le 40\)

仍然是区间 DP。但是显然状态不能设成 \(f_{l, r}\) 这样 \(\Theta(n^2)\) 的。

同时仍然可以发现,对于两个有着相同左端点和不同右端点的区间 \([l, r], [l, r']\),那么一定有 \(f_{l, r} \ne f_{l, r'}\)

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

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

$\color{blue}\text{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;
}

\(\color{#3498D8}(4)\) P2051 [AHOI2009] 中国象棋

  • 求在 \(n \times m\) 的棋盘上棋子,且不存在某一行或某一列有大于两个棋子的方案数。
  • \(n, m \le 100\)

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

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

为了方便可以写成刷表。

$\color{blue}\text{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;
}

\(\color{#3498D8}(5)\) P4805 [CCC2016] 合并饭团

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

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

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

  • \(n \le 400\)

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

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

  • 第一种操作:\(f_{l, r} = \operatorname{or}_{k=l}^{r-1}\{[s(l, k) = s(k + 1, r)] \operatorname{and} f_{l, k} \operatorname{and} f_{k + 1, r}\}\)
  • 第二种操作:\(f_{l, r} = \operatorname{or}_{k = l}^{r - 1} \operatorname{or}_{p=k}^{r - 1} \{[s(l, k) = s(p + 1, r) ]\operatorname{and} f_{l, k} \operatorname{and} f_{k + 1, p} \operatorname{and} f_{p + 1, r}\}\)

直接转移是 \(\Theta (n^4)\) 的,卡常可过。

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

所以 two-pointer 即可。

$\color{blue}\text{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;
}

\(\color{#3498D8}(6)\) P4290 [HAOI2008] 玩具取名

  • 给定一个由字母 \(\texttt{WING}\) 组成的字符串和若干个变化规则,表示可以将相邻两个字母合并成一个字母。求这个字符串可以合并为哪些独个字母。
  • \(n \le 200\)

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

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

$\color{blue}\text{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!");
}

\(\color{#52A41A}(7)\) P4170 [CQOI2007] 涂色

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

设状态 \(f_{l, r}\) 表示将区间 \([l, r]\) 染成目标颜色的最少操作次数。

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

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

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

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

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

$\color{blue}\text{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];
}

\(\color{#BFBFBF}(8)\) LOJ P507 接竹竿

  • \(n\) 张牌排成一排,每张牌有属性 \((c_i, v_i)\)。保证 \(c_i \le k\)

    每次操作选择两张牌 \(l, r\) 满足 \(c_l = c_r\),删除 \(l \sim r\) 中的所有牌,并获得 \(\sum_{i=l}^rv_i\) 的收益。

    求最大的收益。

  • \(n, k \le 10^6\)

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

转移枚举第 \(i\) 张牌是否是在最后一次操作中被删,以及被哪个区间删。即 \(f_{i - 1}\)\(\max_{j=1}^{i - 1}\{f_{j - 1} + \sum_{k=j}^iv_k \mid c_i = c_j\}\) 的较大值。

直接做是 \(n^3\) 的。区间求和那个部分可以前缀和优化,但仍然是 \(n^2\) 的,即 \(\max_{j=1}^{i - 1}\{f_{j - 1} + \sum_{k=1}^iv_k - \sum_{k=1}^{j-1}v_k \mid c_i = c_j\}\)

可以把与 \(j\) 无关的提到外面,即 \(\sum_{k=1}^iv_k + \max_{j=1}^{i - 1}\{f_{j - 1} - \sum_{k=1}^{j-1}v_k \mid c_i = c_j\}\)

然后这个就很好维护了。我们用桶维护每个 \(c_i\) 所对应的最大的 \(f_{i-1} - \sum_{k=1}^{i-1}v_k\),转移可以优化成 \(\Theta(1)\)。总时间复杂度 \(\Theta(n)\)

$\color{blue}\text{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;
}

\(\color{#3498D8}(9)\) P4342 [IOI1998] Polygon

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

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

    求最后元素的最大值。

  • \(n \le 50\)

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

设状态 \(f_{l, r}\) 表示将区间 \(l \sim r\) 内的数字处理后得到的最大数字。转移枚举断点 \(k\),即 \(f_{l, r} = \max_{k=l}^{r-1} \operatorname{opt}(f_{l, k}, f_{k + 1, r})\),其中 \(\operatorname{opt}\) 表示边上的运算符号。

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

复杂度 \(\Theta(n^3)\)

$\color{blue}\text{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 << ' ';
}

\(\color{#52A41A}(10)\) P4933 大师

  • 给定一个长度为 \(n\) 的序列。求有多少个子序列是等差数列。
  • \(v\) 为序列最大值。\(n \le 1000\)\(v \le 20000\)

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

转移枚举倒数第二个元素 \(k\),即 \(f_{i, j} = \sum_{k=1}^{i - 1} \{f_{k, j} + 1\mid a_i - a_k = j\}\)。其中 \(+1\) 的原因是我们可以选择长度为 \(1\) 的子序列。

复杂度是 \(\Theta(n^2v)\) 的。考虑优化。

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

时间复杂度 \(\Theta(nv)\)

$\color{blue}\text{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;
}

\(\color{#52A41A}(11)\) P5662 [CSP-J2019] 纪念品

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

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

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

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

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

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

  • \(T ,N \le 100\)\(M \le 1000\)

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

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

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

\[dp_{i, j, k} = \max(dp_{i, j - 1, k}, dp_{i, j - 1, k + p_{i, j}} + p_{i + 1, j}) \]

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

$\color{blue}\text{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;
	
}

\(\color{#9D3DCF}(12)\) CF1234F Yet Another Substring Reverse

  • 给你一个字符串 \(S\),你可以翻转一次 \(S\) 的任意一个子串。问翻转后 \(S\) 的子串中各个字符都不相同的最长子串长度。
  • \(|S| = n \le 10^6\)\(s_i \in \{\texttt a, \texttt b, \dots, \texttt t\}\),字符集大小 \(V \le 20\)

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

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

\[\max_{f(S) \text{ is true} ,f(S') \text{ is true},S \cap S' = \varnothing}\{|S| + |S'|\} \]

可以用 \(\Theta(Vn)\) 预处理 \(f(S)\),并用 \(\Theta(2^{2n})\) 计算答案。显然不优。

考虑优化:

\[\max_{f(S) \text{ is true}, S' \subseteq \overline{S}, f(S')\text{ is true}}\{|S| + |S'|\} \]

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

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

\[g(S)= \max_{S' \subseteq S, f(S) \text{ is true}} \{|S'|\} \]

那么答案为:

\[\max_{f(S) \text{ is true}}\{|S| + g(\overline{S})\} \]

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

$\color{blue}\text{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;
}

\(\color{#9D3DCF} (13)\) CF1550E Stringforces

  • 给定字符串 \(s\) 和整数 \(k\)\(s\) 由前 \(k\) 小的字母或 \(\texttt?\) 构成。你需要将每个 \(\texttt ?\) 替换成某个前 \(k\) 小的字母。定义其价值为前 \(k\) 个字母中最小的最大连续出现的长度,例如 \(\texttt {aaaabbbbbb}\) 的价值为 \(4\)。求最大价值。
  • \(n\leq 2\times 10^5\)\(k\leq 17\)

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

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

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

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

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

\[g(c, i) = \left\{\begin{matrix} i + x - 1 & \forall j \in [i, i + x - 1] ,s_j \in \{c, \texttt ?\}\\ g(c, i + 1) & \text{otherwise.} \end{matrix}\right. \]

\[f(S\cup \{i\}) = \min_S g(f(S) + 1, i) \]

$\color{blue}\text{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;
}

\(\color{#9D3DCF}(14)\) CF1316E Team Building

  • \(n\) 个人。你需要从中选出 \(p\) 个人作队员,\(k\) 个人作观众。第 \(i\) 个人作观众的价值为 \(a_i\),作第 \(j\) 个队员的价值为 \(s_{i, j}\)。求最大价值和。
  • \(p \le 7\)\(p + k \le n \le 10^5\)

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

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

分类讨论第 \(i\) 个人。

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

\[f(i - 1, S \backslash j)+s_{i, j} \]

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

\[f(i - 1, S) + [i - 1 - |S| < k]\times a_i \]

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

$\color{blue}\text{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;
}

\(\color{#9D3DCF}(15)\) CF482C Game with Strings

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

不妨枚举小 A 选择的字符串为 \(s_i\)

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

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

\[f(S) = \sum_{v \notin S} \dfrac{f(S \cup \{v\}) + 1}{m - |S|} = 1 + \sum_{v \notin S} \dfrac{f(S \cup \{v\})}{m - |S|} \]

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

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

类似的有转移:

\[f(S) = g(S) + \sum_{v \notin S} \dfrac{f(S \cup \{v\})}{m - |S|} \]

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

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

$\color{blue}\text{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;
}

\(\color{#3498D8}(16)\) CF797F Mice and Holes

  • \(n\) 个老鼠,\(m\) 个洞,告诉你他们的一维坐标和 \(m\) 个洞的容量限制,问最小总距离。
  • \(n, m \le 5 \times 10^3\),坐标在 \(\pm 10^9\) 内。

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

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

\[\begin{aligned} f(i, j) &= \min_{k=0}^{c_i} \{f(i - 1, j - k) + g(j - k + 1, j, i) \} \\&= \min_{k=\max(0,j-c_i)}^{j} \{f(i - 1, k) + g(k+1, j, i)\} \\&= \min_{k=\max(0,j-c_i)}^{j} \{f(i - 1, k) + g(1, j, i) - g(1, k, i)\} \\&= g(1, j, i) + \min_{k=\max(0,j-c_i)}^{j} \{f(i - 1, k) - g(1, k, i)\} \end{aligned} \]

单调队列维护即可。

$\color{blue}\text{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;
}

\(\color{#3498D8}(17)\) P1545 [USACO04DEC] Dividing the Path G

  • 给定一个长为偶数 \(l\) 的线段。要求用若干两两不交的,长度在 \([2a,2b]\) 之间的偶数长度线段来覆盖整条线段。给定 \(n\) 个区间 \([s_i,e_i]\)每个区间必须只被一个线段覆盖。求最少需要的线段数量。
  • \(l \le 10^6\)\(a, b, n \le 10^3\)

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

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

\[\begin{aligned} f(i) &= \min_{j=a}^b \{f(i - 2j) + 1\}\\ &= 1 + \min_{j=\max(0, i - 2b)}^{i-2a} f(j) \end{aligned} \]

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

$\color{blue}\text{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;
}

\(\color{#3498D8}(18)\) CF900D Unusual Sequences

  • 给定 \(x, y\)。求有多少个序列的 \(\gcd\)\(x\),和为 \(y\)。取模 \(10^9 + 7\)
  • \(x, y \le 10^9\)

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

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

考虑:

  • \(g(x)\) 表示和为 \(x\) 的序列个数,即全集。显然插板法 \(g(x) = 2^{x-1}\)
  • \(h(y, x)\) 表示和为 \(y\)\(\gcd\)\(x\) 的序列个数。显然 \(h(y, x) = \left\{\begin{matrix} f(\frac xy) & y \mid x\\ 0 & y \nmid x\end{matrix}\right.\)

那么转移为:

\[\begin{aligned}f(x) &= g(x) - \sum_y h(y, x)\\ &= 2^{x-1} - \sum_{y \mid x} f\left(\frac xy\right) \end{aligned} \]

$\color{blue}\text{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;
}

\(\color{#9D3DCF}(19)\) CF79D Password

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

    同时你有 \(l\) 个长度,分别为 \(a_1 \sim a_l\)

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

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

  • \(n \le 10^4\)\(k \le 10\)\(l \le 100\)

\(n\) 个灯泡的开关状态为 \(b_1 \sim b_n\)。若 \(b_i = 1\) 表示第 \(i\) 盏灯开启,\(b_i = 0\) 表示第 \(i\) 盏灯关闭。

第一个观察是,我们从 \(b_1 = b_2 = \dots = b_n = 0\) 变化成所有 \(b_{x_i} = 1\) 的局面,等价于从所有 \(b_{x_i} = 1\) 变化成 \(b_1 = b_2 = \dots = b_n = 0\) 的局面。

所以最开始我们让所有 \(b_{x_i} \gets 1\)。现在的问题是:

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

区间反转用差分维护。令 \(b\) 的差分数组为 \(c_1 \sim c_{n+1}\),即 \(c_i = b_i \operatorname{xor} b_{i-1}\)。那么将区间 \([l, l + a_k - 1]\) 取反等价于将 \(c_l, c_{l+a_k}\) 取反。

显然当 \(c_1 = c_2 = \dots = c_{n+1} = 0\) 时,我们的任务就完成了。现在的问题是:

给定 \(a, c\)。每次可以选择一个 \(c\) 的区间 \([x, x + a_i]\),并将 \(c_x, c_{x+a_i}\) 取反。求将 \(c\) 全部变为 \(0\) 的最少操作数。

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

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

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

转移显然:

\[f(S) = \min_{u, v \in S}\{f(S/u/v) + g(u, v)\} \]

答案为 \(f(\{x \mid c_x = 1\})\)

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

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

$\color{blue}\text{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 @ 2024-05-15 17:01  2huk  阅读(21)  评论(0编辑  收藏  举报