• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
The blog of EzSun
lyy && ?
博客园    首页    新随笔    联系   管理    订阅  订阅

计数DP总结

计数DP一般是依靠状态去表示方案数,因此状态的设计就很重要,需要便于转移。
方程一般是由前一个状态乘上这一步操作的方案数得到这一个状态,需要灵活选择填表或刷表。
Trick:

  1. 钦定
    我们可以在转移时先不考虑这一步操作的贡献,在随后的操作中再一并计算。
    例:CSP-S 2025 T4。

P3244 [HNOI2015] 落忆枫音

记每个节点的入度为\(in_i\)
易得对于一个DAG,其生成树数量为\(\prod_{i=1}^{n} in_i\)
但加了新边之后,这张图可能会存在环,会出现一些方案是不合法的。我们便要容斥一下,把不合法的方案去掉。
那我们就考虑不合法方案数怎么算。
记新加的边为\(s->t\)
那么环的数量就等同于原图中\(t->s\)路径的数量
那么我们再去考虑每一个环
我们可以给环上每个节点钦定一个父亲
那么环上每个节点对于答案的贡献就从\(in_i\)变为了\(1\)
记环上的节点为\(w\)
那么这个环对于不合法方案数的贡献即为\(\frac{\prod_{i=1}^{n} in_i}{\prod in_w}\)
注意到这个东西可以dp一下
设\(f_i\)为从\(i\)到\(s\)的路径中上面这个式子的值。
初始化\(f_s\)为原图中\(\prod_{i=1}^{n} in_i \div in_s\)
转移方程:\(f_u = \frac{\sum_{v}^{} f_v}{in_u}(u->v)\)、
最终的答案即为\(f_t\)
那我们便可以反向建图,在反图中记忆化搜索,在回溯时进行转移。
注意,因为转移方程中有除法,所以要求逆元。

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 7, M = 2e5 + 7, p = 1e9 + 7;
int n, m, from, to;
ll dp[N];
ll ans, du[N], f[N], s;
bool vis[N];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % p;
		x = x * x % p;
		y >>= 1;
	}
	return sum;
}
struct Edge{
	int head[N], tot;
	struct edge{
		int to, pre;
	}e[M];
	void add(int x, int y){
		e[++tot] = {y, head[x]};   
		head[x] = tot;
		du[x]++;
	}
	void dfs(int u){
		if(vis[u] || u == to) return;
		vis[u] = 1;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			dfs(v);
			f[u] = (f[u] + f[v] * fpow(du[u], p - 2) % p) % p;
		}
	}
}E;
int main(){
	scanf("%d%d%d%d", &n, &m, &from, &to);
	for(int i = 1, x, y; i <= m; i++){
		scanf("%d%d", &x, &y);
		E.add(y, x);
	}
	ans = s = du[1] = 1;
	for(int i = 1; i <= n; i++){
		if(i == to) ans = ans * (du[i] + 1) % p;
		else ans = ans * du[i] % p;
		s = s * du[i] % p;
	}
	f[to] = (s * fpow(du[to], p - 2)) % p;
	E.dfs(from);
	ans = (ans - f[from] + p) % p;
	if(to == 1 || from == to) printf("%lld\n", s);
	else printf("%lld\n", ans);
	return 0;
}

P3214 [HNOI2011] 卡农

将题意转化一下:
从集合\(S={1,2,3,\dots}\)中选取\(m\)个子集,要求
(1):不存在空集
(2):不能有重复的子集
(3):在这\(m\)个子集中,\(1\)到\(n\)每个元素的出现次数都必须是偶数
设\(f_i\)为选取\(i\)个子集的方案数
试着用容斥去做
根据第三条性质,可得当前\(i-1\)个集合都选取后,第\(i\)个集合也随之确定,总方案数为\(A_{2^n-1}^{i-1}\)
根据第一条性质,可得当第\(i\)个集合为空集时,前\(i-1\)个集合是合法的,那么不符合第一条性质的不合法方案数即为\(f_{i-1}\)
根据第二条性质,当第\(i\)个集合与第\(j\)个集合重复时,另外的\(i-2\)个集合是合法的,方案数为\(f_{i-2}\),又因为\(j\)有\(i-1\)个取值,子集\(i\)有\(2^n-1-(i-2)\)个取值,那么不符合第二条性质的不合法方案数即为\(f_{i-2} \times (i-1) \times (2^n-i+1)\)
综上,转移方程即为\(f_i=A_{2^n-1}^{i-1}-f_{i-1}-f_{i-2} \times (i-1) \times (2^n-i+1)\)

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 7, mod = 1e8 + 7;
int n, m; 
ll a[N], f[N];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return sum;
}
int main(){
	scanf("%d%d", &n, &m);
	a[0] = f[0] = 1;
	ll t = fpow(2, n) - 1, r = 1;
	for(int i = 1; i <= m; i++){
		a[i] = a[i - 1] * (t - i + 1 + mod) % mod;
		r = r * i % mod;
		//printf("a%d:%lld\n", i, a[i]);
	}
	for(int i = 1; i <= m; i++){
		f[i] = (a[i - 1] - f[i - 1] + mod - f[i - 2] * (i - 1) % mod * (t - i + 2) % mod + mod) % mod;
	}
	printf("%lld\n", f[m] * fpow(r, mod - 2) % mod);
	return 0;
}

P2606 [ZJOI2010] 排列计数

我们可以将题意转换成一个满足小根堆性质的完全二叉树的树的个数
然后就很好处理了
设\(f_i\)为\(i\)个节点的树的完全二叉树的个数
先算出左右子树的节点个数\(l,r\)
则可推出\(f_i=C^{i-1}_{l} \times f_l \times f_r\)
注意:组合数要用lucas定理算

Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6;
#define ll long long
int Task;
int n, p;
int lg2[N + 7];
ll fac[N + 7], inv[N + 7];
ll f[N + 7];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % p;
		x = x * x % p;
		y >>= 1;
	}
	return sum;
}
void init(){
	f[1] = fac[0] = fac[1] = 1;
	for(int i = 2; i <= N; i++){
		lg2[i] = lg2[i >> 1] + 1;
		fac[i] = fac[i - 1] * i % p;
	}
	int m = min(p - 1, n);
	inv[m] = fpow(fac[m], p - 2);
	for(int i = m - 1; i >= 0; i--){
		inv[i] = inv[i + 1] * (i + 1) % p;
	}
}
ll C(ll x, ll k){
	return fac[x] * inv[k] % p * inv[x - k] % p;
}
ll lucas(ll x, ll k){
	return k == 0 ? 1 : (C(x % p, k % p) * lucas(x / p, k / p) % p);
}
int main(){
	scanf("%d%d", &n, &p);
	init();
	f[1] = f[2] = 1;
	f[3] = 2;
	int l = 1, r = 1;
	for(int i = 4; i <= n; i++){
		if(i - (1 << lg2[i]) + 1 <= (1 << (lg2[i] - 1))) l++;
		else r++;
		f[i] = lucas(i - 1, l) * f[l] % p * f[r] % p;
	}
	printf("%lld\n", f[n]);
	return 0;
}

P6846 [CEOI 2019] Amusement Park

可以发现对于每一种合法的方案
这个方案的反图也必定合法
且这两个方案对于答案的贡献之和为\(m\)
那么就可以将问题转化为求合法方案数再乘上\(\frac{m}{2}\)
随后开始想怎么求合法方案数
可以想到对于一个\(DAG\),可以将其分解为一个入度为0的点集和一个更小的\(DAG\)
再结合这道题\(n\leq18\)的数据范围
就可以以这个点集为依据去\(dp\)
这也是DAG上DP的一个常用套路:钦定零度点集,简单容斥去重
令\(T\)为\(S\)的一个子集
若\(T\)为独立集
则\(f_S=\sum f_{S-T}\)
但这样算会算重
举一个很简单的例子
若原图为{\({3 \leftarrow 1, 3 \leftarrow 2}\)}
那么在枚举\(1\)和\(2\)和\(1,2\)这三个点集时
{\({3 \rightarrow 1, 3 \rightarrow 2}\)}这个方案会被重复统计
毕竟这个方案对于三个集合都满足集合内点的入度为0
那么我们便要容斥一下
令\(T\)为\(S\)中最大的零度点集
因为\(T\)合法
那么\(T\)的所有子集也必定合法
这便是一个老生常谈的容斥:当\(|T|\)为奇数时,令其贡献为正,否则令其贡献为负
那么就可以AC这道题了

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 18, M = 153, mod = 998244353, inv = (mod + 1) >> 1;
int n, m;
int num[(1 << N) + 7];
ll f[(1 << N) + 7];
bool is[(1 << N) + 7];
struct Edge{
	int x, y;
}e[M];
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i++){
		scanf("%d%d", &e[i].x, &e[i].y);
		e[i].x--;
		e[i].y--;
	}
	for(int S = 0; S < (1 << n); S++){
		is[S] = 1;
		for(int i = 0; i < n; i++){
			num[S] += ((S >> i) & 1);
		}
		for(int i = 1; i <= m; i++){
			if((S & (1 << e[i].x)) && (S & (1 << e[i].y))){
				is[S] = 0;
				break;
			}
		}
	}
	f[0] = 1;
	for(int S = 1; S < (1 << n); S++){
		for(int T = S; T; T = (T - 1) & S){
			if(!is[T]) continue;
			if(num[T] & 1){
				f[S] = (f[S] + f[S ^ T]) % mod;
			}
			else{
				f[S] = (f[S] - f[S ^ T] + mod) % mod;
			}
		}
	}
	printf("%lld\n", f[(1 << n) - 1] * m % mod * inv % mod);
	return 0;
}

P14364 [CSP-S 2025] 员工招聘

题解

我们设 \(cnt_i\) 为耐心程度 \(=i\) 的人数,\(q_i\) 为耐心程度 \(\geq i\) 的人数,\(pre_i\) 为前 \(i\) 天 \(s_i=0\) 的天数。
显然,如果第 \(i\) 个人在第 \(j\) 天去面试成功了,必然满足 \(s_j=1\) 且 \(c_i>pre_j\)。
我们可以先确定状态为 \(f_{i,j}\) 为前 \(i\) 天录取了 \(j\) 个人。
但这样是不可做的,因为你每一次选择的人会对后续的操作产生影响,存在后效性。
这时计数题的一个经典 \(trick\) 就登场了:贡献延后计算。
具体地,我们可以在每次选人时时先不考虑这个人所带来的贡献,在后续的操作中一并计算贡献。
由于每个人是否能被录用基本仅与 \(c_i>pre_j\) 有关,因此我们可以先钦定所有 \(c_i>pre_j\) 的人,在 \(j\) 增加后再一并计算。
我们改状态为 \(f_{i,j,k}\) 代表前 \(i\) 个人中没录用 \(j\) 个,\(i\) 个人中存在 \(k\) 个人 \(c_i>pre_j\)
被选中的人肯定只有三种情况:过面试,没过面试和没参加面试,我们分类讨论一下。
下文的转移将使用刷表法。

  1. 过面试
    显然满足 \(s_j=1\) 且 \(c_i>pre_j\)。
    所以钦定的人数需要增加,但我们不知道钦定的是谁,所以先不考虑贡献。

\[f_{i,j,k} \rightarrow f_{i+1,j,k+1} \]

  1. 没过面试
    显然满足 \(s_j=0\) 且 \(c_i>pre_j\).
    没录用的人数和钦定的人数自然是要增加的。
    但由于 \(j\) 的增加,我们需要计算此前欠下的贡献。
    不难发现,产生贡献的人都是 \(c_i=j+1\) 的。
    我们可以枚举此前使用的 \(c_i=j+1\) 的人数 \(l\)。
    从总共钦定的 \(k+1\) 个人中选 \(l\) 个,贡献为 \(C_{k+1}^{l}\)
    从 \(c_i=j+1\) 的人数中选 \(l\) 个,贡献为 \(C_{cnt_{j+1}}^{l}\)。
    又因为日期和人都是区分顺序的,所以还要乘上 \(l!\)。

\[f_{i,j,k} \times \binom{k+1}{l} \times \binom{cnt_{j+1}}{l} \times l! \rightarrow f_{i+1,j+1,k+1-l} \]

  1. 没进面试
    显然满足 \(c_i \leq pre_j\)。
    没录用的人数自然要增加。
    如上,我们要计算先前欠下的贡献。
    结算的贡献与第2种情况相同。
    但与之前不同的是,我们还要找一个 \(c_i \leq pre_j\) 的人。
    因为我们钦定了 \(k\) 个 \(c_i>pre_j\) 的人
    不难发现选的 \(i\) 个人中有 \(i-k\) 个 \(c_i \leq pre_j\) 的。
    所以还剩 \(q_j-(i-k)\) 个人可以占这个位置。

\[f_{i,j,k} \times \binom{c_{j+1}}{l} \times \binom{k}{l} \times l! \times (q_j-i+k) \]

最后答案只要枚举\(j\)即可,记得要结算先前欠下的贡献。

\[\sum_{j=0}^{n-m} f_{n,j,n-q_j} \times (n-q_j)! \]

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 520, mod = 998244353;
int n, m;
int s[N], c[N], q[N];
ll C[N][N], fac[N], f[2][N][N];
void init(){
	C[0][0] = 1;
	for(int i = 1; i <= N - 7; i++){
		C[i][0] = 1;
		for(int j = 1; j <= i; j++){
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
		}
	}
	fac[0] = 1;
	for(int i = 1; i <= N - 7; i++) fac[i] = fac[i - 1] * i % mod;
	for(int i = 0; i <= N - 7; i++){
		for(int j = 0; j <= i; j++){
			C[i][j] * fac[j] % mod;
		}
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	init();
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		char cc;
		cin >> cc;
		s[i] = (cc == '1');
	}
	for(int i = 1; i <= n; i++){
		int x;
		cin >> x;
		c[x]++;
	}
	q[0] = c[0];
	for(int i = 1; i <= n; i++){
		q[i] = q[i - 1] + c[i];
	}
	f[0][0][0] = 1;
	for(int i = 1; i <= n; i++){
		memset(f[i & 1], 0, sizeof(f[i & 1]));
		for(int j = 0; j < i && j + m <= n; j++){
			for(int k = 0; k < i; k++){
				ll x = q[j] - (i - 1 - k), t = f[i & 1 ^ 1][j][k];
				if(!t) continue;
				if(s[i]){
					f[i & 1][j][k + 1] = (f[i & 1][j][k + 1] + t) % mod;
				}
				else{
					for(int l = 0; l <= c[j + 1] && l <= k + 1; l++){
						f[i & 1][j + 1][k - l + 1] = (f[i & 1][j + 1][k - l + 1] + t * C[k + 1][l] % mod * C[c[j + 1]][l] % mod * fac[l] % mod) % mod;
					}
				}
				if(x > 0){
					for(int l = 0; l <= c[j + 1] && l <= k; l++){
						f[i & 1][j + 1][k - l] = (f[i & 1][j + 1][k - l] + t * C[k][l] % mod * C[c[j + 1]][l] % mod * fac[l] % mod * x % mod) % mod;
					}
				}
			}
		}
	}
	ll ans = 0;
	for(int i = 0; i + m <= n; i++){
		ans = (ans + f[n & 1][i][n - q[i]] * fac[n - q[i]] % mod) % mod;
	}
	printf("%lld\n", ans);
	return 0; 
}

P8502 「CGOI-2」No cost too great

题解:

这一题展示了状态设计的重要性
考虑正难则反,将不去 \(c\) 点的方案数转化为总方案数-去 \(c\) 点的方案数。
设 \(f_{i,j,k}\) 为从 \(i\) 出发走 \(k\) 步到达 \(j\) 的方案数。
很容易可以推出转移方程

\[f_{i,j,k}=\sum_{j \in [l_t,r_t]} f_{i,t,k-1} \]

但这样转移是 \(O(n^3m)\) 的,考虑优化,
发现每次都会由前一个状态转移到一个一段连续の区间,
便可以改用刷表法并差分优化一下。
那么答案似乎就可以表示为

\[f_{a,b,m}-\sum_{k=0}^{k \leq m} f_{a,c,k} f_{c,b,m-k} \]

吗?
很显然会算重,我们还需要再另设一个状态去表示。
会算重的原因是因为在路线中 \(c\) 可能会经过多次,那我们可以考虑去枚举其最后一次经过 \(c\) 点的时间。
我们可以将路线拆分为 \(a \rightarrow c\) 到 \(c \rightarrow b\) 两段。
我们钦定第二段只经过一次 \(c\)。
令 \(g_{i,j,k}\) 为从 \(i\) 走 \(k\) 步到达 \(j\) 且不重复经过 \(i\) 的方案数。
不难发现转移方式和第一个状态是一样的,只是要令 \(g_{i,i,m}=0\)。
那么最终的答案即为

\[f_{a,b,m}-\sum_{k=0}^{k \leq m} f_{a,c,k} g_{c,b,m-k} \]

但空间限制不足以支持这个做法。
将 \(g\) 的第三维滚动一下再离线做即可。

Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7, mod = 998244353;
int n, q;
int l[N], r[N];
int f[107][N][N], g[2][N][N];
int ans[100007];
struct Query{
	int a, b, c, m;
}Q[100007];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> q;
	for(int i = 1; i <= n; i++){
		cin >> l[i] >> r[i];
	}
	for(int i = 1; i <= n; i++) f[0][i][i] = g[0][i][i] = 1;
	for(int k = 0; k < 100; k++){
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				if(!l[j]) continue;
				f[k + 1][i][l[j]] = 1ll * (f[k + 1][i][l[j]] + f[k][i][j]) % mod;
				f[k + 1][i][r[j] + 1] = 1ll * (f[k + 1][i][r[j] + 1] - f[k][i][j] + mod) % mod;
			}
			for(int j = 1; j <= n; j++){
				f[k + 1][i][j] = 1ll * (f[k + 1][i][j - 1] + f[k + 1][i][j]) % mod;
			}
		}
	}
	for(int i = 1; i <= q; i++){
		cin >> Q[i].a >> Q[i].b >> Q[i].c >> Q[i].m;
		ans[i] = f[Q[i].m][Q[i].a][Q[i].b];
	}
	for(int k = 0; k < 100; k++){
		memset(g[k & 1 ^ 1], 0, sizeof(g[k & 1 ^ 1]));
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				if(!l[j]) continue;
				g[k & 1 ^ 1][i][l[j]] = 1ll * (g[k & 1 ^ 1][i][l[j]] + g[k & 1][i][j]) % mod;
				g[k & 1 ^ 1][i][r[j] + 1] = 1ll * (g[k & 1 ^ 1][i][r[j] + 1] - g[k & 1][i][j] + mod) % mod;
			}
			for(int j = 1; j <= n; j++){
				g[k & 1 ^ 1][i][j] = 1ll * (g[k & 1 ^ 1][i][j - 1] + g[k & 1 ^ 1][i][j]) % mod;
			}
			g[k & 1 ^ 1][i][i] = 0;
		}
		for(int i = 1; i <= q; i++){
			int a = Q[i].a, b = Q[i].b, c = Q[i].c, m = Q[i].m;
			if(k + 1 <= m){
				ans[i] = (ans[i] - 1ll * f[m - (k + 1)][a][c] * g[k & 1 ^ 1][c][b] % mod + mod) % mod;
			}
			if(!k){
				ans[i] = (ans[i] - 1ll * f[m][a][c] * g[0][c][b] % mod + mod) % mod;
			}
		}
	}
	for(int i = 1; i <= q; i++){
		printf("%d\n", ans[i]);
	}
	return 0; 
}

AT_arc132_c [ARC132C] Almost Sorted

题解:

这一题其实并不是很难,但实现时把我难受得很。
不难发现,对于每个 \(i\),可能的取值最多只有 \(11\) 种,因此考虑状压 \(dp\),设 \(f_{i,S}\) 表示 \([i-d,i+d]\) 这个区间内的数被选择的情况。
如果 \(a_i=-1\),那么枚举区间内未被选择的数并转移即可,
否则。只能由 \(a_i\) 转移到下一个状态。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e2 + 7, mod = 998244353;
int n, d;
int a[N];
ll f[N][1 << 11];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> d;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
	}
	f[0][0] = 1;
	int U = 1 << (2 * d + 1);
	for(int i = 1; i <= n; i++){
		for(int S = 0; S < U; S++){
			//printf("%d %d:%lld\n", i, S, f[i][S]);
			for(int j = -d; j <= d; j++){
				if((((S >> (j + d)) & 1) == 0) && (i + j >= 1 && i + j <= n) && (a[i] == -1 || a[i] == i + j)){
					f[i][(S | (1 << j + d))  >> 1] = (f[i][(S | (1 << j + d)) >> 1] + f[i - 1][S]) % mod;
				}
			}
		}
	}
	printf("%lld\n", f[n][(1 << d) - 1]);
	return 0; 
}

CF1327F AND Segments

题解:

不难发现每一位的约束都是独立的,我们便可以对每一个二进制位单独做一次dp。
对于每一条限制,若 \(x_i\) 的当前这一个二进制位为 \(1\),则当前这个区间的这一位肯定要全部为 \(1\),否则至少要有一个 \(0\)。
考虑dp。
设 \(f_{i,j}\) 为前 \(i\) 个数且最后一个放 \(0\) 的位置是 \(j\)。
我们首先要预处理一下 \(pre_i\) 和 \(a_i\),代表 前 \(i\) 个数最后一个 \(0\) 最早可以放在哪里和当前这一个数是否必须要放 \(1\)。
考虑转移方程。

  1. \(j<pos_i\)
    显然,这样是不存在方案的。

\[f_{i,j}=0 \]

  1. \(pos_i \leq j < i\)
    在这种情况下,\(i\) 显然必须填 \(1\),因此直接从上一个状态转移过来。

\[f_{i,j}=f{i-1,j} \]

  1. \(j=i\)
    若是 \(a_i=1\), 那么显然无解。
    否则可以枚举上一个 \(0\) 填的位置。

\[f_{i,j}= \sum_{x \in [pos_i,i)} f_{i-1,x} \]

但是强行转移的时空复杂度都不是很够用,考虑优化。
不难发现 \(f_i\) 只会从 \(f_{i-1}\) 转移过来,因此可以使用滚动数组优化。
并且滚动数组还同时帮我们省掉了第二种情况的转移。
对于第一种情况,
不难发现 \(pre_i\) 是单调不降的,
所以只需维护一个指针,每次将指针右移并清零即可。
对于第三种情况,
我们可以配合指针一起处理前面一段区间的和。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 7, mod = 998244353;
int n, k, m;
int l[N], r[N], x[N];
int pos[N], a[N];
ll f[N];
void init(){
	memset(pos, 0, sizeof(pos));
	memset(a, 0, sizeof(a));
	memset(f, 0, sizeof(f));
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> k >> m;
	for(int i = 1; i <= m; i++){
		cin >> l[i] >> r[i] >> x[i];
	}
	ll ans = 1;
	for(int p = 0; p < k; p++){
		init();
		for(int i = 1; i <= m; i++){
			if(x[i] & (1 << p)) a[l[i]]++, a[r[i] + 1]--;
			else pos[r[i] + 1] = max(pos[r[i] + 1], l[i]);
		}
		for(int i = 1; i <= n + 1; i++) pos[i] = max(pos[i - 1], pos[i]), a[i] += a[i - 1];
		f[0] = 1;
		ll s = 1;
		for(int i = 1, j = 0; i <= n + 1; i++){
			while(j < pos[i]) s = (s - f[j] + mod) % mod, f[j] = 0, j++;
			if(a[i]) f[i] = 0;
			else f[i] = s, s = (s << 1) % mod;
		}
		ans = ans * f[n + 1] % mod;
	}
	printf("%lld\n", ans);
	return 0; 
}

P14636 [NOIP2025] 清仓甩卖

题解:

分析一下题目,可以得到若这个策略无法得到最优,则一定存在一个 \(x\) 将本来更加优秀的 \(y\) 卡掉。
不难发现, \(x\) 和 \(y\) 满足 \(\frac{a_y}{2}<a_x<a_y\) 且 \(x\) 为最后一个选中的物品。
这也很容易能够证明。
因此 \(x\) 的定价必须为1,\(y\) 的定价必须为2,且在买 \(x\) 之前要剩余 \(2\) 元,考虑 \(y\) 之前要剩余 \(1\) 元。
注意到题目的数据范围,我们可以 \(O(n^2)\) 枚举 \(x\) 和 \(y\)。
在此之前,我们可以先对 \(a\) 数组升序排序。
对于其他的数,我们可以将其分成几类。

  1. \(a_i \geq a_y\)
    不难发现,这种无论怎样都必定会被取到,花费 \(1\) 或 \(2\) 元,存在 \(n-y\) 个这样的数。
  2. \(a_y>a_i \geq a_x\)
    这种如果定价1元,就会被选到,花费 \(0\) 或 \(1\) 元,存在 \(y-x-1\) 这样的数。
  3. \(a_x>a_i \geq \frac{a_y}{2}\)
    这种是一定不能选的,因为会占用 \(1\) 元,钦定定价为 \(2\)。
  4. \(\frac{a_y}{2} \geq a_i>a_y-a_x\)
    这种也不可以选,因为如果被选,这个策略就会变得优秀,钦定定价为 \(2\)。
  5. \(a_i<a_y-a_x\)
    这种的定价对策略的优劣明显没有影响,因此可以随便定。
    由于在买 \(x\) 之前要花 \(m-2\) 元,将第一种情况的 \(1\) 元从 \(m-2\) 中扣除,便得到从 \(n-y+(y-x-1)\) 即 \(n-x-1\) 个物品中选 \(m-2-(y-x)\) 个 \(1\) 元,预处理组合数即可。
    对于第 \(5\) 种情况,由于其具有单调性,可以维护一个指针处理。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e3 + 7, mod = 998244353;
int Case, Task;
int n, m;
ll a[N];
ll pw[N];
int C[N << 1][N << 1];
void init(){
	pw[0] = 1;
	for(int i = 1; i <= N - 7; i++){
		pw[i] = (pw[i - 1] * 2) % mod;
 	}
	C[0][0] = 1;
	for(int i = 1; i <= N - 7; i++){
		C[i][0] = 1;
		for(int j = 1; j <= i; j++){
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
		}
	}
}
void Main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
	}
	sort(a + 1, a + n + 1);
	ll ans = 0;
	for(int i = 1; i <= n; i++){
		int p = 0;
		for(int j = i + 1; j <= n; j++){
			if(a[i] == a[j] || (m - 2 - (n - j)) < 0) continue;
			if(a[j] >= a[i] * 2) break;
			while(a[p + 1] + a[i] < a[j] && p < n) p++;
			ans = (ans + 1ll * C[n - i - 1][m - 2 - (n - j)] * pw[p] % mod) % mod;
		}
	}
	printf("%lld\n", (pw[n] - ans + mod) % mod);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> Case >> Task;
	init();
	while(Task--) Main();
	return 0; 
}

P7154 [USACO20DEC] Sleeping Cows P

题解:

我们先将 \(s_i\) 和 \(t_i\) 合并在一个数组中并升序排序,特别的,如果 \(s_i\) 和 \(t_i\) 相等,则优先考虑 \(s_i\)。
不难发现,每个 \(t_i\) 所能匹配的 \(s_i\) 都在其之前。
若这个匹配是合法的,
则需保证对于每一个被放弃的 \(t_i\),其所能匹配的 \(s_i\) 都未被放弃匹配。
因此不难得到一个 \(O(n^3)\) 的做法,设 \(f_{i,j}\) 为考虑前 \(i\) 个 \(t_i\),\(j\) 个 \(s_i\) 待匹配。
枚举最小的被放弃的 \(s_i\),再 \(O(n^2)\) DP即可。
这个dp在转移中显然做到最优了,考虑从状态定义上优化。
不难发现,我们并不关心具体哪一个 \(s_i\) 被放弃,只关心哪一段前缀被选取了。
改状态为 \(f_{i,j,k}\),其中 \(k \in \{ 0,1 \}\),代表前 \(i\) 个中的所有 \(s_i\) 是否全部被加入匹配。
接下来分类讨论一下

  1. 第 \(i\) 项为 \(s_i\)

\[f_{i,j,0}=f_{i-1,j,0}+f_{i-1,j-1,0}+f_{i-1,j,1} \\ f_{i,j,1}=f_{i-1,j-1,1}\\ \]

当 \(k=1\) 时,由于状态的定义,所以必须选择。
2. 第 \(i\) 项 为 \(t_i\)

\[f_{i,j,0}=f_{i-1,j+1,0} \times (j+1)\\ f_{i,j,1}=f_{i-1,j,1}+ f_{i-1,j+1,1} \times (j+1)\\ \]

当 \(k=0\) 时,由于此前存在放弃的 \(s_i\),如果放弃 \(t_i\),便不满足极大匹配的要求,所以强制选择。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 3007, mod = 1e9 + 7;
int n;
struct node{
	int w;
	bool t;
}a[N << 1];
ll f[2][N][2];
bool cmp1(node a, node b){
	return (a.w != b.w ? a.w < b.w : a.t < b.t);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++){
		cin >> a[i].w;
		a[i].t = 0;
	}
	for(int i = 1; i <= n; i++){
		cin >> a[i + n].w;
		a[i + n].t = 1;
	}
	n <<= 1;
	sort(a + 1, a + n + 1, cmp1);
	f[0][0][1] = 1;
	for(int i = 1, now = 1, pre = 0; i <= n; i++, now ^= 1, pre ^= 1){
		for(int j = 0; j <= (n >> 1); j++){
			if(a[i].t == 0){
				f[now][j][0] = (f[pre][j][0] + (j ? f[pre][j - 1][0] : 0) + f[pre][j][1]) % mod;
				if(j) f[now][j][1] = f[pre][j - 1][1];
				else f[now][j][1] = 0;
			}
			else{
				f[now][j][0] = f[pre][j + 1][0] * (j + 1) % mod;
				f[now][j][1] = (f[pre][j][1] + f[pre][j + 1][1] * (j + 1) % mod) % mod;
			}
		}
	}
	printf("%lld\n", (f[0][0][0] + f[0][0][1]) % mod);
	return 0; 
}

P8867 [NOIP2022] 建造军营

题解:

一道计数dp好题。
不难发现非割边是可以随便断的,于是可以边双缩点成一棵树,再在新的树上做 \(dp\)。
可以设 \(f_{i,0/1}\) 为以 \(i\) 为根的子树内是否存在军营的方案数。
设 \(j \in son_i\)。
则

\[f_{i,0} \leftarrow f_{i,0} \times 2f_{j,0} f_{i,1} \leftarrow f_{i,1} \times 2f_{j,0} + f_{i,1} \times f_{j,1} + f_{i,0} \times f_{j,1} \]

统计答案也并不难,我们枚举仅每个子树内存在军营的情况,设 \(E_i\)为每个子树内在原图中的边数,
则

\[ans= \sum_{i \neq root} f_{i,1} \times 2^{E_{root}-E_{i}-1}+f_{root,1} \]

不难发现这样算的正确性。
初始化也很简单,设 \(e_i\) 为原图中每个边双的边数,\(v_i\) 为原图中每个边双的点数,则

\[f_{i,0}=2^{e_i} f_{i,1}=2^{e_i+v_i}-2^{e_i} \]

GG.

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 7, M = 1e6 + 7, mod = 1e9 + 7;
int n, m;
int low[N], dfn[N];
int stk[N], top;
int belong[N];
int ecnt;
ll f[N][2];
int ec[N], sz[N];
int nsz[N];
ll ans;
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return sum;
}
struct Edge{
	int head[N], tot = 1;
	int cnt = 0;
	struct edge{
		int to, pre;
	}e[M << 1];
	void add(int x, int y){
		e[++tot] = {y, head[x]};
		head[x] = tot;
	}
	void dfs(int u, int from){
		dfn[u] = low[u] = ++cnt;
		stk[++top] = u;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(i == (from ^ 1)) continue;
			if(dfn[v] == 0){
				dfs(v, i);
				low[u] = min(low[u], low[v]);
			}
			else low[u] = min(low[u], dfn[v]);
		}
		if(low[u] == dfn[u]){
			ecnt++;
			do{
				belong[stk[top]] = ecnt;
				sz[ecnt]++;
				top--;
			}while(stk[top + 1] != u);
		}
	}
	void dp(int u, int fa){
		//printf("%d:%d %d\n", u, sz[u], ec[u]);
		nsz[u] = ec[u];
		f[u][0] = fpow(2, ec[u]), f[u][1] = (fpow(2, sz[u] + ec[u]) - f[u][0] + mod) % mod;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(v == fa) continue;
			dp(v, u);
			nsz[u] += nsz[v] + 1;
			f[u][1] = (f[u][1] * (2 * f[v][0] % mod + f[v][1]) % mod + f[u][0] * f[v][1] % mod) % mod;
			f[u][0] = f[u][0] * f[v][0] % mod * 2 % mod;
		}
		if(!fa) ans = (ans + f[u][1]) % mod;
		else ans = (ans + f[u][1] * fpow(2, m - nsz[u] - 1) % mod) % mod;
	}
}E, G;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++){
		int x, y;
		cin >> x >> y;
		E.add(x, y);
		E.add(y, x);
	}
	E.dfs(1, 0);
	for(int u = 1; u <= n; u++){
		for(int i = E.head[u]; i; i = E.e[i].pre){
			int v = E.e[i].to;
			if(belong[u] != belong[v]){
				G.add(belong[u], belong[v]);
			}
			else ec[belong[u]]++;
		}
	}
	for(int i = 1; i <= ecnt; i++) ec[i] /= 2; 
	G.dp(1, 0);
	printf("%lld\n", ans);
	return 0;
} 

P14666 [KenOI 2025] 游走题

题解:

直接维护路径长度显然不是很好做。
我们考虑维护每一条路径的点集大小再去求得路径长度。
对于一个树与其漫游的起点 \(s\),设有 \(f\) 种漫游方法。
考虑任意一个点集 \(S\)。
不难发现,从 \(s\) 到根节点的链上的点和根节点的儿子都是必选的,而这条链上的边数即为 \(s\) 的深度(在这里我们令根节点深度为 \(0\))。
则点集边数为 \(d+(|S|-d-1) \times 2\)。
则不难发现该树的答案为

\[\sum_{S}^{} d+(|S|-d-1) \times 2=2 \times |S| - d - 2 \]

不难发现这个东西是可以通过子树合并的,因此考虑树形 \(dp\)。
设 \(f_i\) 为子树内方案数,\(g_i\) 为子数内点集大小之和。
对于 \(v \in son_u\),

  1. 若 \(v\) 是必选点

\[g_u=g_u \times f_v+g_v \times f_u f_u=f_u \times f_v \]

  1. 反之

\[g_u=g_u \times (f_v+1)+g_v \times f_u f_u=f_u \times (f_v+1) \]

则答案为

\[g_1 \times 2+(dep_s-2) \times f_1 \]

解释一下转移方程的原理。
注意到转移时的 \(g\) 数组储存的是处理该子树之前的信息,
我们现在要将此前的信息与这棵子树的信息做合并,
合并的方法肯定就是用此前的一条路径对接上该子树的一条路径,
对于 \(f_u\) 个当前的方案,可以在每一个方案后面加上若干个和为 \(g_v\) 的路径
设当前方案的集合为 \(S\),\(v\)子树方案的集合为\(T\)。
则贡献为\(\sum_{S} \sum{T} |S_i| + |T_j|\)

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 7, mod = 1e9 + 7;
int n, s;
int dep[N];
ll f[N], g[N];
int fa[N];
bool must[N];
struct Edge{
	int head[N], tot;
	struct edge{
		int to, pre;
	}e[N << 1];
	void add(int x, int y){
		e[++tot] = {y, head[x]};
		head[x] = tot;
	}
	void dfs1(int u, int ft){
		fa[u] = ft;
		if(ft) dep[u] = dep[ft] + 1;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(v == ft) continue;
			dfs1(v, u);
		}
	}
	void dfs2(int u, int ft){
		f[u] = 1;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(v == ft) continue;
			dfs2(v, u);
			if(must[v]){
				g[u] = (g[u] * f[v] % mod + g[v] * f[u] % mod) % mod;
				f[u] = f[u] * f[v] % mod;
			}
			else{
				g[u] = (g[u] * (f[v] + 1) % mod + g[v] * f[u] % mod) % mod;
				f[u] = f[u] * (f[v] + 1) % mod;
			}
		}
		g[u] = (g[u] + f[u]) % mod;
	}
}E;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> s;
	for(int i = 1; i < n; i++){
		int u, v;
		cin >> u >> v;
		E.add(u, v);
		E.add(v, u);
	}
	E.dfs1(1, 0);
	for(int i = E.head[1]; i; i = E.e[i].pre){
		int v = E.e[i].to;
		must[v] = 1;
	}
	for(int u = s; u; u = fa[u]){
		must[u] = 1;
	}
	E.dfs2(1, 0);
	//printf("%lld\n",g[1]);
	printf("%lld\n", (g[1] * 2 % mod - 1ll * (dep[s] + 2) * f[1] % mod + mod) % mod);
	return 0; 
}

P14982 [USACO26JAN1] Supervision G

题解:

考虑只对教练做 \(dp\)。
很容易想到设 \(f_i\) 代表最后选择第 \(i\) 头牛(教练)的方案数。
转移方程即为 \(f_i= \sum_{j < i} f_j \times 2^{k}\),\(k\) 为 \(j\) 和 \(i\) 之间可以被 \(j\) 覆盖到的学员数量。
不难发现这个方法是 \(O(n^2)\) 的,考虑优化。
考虑维护满足 \(p_j+d>=p_i\) 的一个窗口,用单指针维护即可。
每次枚举到一个学员,就更新这个窗口,并将该窗口内的值 \(\times 2\)。
对于这些操作,我们可以考虑维护一个 \(ans= \sum_{j < i} f_j \times 2^{k}\) 和 \(sum = \sum_{p_j + d >= i} f_j \times 2^{k}\)。
再维护一下 \(k\) 即可。
时间复杂度便优化成了 \(O(n)\)。
当然,直接上线段树也是可以的。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 7, mod = 1e9 + 7;
int n, d;
int p[N], o[N];
ll pw[N], f[N];
void init(){
	pw[0] = 1;
	for(int i = 1; i <= N - 7; i++) pw[i] = (pw[i - 1] << 1) % mod;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	init();
	cin >> n >> d;
	for(int i = 1; i <= n; i++){
		cin >> p[i] >> o[i];
	}
	ll sum = 0, ans = 0;
	for(int i = 1, j = 1, k = 0; i <= n; i++){
		if(o[i]){
			f[i] = (ans + 1) % mod;
			ans = (ans + f[i]) % mod;
			sum = (sum + f[i]) % mod;
		}
		else{
			while(p[j] + d < p[i]){
				sum = (sum - f[j] * pw[k] % mod + mod) % mod;
				if(!o[j]) k--;
				j++;
			}
			ans = (ans + sum) % mod;
			sum = (sum << 1) % mod;
			k++;
		}
	}
	printf("%lld\n", ans);
	return 0;
}

P11363 [NOIP2024] 树的遍历

题解:

一道思维含量极高的题目。
我们可以先观察一下部分分的做法。
对于 \(k=1\),
不难发现,任意一个原图点所连的边在新树中必定形成一条链,
那么便会形成 \(n\) 条链。
而每条链的起点都会被上一条链的终点所钦定,但剩下可以随便排,
因此答案为:

\[\prod_{i=1}^{n} (d_i-1) \]

其中 \(d_i\) 为点 \(i\) 的度数。
我们再将讨论范围扩展到所有情况,
不难发现可以容斥,
用 \(k=1\) 的答案 \(\times k\) 再减去重复的情况即可。
设 \(f_i\) 为选了 \(i\) 条边作为 dfs 起点的方案数。
答案即为 \(\sum (-1)^{i+1} f_i\)。
再分析一下要如何才会算重。
如果一个方案被计算多次,就一定是可以被多条边作为起点生成出来。
我们在原图中任意一个点所连边形成的链上讨论,
由于链只有两个方向,因此那若干个起点也只能从这两个方向搜到这一条链。
这也引出了这题最重要的一个性质:被算重的若干个起点必定在原树的一条链上,我们称之为关键链。
我们便可以枚举任意两个关键边作为关键链的两端,处理一下这条链中的关键点个数。
对于这条关键链上的点,因为这条链上的起点和终点都被钦定了,因此其总贡献变为 \(\prod_{i \in link} (d_i-2)\)。
设这个式子的值为 \(w\),这条链上有 \(l\) 条链, 则其对 \(f_i\) 的贡献为 \(w \times C_{l-2}^{i-2}\)
答案即为

\[\sum_{i=1}^{n} (-1)^{i+1} f_i \]

由容斥的性质,可以发现所有 \(l \geq 3\) 对答案都没有贡献,因此可以只统计相邻的关键边。

Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, mod = 1e9 + 7;
using ll = long long;
int Case, Task;
int n, k;
int deg[N];
bool is[N << 1], vis[N];
ll s, ans, fac[N], inv[N];
ll f[N], g[N];
struct Edge{
	struct edge{
		int to, pre;
	}e[N << 1];
	int head[N], tot;
	void init(){
		tot = 0;
		memset(head, 0, sizeof(head));
	}
	void add(int u, int v){
		e[++tot] = {v, head[u]};
		head[u] = tot;
		deg[u]++;
	}
	void dfs1(int u, int fa){
		vis[u] = 1;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(v == fa) continue;
			if(is[i]){
				f[u] = (f[u] + s) % mod;
			}
			else{
				dfs1(v, u);
				f[u] = (f[u] + f[v]) % mod;
			}
		}
		f[u] = f[u] * inv[deg[u] - 1] % mod;
	}
	void dfs2(int u, int fa){
		ll cnt = 0;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(is[i]) cnt++;
			else{
				if(v != fa) g[u] = (g[u] + f[v]) % mod;
				else g[u] = (g[u] + g[v]) % mod;
			}
		}
		ans = (ans + (cnt * g[u] % mod + s * cnt % mod * (cnt - 1) % mod) % mod * inv[deg[u] - 1] % mod) % mod;
		g[u] = (g[u] + cnt * s % mod) % mod;
		ll t = g[u];
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			if(v != fa && !is[i]){
				g[u] = (g[u] + mod - f[v]) * inv[deg[u] - 1] % mod;
				dfs2(v, u);
				g[u] = t;
			}
		}
		g[u] = g[u] * inv[deg[u] - 1] % mod;
	}
}E;
struct eUnion{
	int x, y;
}U[N];
ll fpow(ll x, ll y = mod - 2){
	if(!y) return 1;
	else return ((y & 1) * x) % mod * fpow(x * x % mod, y >> 1) % mod;
}
void init(){
	fac[0] = fac[1] = inv[0] = inv[1] = 1;
	for(int i = 2; i <= N - 7; i++) fac[i] = fac[i - 1] * i % mod;
	for(int i = 2; i <= N - 7; i++) inv[i] = mod - (mod / i) * inv[mod % i] % mod;
}
void Refresh(){
	ans = 0;
	s = 1;
	E.init();
	memset(is, 0, sizeof(is));
	memset(deg, 0, sizeof(deg));
	memset(f, 0, sizeof(f));
	memset(g, 0, sizeof(g));
	memset(vis, 0, sizeof(vis));
}
void Main(){
	Refresh();
	cin >> n >> k;
	for(int i = 1, u, v; i < n; i++){
		cin >> u >> v;
		E.add(u, v);
		E.add(v, u);
		U[i] = {u, v};
	}
	for(int i = 1; i <= k; i++){
		int x;
		cin >> x;
		is[x * 2 - 1] = is[x * 2] = 1;
	}
	s = 1;
	for(int i = 1; i <= n; i++){
		s = s * fac[deg[i] - 1] % mod;
	}
	for(int i = 1; i <= n; i++){
		if(!vis[i]){
			E.dfs1(i, 0);
			E.dfs2(i, 0);
		}
	}
	printf("%lld\n", (s * k + mod - ans * inv[2] % mod) % mod);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	init();
	cin >> Case >> Task;
	while(Task--) Main();
	return 0;
}

P2523 [HAOI2011] Problem c

题解:

先考虑无解的情况
对于第 \(i\) 到 \(n\) 个位置的编号的人的数量 \(s\),当 \(s>n-i+1\) 时,该方案无解。
同时这也是无解的唯一条件。
因此可以考虑围绕这个条件来设计状态,
设 \(f_{i,j}\) 为第 \(i\) 到 \(n\) 个位置的编号的人的数量为 \(j\) 的方案数。
考虑只对未确定编号的人进行 dp。
转移方程即为

\[f_{i,j}=\sum_{k=0}^{k \leq j} f_{i+1,j-k} \times C_j^k \]

其中组合数的意义是区分每个人,即从 \(j\) 个人中任意选 \(k\) 个。
再处理一下 \(j\) 的边界即可。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 307;
int Task;
int n, m, mod;
int s[N];
ll C[N][N], f[N][N];
void pre_make(){
	memset(s, 0, sizeof(s));
	memset(f, 0, sizeof(f));
	memset(C, 0, sizeof(C));
	C[0][0] = 1;
	for(int i = 1; i <= n; i++){
		C[i][0] = 1;
		for(int j = 1; j <= i; j++){
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
		}
	}
}
void Main(){
	cin >> n >> m >> mod;
	pre_make();
	for(int i = 1; i <= m; i++){
		int x, y;
		cin >> x >> y;
		s[y]++;
	}
	for(int i = n; i; i--){
		s[i] += s[i + 1];
		if(s[i] > (n - i + 1)){
			printf("NO\n");
			return;
		}
	}
	f[n + 1][0] = 1;
	for(int i = n; i; i--){
		for(int j = 0; j <= n - s[i] - i + 1; j++){
			for(int k = 0; k <= j; k++){
				f[i][j] = (f[i][j] + f[i + 1][j - k] * C[j][k]) % mod;
			}
		}
	}
	printf("YES %lld\n", f[1][n - m]);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> Task;
	while(Task--) Main();
	return 0;
}

P14568 【MX-S12-T3】排列

排列计数 dp 好题。
发现这道题目用绝对大小处理没什么前途,试着去处理对于每一个前缀的相对大小。
考虑限制如何刻画成插入。
发现 \(op_i = \{ 0,1 \}\) 的情况是好考虑的,因为是对前缀的限制。
但 \(op_i = \{ 2,3 \}\) 的情况是对后缀的限制,并不好处理。
考虑贡献延后钦定,设 \(f_{i,j}\) 为考虑到下标 \([1,i]\),其中有 \(j\) 个位置还未确定具体数值。

  1. 当 \(op_i = \{ 0,1 \}\) 时,

\[f_{i,j}=f{i-1,j-1} \]

在这种情况下,当前这个位置的绝对大小是无法确定的。

  1. 当 $op_i = { 2,3 } $ 时:

\[f_{i,j} = \sum_{k=0}^{k \leq j} f_{i-1,k} \]

对后缀的限制会使得后面一部分数的绝对大小被确定,也自然会影响到前面一部分数的绝对大小,此时我们便要结算此前欠下的贡献,枚举在这一次操作中被确定的位置的数量。

Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 5007, mod = 998244353;
int n;
int op[N];
int f[N][N], g[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> op[i];
	bool flg = 0;
	for(int i = 1; i <= n; i++){
		for(int j = i + 1; j <= n; j++){
			if((op[i] == 2 && op[j] == 0) || (op[i] == 3 && op[j] == 1)){
				flg = 1;
				break;
			}
		}
	}
	if(flg){
		printf("0\n");
		return 0;
	}
	f[0][0] = g[0][0] = 1;
	for(int i = 1; i <= n; i++){
		for(int j = 0; j <= n; j++){
			if(op[i] < 2 && j) f[i][j] = f[i - 1][j - 1];
			if(op[i] >= 2) f[i][j] = g[i - 1][j];
		}
		for(int j = n; j >= 0; j--) g[i][j] = (g[i][j + 1] + f[i][j]) % mod;
	}
	printf("%d\n", g[n][0]);
	return 0;
}

P12742 [POI 2016 R3] 信使 Messenger

险些切紫?
答案的要求是不能经过起点和终点,考虑做个容斥,强制所有路线不重复经过起点,再减去其中重复经过终点的路线数量。
设 \(f_{k,i,j}\) 为 从 \(i\) 走到 \(j\) 走了 \(k\) 步且不重复经过 \(i\) 的方案数,\(g_{k,i,j}\) 为从 \(i\) 走回 \(i\) 走了 \(k\) 步且其中不经过 \(j\) 的方案数,\(h_{k,i,j}\) 为从 \(i\) 走到 \(j\) 走了 \(k\) 步且无任何限制的方案数,\(ans_{k,i,j}\) 为答案。

那么 \(ans\) 的转移方程即为

\[ans_{k,i,j} = f_{k,i,j} - \sum_{1 leq l < k} ans_{l,i,j} \times g_{k - l, j, i} \]

枚举第一次走到 \(j\) 的时间即可。
\(f\) 和 \(g\) 的处理非常简单,这里就不过多赘述了,着重说一下 \(g\) 的转移,考虑和上面一样做个容斥。

\[g_{k,i,j} = h_{k,i,i} - \sum_{1 \leq l < k} h_{l, i, j} \times f_{k - l, j, i} \]

枚举最后一次走到 \(j\) 的时间即可。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 107, M = N * N;
int n, m, mod, q;
int e[N][N];
ll f[57][N][N], g[57][N][N], h[57][N][N], ans[57][N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m >> mod;
	for(int i = 1, x, y; i <= m; i++){
		cin >> x >> y;
		e[x][y] = 1;
	}
	for(int i = 1; i <= n; i++) f[0][i][i] = h[0][i][i] = 1;
	for(int k = 1; k <= 50; k++){
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				for(int l = 1; l <= n; l++){
					if(!e[l][j]) continue;
					h[k][i][j] = (h[k][i][j] + h[k - 1][i][l]) % mod;
					if(i != j) f[k][i][j] = (f[k][i][j] + f[k - 1][i][l]) % mod;
				}
			}
		}
	}
	for(int k = 1; k <= 50; k++){
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				if(i == j) continue;
				g[k][i][j] = h[k][i][i];
				for(int l = 1; l < k; l++){
					g[k][i][j] = (g[k][i][j] - h[l][i][j] * f[k - l][j][i] % mod + mod) % mod;
				}
			}
		}
	}
	for(int k = 1; k <= 50; k++){
		for(int i = 1; i <= n; i++){
			for(int j = 1; j <= n; j++){
				if(i == j) continue;
				ans[k][i][j] = f[k][i][j];
				for(int l = 1; l < k; l++){
					ans[k][i][j] = (ans[k][i][j] - ans[l][i][j] * g[k - l][j][i] % mod + mod) % mod;
				}
			}
		}
	}
	cin >> q;
	while(q--){
		int u, v, d;
		cin >> u >> v >> d;
		printf("%d\n", ans[d][u][v]);
	}
	return 0;
}

P7293 [USACO21JAN] Sum of Distances P

模拟一下走的方式可以发现等价于在 \(k\) 张图上同时走,那么先到达的点便可以选择原地横跳等其他未到达的点。
这就要求这 \(k\) 个点路程的奇偶性相同。
考虑先处理出每个点离其所在的图的 \(1\) 号点的奇偶最短路,设其分别为 \(dis_{i,0/1}\)。
对于一个 \(G\) 上的节点 \((a_1, a_2, \cdots, a_k)\),其距离原点的距离即为

\[\min( \max(dis_{a_i, 0}), \max(dis_{a_i, 1})) \]

。
利用容斥将其转化为

\[\max(dis_{a_i, 0}) + \max({dis_{a_i, 1}}) - \max(\max(dis_{a_i, 0}), \max({dis_{a_i, 1}})) \]

分别计算贡献即可。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 7, M = 2e5 + 7, P = 1e9 + 7;
ll inv[N];
int K, n;
int root[N];
int hh, tt;
int dis[N][2];
vector<int>g[3][N];
int num[N];
struct Node{
	int u, w;
}q[N << 1];
struct Edge{
	struct edge{
		int to, pre;
	}e[M << 1];
	int head[N], tot;
	void add(int x, int y){
		e[++tot] = {y, head[x]};
		head[x] = tot;
	}
	void bfs(int op){
		hh = 1, tt = 0;
		q[++tt] = {root[op], 0};
		dis[root[op]][0] = 0;
		while(hh <= tt){
			int u = q[hh].u, w = q[hh].w;
			hh++;
			for(int i = head[u]; i; i = e[i].pre){
				int v = e[i].to;
				if(dis[v][w ^ 1] == -1){
					dis[v][w ^ 1] = dis[u][w] + 1;
					q[++tt] = {v, w ^ 1};
				}
			}
		}
		for(int i = root[op]; i < root[op + 1]; i++){
			if(dis[i][0] >= 0) g[0][dis[i][0]].push_back(op);
			if(dis[i][1] >= 0) g[1][dis[i][1]].push_back(op);
			if(min(dis[i][0], dis[i][1]) >= 0) g[2][max(dis[i][0], dis[i][1])].push_back(op);
		}
	}
}E;
void init(){
	inv[1] = 1;
	for(int i = 2; i <= N - 7; i++){
		inv[i] = 1ll * (P - (P / i)) * inv[P % i] % P;
	}
}
ll solve(vector<int>*v){
	int cnt = K;
	ll sum = 0, tmp = 1;
	memset(num, 0, sizeof(num));
	for(int i = 0; i <= n; i++){
		for(int u : v[i]){
			if(num[u]) tmp = tmp * inv[num[u]] % P;
			else cnt--;
			if(!cnt) sum = (sum + tmp * i % P) % P;
			tmp = tmp * (++num[u]) % P;
		}
	}
	return sum;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	init();
	cin >> K;
	for(int i = 1; i <= K; i++){
		int n_, m_;
		cin >> n_ >> m_;
		root[i] = n + 1;
		for(int j = 1; j <= m_; j++){
			int x, y;
			cin >> x >> y;
			E.add(n + x, n + y);
			E.add(n + y, n + x);
		}
		n += n_;
	}
	root[K + 1] = n + 1;
	memset(dis, -1, sizeof(dis));
	for(int i = 1; i <= K; i++) E.bfs(i);
	printf("%lld\n", (solve(g[0]) + solve(g[1]) - solve(g[2]) + P) % P);
	return 0;
}

P6144 [USACO20FEB] Help Yourself P

先只考虑 \(k=1\) 的情况。
先按左端点将所有线段排序,考虑新加入一条线段 \([l,r]\) 会产生的所有贡献,同时考虑对值域进行 dp,设 \(f_i\) 为线段右端点为 \(i\) 会产生的贡献。
对于 \(i < l\),其会新产生一个连通块,因此 \(f_r \gets f_i + 1\) 。
对于 \(i \in [l,r]\),其不会产生连通块,因此 \(f_r \gets f_i\)。
对于 \(i > r\),其将该线段包含,贡献应记在 \(i\) 上,同时不难发现该线段是否选择不影响最终答案,因此 \(f_i = f_i \times 2\)。
将问题扩展到 \(k>1\),二项式定理计算,线段树上维护多项式即可。

Code

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 7, K = 17, P = 1e9 + 7;
int n, k;
int m;
int C[K][K];
inline int read(){
	int sum = 0;
	char c = getchar_unlocked();
	while(c < '0' || c > '9'){
		c = getchar_unlocked();
	}
	while(c >= '0' && c <= '9'){
		sum = (sum << 1) + (sum << 3) + (c - 48);
		c = getchar_unlocked();
	}
	return sum;
}
inline void write(ll x){
	if(!x) return;
	write(x / 10);
	putchar(x % 10 + '0');
}
struct Segment{
	int l, r;
}a[N];
struct Segment_Tree{
	struct sgt{
		int l, r;
		ll mul, sum;
	}t[N << 3];
	inline void push_up(int x){
		t[x].sum = (t[x << 1].sum + t[x << 1 | 1].sum) % P;
	}
	inline void push_mul(int x, ll d){
		t[x].mul = t[x].mul * d % P;
		t[x].sum = t[x].sum * d % P;
	}
	inline void push_down(int x){
		if(t[x].mul == 1) return;
		push_mul(x << 1, t[x].mul);
		push_mul(x << 1 | 1, t[x].mul);
		t[x].mul = 1;
	}
	inline void build(int x, int l, int r){
		t[x].l = l;
		t[x].r = r;
		t[x].mul = 1;
		if(l == r){
			return;
		}
		int mid = (l + r) >> 1;
		build(x << 1, l, mid);
		build(x << 1 | 1, mid + 1, r);
	}
	inline void update_mul(int x, int l, int r){
		if(t[x].l >= l && t[x].r <= r){
			push_mul(x, 2);
			return;
		}
		push_down(x);
		int mid = (t[x].l + t[x].r) >> 1;
		if(l <= mid) update_mul(x << 1, l, r);
		if(r > mid) update_mul(x << 1 | 1, l, r);
		push_up(x);
	}
	inline void update(int x, int p, ll d){
		if(t[x].l == t[x].r){
			t[x].sum = d;
			return;
		}
		push_down(x);
		int mid = (t[x].l + t[x].r) >> 1;
		if(p <= mid) update(x << 1, p, d);
		else update(x << 1 | 1, p, d);
		push_up(x);
	}
	inline ll query(int x, int l, int r){
		if(t[x].l >= l && t[x].r <= r){
			return t[x].sum;
		}
		push_down(x);
		int mid = (t[x].l + t[x].r) >> 1;
		ll sum = 0;
		if(l <= mid) sum += query(x << 1, l, r);
		if(r > mid) sum += query(x << 1 | 1, l, r);
		return sum % P;
	}
}T[K];
bool cmp1(Segment a, Segment b){
	return a.l < b.l;
}
void init(){
	C[0][0] = 1;
	for(int i = 1; i <= 10; i++){
		C[i][0] = 1;
		for(int j = 1; j <= i; j++){
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
		}
	}
}
int main(){
	init();
	n = read(), k = read();
	m = n << 1;
	for(int i = 0; i <= k; i++) T[i].build(1, 1, m);
	for(int i = 1; i <= n; i++){
		a[i].l = read(), a[i].r = read();
	}
	sort(a + 1, a + n + 1, cmp1);
	for(int i = 1; i <= n; i++){
		int l = a[i].l, r = a[i].r;
		for(int j = 0; j <= k; j++){
			if(r + 1 <= m) T[j].update_mul(1, r + 1, m);
			ll sum = 1;
			for(int p = 0; p <= j; p++){
				sum = (sum + T[p].query(1, 1, l - 1) * C[j][p]) % P; 
			}
			sum = (sum + T[j].query(1, l, r - 1)) % P;
			T[j].update(1, r, sum);
		}
	}
	ll ans = 0;
	for(int i = 1; i <= m; i++){
		ans = (ans + T[k].query(1, i, i) % P) % P;
	}
	write(ans);
	return 0;
}

AT_arc107_d [ARC107D] Number of Multisets

思维含量很高的题目。
由于集合是无序的,考虑将集合从小到大排序。
发现任意一个集合都可以由一个初始全是 \(1\) 的集合再在若干个前缀上乘 \(\frac{1}{2}\) 得到。
那我们就可以通过模拟这个过程来计数。
设 \(f_{i,j}\) 为选了 \(i\) 个数且当前和为 \(j\) 的方案数。
转移方程即为

\[f_{i,j} = f_{i-1,j-1} + f_{i, 2j} \]

原理也很简单,每一步可以选择添加一个 \(1\) 或给此前的前缀乘上 \(\frac{1}{2}\)。

Code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 3007, P = 998244353;
int n, m;
ll f[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	f[0][0] = 1;
	for(int i = 1; i <= n; i++){
		for(int j = i; j >= 0; j--){
			if(j) f[i][j] = f[i - 1][j - 1];
			if((j << 1) <= i) f[i][j] = (f[i][j] + f[i][j << 1]) % P;
		}
	}
	printf("%lld\n", f[n][m]);
	return 0;
}
posted @ 2026-01-22 13:40  EzSun599  阅读(26)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3