• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
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] 员工招聘

题目背景

由于评测机性能差异,本题时限提升 1 秒。

题目描述

小 Z 和小 H 想要合伙开一家公司,共有 \(n\) 人前来应聘,编号为 \(1 \sim n\)。小 Z 和小 H 希望录用至少 \(m\) 人。

小 H 是面试官,将在接下来 \(n\) 天每天面试一个人。小 Z 负责决定应聘人前来面试的顺序。具体地,小 Z 可以选择一个 \(1 \sim n\) 的排列 \(p\),然后在第 \(i\) (\(1 \leq i \leq n\)) 天通知编号为 \(p_i\) 的人前来面试。

小 H 准备了 \(n\) 套难度不一的面试题。由于 \(n\) 个前来应聘的人水平大致相同,因此对于同一套题,所有人的作答结果是一致的。具体地,第 \(i\) (\(1 \leq i \leq n\)) 天的面试题的难度为 \(s_i \in \{0,1\}\),其中 \(s_i = 0\) 表示这套题的难度较高,没有人能够做出;\(s_i = 1\) 表示这套题的难度较低,所有人都能做出。小 H 会根据面试者的作答结果决定是否录用,即如果面试者没有做出面试题,则会拒绝,否则会录用。

然而,每个人的耐心都有一定的上限,如果在他面试之前未录用的人数过多,则他会直接放弃参加面试。具体地,编号为 \(i\) (\(1 \leq i \leq n\)) 的人的耐心上限可以用非负整数 \(c_i\) 描述,若在他之前已经有不少于 \(c_i\) 人被拒绝或放弃参加面试,则他也将放弃参加面试。

小 Z 想知道一共有多少种面试的顺序 \(p\) 能够让他们录用至少 \(m\) 人。你需要帮助小 Z 求出,能够录用至少 \(m\) 人的排列 \(p\) 的数量。由于答案可能较大,你只需要求出答案对 \(998\,244\,353\) 取模后的结果。

输入格式

输入的第一行包含两个正整数 \(n, m\),分别表示前来应聘的人数和希望录用的人数。

输入的第二行包含一个长度为 \(n\) 的字符串 \(s_1 \dots s_n\),表示每一天的面试题的难度。

输入的第三行包含 \(n\) 个非负整数 \(c_1, c_2, \dots, c_n\),表示每个人的耐心上限。

输出格式

输出一行一个非负整数,表示能够录用至少 \(m\) 人的排列 \(p\) 的数量对 \(998\,244\,353\) 取模后的结果。

输入输出样例 #1

输入 #1

3 2
101
1 1 2

输出 #1

2

输入输出样例 #2

输入 #2

10 5
1101111011
6 0 4 2 1 2 5 4 3 3

输出 #2

2204128

说明/提示

【样例 1 解释】

共有以下 2 种面试的顺序 \(p\) 能够让小 Z 和小 H 录用至少 2 人:

  1. \(p = [1,2,3]\), 依次录用编号为 1 的人和编号为 3 的人;
  2. \(p = [2,1,3]\), 依次录用编号为 2 的人和编号为 3 的人。

【样例 3】

见选手目录下的 \(\textbf{\textit{employ/employ3.in}}\) 与 \(\textbf{\textit{employ/employ3.ans}}\)。

该样例满足测试点 6 ~ 8 的约束条件。

【样例 4】

见选手目录下的 \(\textbf{\textit{employ/employ4.in}}\) 与 \(\textbf{\textit{employ/employ4.ans}}\)。

该样例满足测试点 12 ~ 14 的约束条件。

【样例 5】

见选手目录下的 \(\textbf{\textit{employ/employ5.in}}\) 与 \(\textbf{\textit{employ/employ5.ans}}\)。

该样例满足测试点 18 ~ 21 的约束条件。

【数据范围】

对于所有测试数据,保证:

  • \(1 \leq m \leq n \leq 500\);
  • 对于所有 \(1 \leq i \leq n\),均有 \(s_i \in \{0,1\}\);
  • 对于所有 \(1 \leq i \leq n\),均有 \(0 \leq c_i \leq n\)。

::cute-table{tuack}

测试点编号 \(n \leq\) \(m\) 特殊性质
\(1,2\) \(10\) \(\leq n\) 无
\(3 \sim 5\) \(18\) ^ ^
\(6 \sim 8\) \(10^2\) ^ A
\(9 \sim 11\) ^ ^ 无
\(12 \sim 14\) \(500\) \(=1\) ^
\(15\) ^ \(=n\) ^
\(16,17\) ^ \(\leq n\) A
\(18 \sim 21\) ^ ^ B
\(22 \sim 25\) ^ ^ 无

特殊性质 A: 对于所有 \(1 \leq i \leq n\),均有 \(s_i = 1\)。

特殊性质 B: 在 \(s_1, s_2, \dots, s_n\) 中最多只有 18 个取值为 1,即 \(\sum_{i=1}^{n} s_i \leq 18\)。

题解

我们设 \(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=1\) 且 \(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

题目背景

光芒浸透圣巢,她正犯下弥天之错。

所剩寥寥无几的信仰,为什么始终执着。

我将作灯塔,照耀王国。

但在那之前有更重要的事去做,

无论什么代价都在所不惜,尽管我所剩无多……

题目描述

白王正在最后一次参观他建造的宏伟宫殿。现在假设宫殿由 \(n\) 个房间构成,房间之间有若干个单向通道。出于白王奇怪的装修癖好(不是指到处安电锯),对于第 \(i\) 个房间,它向编号在区间 \([l_i,r_i]\) 中的所有房间都连有一条单向通道。例如,\(4\) 号房间向 \([2,5]\) 连有单向通道,就意味着从 \(4\) 号房间到 \(2,3,4,5\) 号房间各有一条单向通道(一个房间可以向自己连有通道)。当一个房间向 \([0,0]\) 连有通道时,表示没有从这个房间出发的通道。

白王提出了 \(q\) 个问题,每次询问从 \(a\) 号房间,通过恰好 \(m\) 条单向通道且不经过 \(c\) 号房间到达 \(b\) 号房间的方案数(两方案不同,当且仅当存在 \(i\) 使得两方案通过的第 \(i\) 条通道不同)。因为这个数字可能会很大,所以白王让你将答案模 \(998244353\) 后再回答。

输入格式

第一行,两个整数 \(n, q\) 表示点数和询问数。

接下来 \(n\) 行,每行两个整数 \(l, r\),第 \(i+1\) 行的整数 \(l_i, r_i\) 表示 \(i\) 号节点向区间 \([l_i, r_i]\) 中的每个点都连了一条单向边。当 \(l_i=r_i=0\) 时,表示该节点没有向任何点连边。

接下来 \(q\) 行,每行四个整数 \(a, b, c, m\) 表示一个询问。

输出格式

\(q\) 行,每行一个整数,第 \(i\) 行的数字表示第 \(i\) 个询问的方案数模 \(998244353\) 的结果。

输入输出样例 #1

输入 #1

4 5
2 3
1 1
2 4
0 0
1 3 4 5
1 4 2 4
2 3 1 2
4 4 3 0
1 3 2 5

输出 #1

5
1
0
1
1

输入输出样例 #2

输入 #2

10 10
6 6
4 10
2 5
1 7
3 4
5 7
4 10
1 7
1 3
2 5
8 8 5 1
4 7 5 3
5 9 4 4
1 5 5 2
6 2 10 2
3 3 7 4
1 10 1 2
6 2 4 4
9 2 1 4
9 10 3 2

输出 #2

0
17
2
0
0
46
0
12
23
1

输入输出样例 #3

输入 #3

10 10
2 6
6 9
5 7
3 9
0 0
0 0
3 5
5 5
3 6
1 10
5 9 6 3
10 8 6 4
10 8 5 1
8 6 5 4
7 2 5 4
6 1 5 3
10 4 5 1
5 5 6 0
7 9 6 4
4 9 6 2

输出 #3

0
17
1
0
0
0
1
1
4
1

说明/提示

样例说明

在样例一中,\(1\) 号房间能到达 \(2,3\) 号房间,\(2\) 号房间能到达 \(1\) 号房间,\(3\) 号房间能到达 \(2,3,4\) 号房间,\(4\) 号房间不能到达任何房间。

对于第一个询问,从 \(1\) 号房间经过 \(5\) 条通道且不经过 \(4\) 号房间到达 \(3\) 号房间的方案有 121213,121333,133213,132133,133333 共五种。


数据范围

本题采用捆绑测试。

编号 特殊性质 空间限制 分数
0 \(n\le10\),\(q\le10\),\(m\le4\) 256MB 10pts
1 \(n\le100\),\(q\le10^4\),\(m\le40\) 256MB 15pts
2 对于所有询问,\(l_c=r_c=0\) 256MB 15pts
3 无 256MB 30pts
4 无 128MB 30pts

对于 \(100\%\) 的数据,\(1\le n \le 500\),\(1\le q \le 10^5\),\(1\le m \le 100\),\(0 \le l_i \le r_i \le n\),\(1 \le a,b,c \le n\)。当且仅当 \(l_i=0\) 时 \(r_i=0\)。时间限制均为 1s。


提示

注意空间常数。

题解:

这一题展示了状态设计的重要性
考虑正难则反,将不去 \(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

题目描述

给定一个由 \(1,\dots,n\) 和 \(-1\) 组成的数列 \(a_1,\dots,a_n\),以及一个整数 \(d\)。请问有多少个满足以下条件的数列 \(p_1,\dots,p_n\)?请输出答案对 \(998244353\) 取模后的结果。

  • \(p_1,\dots,p_n\) 是 \(1,\dots,n\) 的一个排列。
  • 对于 \(i=1,\dots,n\),如果 \(a_i\neq -1\),则 \(a_i=p_i\)(也就是说,可以通过将 \(a_1,\dots,a_n\) 中的 \(-1\) 项适当替换,得到 \(p_1,\dots,p_n\))。
  • 对于 \(i=1,\dots,n\),有 \(|p_i-i|\le d\)。

输入格式

输入通过标准输入按以下格式给出。

\(n\) \(d\) \(a_1\) \(a_2\) \(\dots\) \(a_n\)

输出格式

请输出满足条件的数列个数对 \(998244353\) 取模后的结果。

输入输出样例 #1

输入 #1

4 2
3 -1 1 -1

输出 #1

2

输入输出样例 #2

输入 #2

5 1
2 3 4 5 -1

输出 #2

0

输入输出样例 #3

输入 #3

16 5
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1

输出 #3

794673086

说明/提示

限制条件

  • \(1 \le d \le 5\)
  • \(d < n \le 500\)
  • \(1 \le a_i \le n\) 或 \(a_i = -1\)
  • 如果 \(a_i \neq -1\),则 \(|a_i-i| \le d\)
  • 如果 \(i \neq j\) 且 \(a_i, a_j \neq -1\),则 \(a_i \neq a_j\)
  • 所有输入均为整数

样例解释 1

\((3,2,1,4)\) 和 \((3,4,1,2)\) 满足条件。

样例解释 2

将 \(-1\) 替换后能得到的 \(1,2,3,4,5\) 的排列只有 \((2,3,4,5,1)\)。但该排列的第 \(5\) 项不满足条件,因此答案为 \(0\)。

样例解释 3

请输出答案对 \(998244353\) 取模后的结果。

由 ChatGPT 4.1 翻译

题解:

这一题其实并不是很难,但实现时把我难受得很。
不难发现,对于每个 \(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

题目描述

你有三个整数 \(n, k, m\) 以及 \(m\) 个限制 \((l_1, r_1, x_1), (l_2, r_2, x_2), \ldots, (l_m, r_m, x_m)\)。

计算满足下列条件的,长度为 \(n\) 的序列 \(a\) 的个数:

  • 对于每个 \(1 \le i \le n\),\(0 \le a_i \lt 2 ^ k\)。
  • 对于每个 \(1 \le i \le m\),数字的按位与 \(a[l_i] \text{ and } a[l_i + 1] \text{ and } \ldots \text{ and } a[r_i] = x_i\)。

两个序列 \(a, b\) 被认为是不同的,当且仅当存在一个位置 \(i\) 满足 \(a_i \neq b_i\)。

由于答案可能过大,请输出其对 \(998\ 244\ 353\) 取模的结果。

输入格式

第一行输入三个整数 \(n, k, m ~(1 \le n \le 5 \cdot 10 ^ 5; 1 \le k \le 30; 0 \le m \le 5 \cdot 10 ^ 5)~\),分别表示数组 \(a\) 的长度,\(a\) 中元素的值域,以及限制的个数。

接下来 \(m\) 行,每行描述一个限制 \(l_i, r_i, x_i ~ (1 \le l_i \le r_i \le n; 0 \le x_i \lt 2 ^ k)\),分别表示限制的线段区间以及按位与值。

输出格式

输出一行一个整数,表示满足条件的序列 \(a\) 的个数,对 \(998\ 244\ 353\) 取模的结果。

输入输出样例 #1

输入 #1

4 3 2
1 3 3
3 4 6

输出 #1

3

输入输出样例 #2

输入 #2

5 2 3
1 3 2
2 5 0
3 3 3

输出 #2

33

说明/提示

在一个样例中,合法的序列 \(a\) 有:\([3, 3, 7, 6]\),\([3, 7, 7, 6]\) 以及 \([7, 3, 7, 6]\)。

题解:

不难发现每一位的约束都是独立的,我们便可以对每一个二进制位单独做一次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] 清仓甩卖

题目背景

额外提供了 0.5 秒的时限。

题目描述

小 X 的糖果促销策略很成功,现在糖果店只剩下了 \(n\) 颗糖果,其中第 \(i\) (\(1 \le i \le n\)) 颗糖果的原价为 \(a_i\) 元。小 X 计划将它们全部重新定价,清仓甩卖。具体地,小 X 会将每颗糖果的清仓价格分别定为 \(1\) 元或 \(2\) 元。设第 \(i\) (\(1 \le i \le n\)) 颗糖果的清仓价格为 \(w_i \in \{1,2\}\) 元,则它的性价比被定义为原价与清仓价格的比值,即 \(\frac{a_i}{w_i}\)。

小 R 又带了 \(m\) 元钱买糖果。这一次,小 R 希望他购买到的糖果的原价总和最大,于是他采用了以下购买策略:将所有糖果按照性价比从大到小排序,然后依次考虑每一颗糖果。具体地,若小 R 在考虑第 \(i\) (\(1 \le i \le n\)) 颗糖果时剩余的钱至少为 \(w_i\) 元,则他会购买这颗糖果;否则他会跳过这颗糖果,继续考虑下一颗。特别地,若存在两颗糖果的性价比相同,则小 R 会先考虑原价较高的糖果;若存在两颗糖果的性价比与原价均相同,则小 R 会先考虑编号较小的糖果。

例如,若小 X 的糖果商店剩余 \(3\) 颗糖果,原价分别为 \(a_1=1\),\(a_2=3\),\(a_3=5\),而清仓价格分别为 \(w_1=w_2=1\),\(w_3=2\),则性价比分别为 \(1, 3, \frac{5}{2}\)。因此小 R 会先考虑第 \(2\) 颗糖果,然后考虑第 \(3\) 颗糖果,最后考虑第 \(1\) 颗糖果。

小 R 想知道,在小 X 的所有 \(2^n\) 种定价方案中,有多少种定价方案使得他按照上述购买策略能购买到的糖果的原价总和最大。你需要帮助小 R 求出满足要求的定价方案的数量。由于答案可能较大,你只需要求出答案对 \(998,244,353\) 取模后的结果。

输入格式

本题包含多组测试数据。

输入的第一行包含两个非负整数 \(c, t\),分别表示测试点编号与测试数据组数。\(c=0\) 表示该测试点为样例。

接下来依次输入每组测试数据,对于每组测试数据:

  • 第一行包含两个正整数 \(n, m\),分别表示糖果的数量与小 R 的钱数;
  • 第二行包含 \(n\) 个正整数 \(a_1, a_2, \ldots, a_n\),分别表示每颗糖果的原价。

输出格式

对于每组测试数据,输出一行一个非负整数,表示使得小 R 购买到的糖果的原价总和达到最大值的定价方案数对 \(998,244,353\) 取模后的结果。

输入输出样例 #1

输入 #1

0 1
3 2
1 3 5

输出 #1

6

说明/提示

【样例 1 解释】

该样例即为【题目描述】中的例子。共有以下 \(6\) 种定价方案使得小 R 购买到的糖果原价总和最大,分别为:

  • \(w_1 = w_2 = w_3 = 1\),小 R 购买到的糖果原价总和为 \(8\);
  • \(w_1 = w_3 = 1\),\(w_2 = 2\),小 R 购买到的糖果原价总和为 \(6\);
  • \(w_1 = 1\),\(w_2 = w_3 = 2\),小 R 购买到的糖果原价总和为 \(5\);
  • \(w_2 = w_3 = 1\),\(w_1 = 2\),小 R 购买到的糖果原价总和为 \(8\);
  • \(w_3 = 1\),\(w_1 = w_2 = 2\),小 R 购买到的糖果原价总和为 \(5\);
  • \(w_1 = w_2 = w_3 = 2\),小 R 购买到的糖果原价总和为 \(5\)。

注意:若 \(w_1 = w_2 = 1\),\(w_3 = 2\),则小 R 会依次购买第 \(2\) 颗和第 \(1\) 颗糖果,原价总和为 \(4\),但小 R 可以只购买第 \(3\) 颗糖果,原价总和为 \(5\)。因此该定价方案无法使小 R 购买到的糖果的原价总和达到最大值。

【样例 2】

见选手目录下的 sale/sale2.in 与 sale/sale2.ans。

该样例满足测试点 \(1 \sim 3\) 的约束条件。

【样例 3】

见选手目录下的 sale/sale3.in 与 sale/sale3.ans。

该样例满足测试点 \(4,5\) 的约束条件。

【样例 4】

见选手目录下的 sale/sale4.in 与 sale/sale4.ans。

该样例满足测试点 \(7 \sim 9\) 的约束条件。

【样例 5】

见选手目录下的 sale/sale5.in 与 sale/sale5.ans。

该样例满足测试点 \(10 \sim 12\) 的约束条件。

【样例 6】

见选手目录下的 sale/sale6.in 与 sale/sale6.ans。

该样例满足测试点 \(13\) 的约束条件。

【样例 7】

见选手目录下的 sale/sale7.in 与 sale/sale7.ans。

该样例满足测试点 \(14,15\) 的约束条件。

【样例 8】

见选手目录下的 sale/sale8.in 与 sale/sale8.ans。

该样例满足测试点 \(17\) 的约束条件。

【样例 9】

见选手目录下的 sale/sale9.in 与 sale/sale9.ans。

该样例满足测试点 \(19,20\) 的约束条件。

【样例 10】

见选手目录下的 sale/sale10.in 与 sale/sale10.ans。

该样例满足测试点 \(21 \sim 23\) 的约束条件。

【样例 11】

见选手目录下的 sale/sale11.in 与 sale/sale11.ans。

该样例满足测试点 \(24,25\) 的约束条件。

【数据范围】

设 \(N\) 为单个测试点内所有测试数据的 \(n\) 的和。对于所有测试数据,均有:

  • \(1 \le t \le 5 \times 10^4\);
  • \(1 \le n \le 5,000\),\(N \le 5 \times 10^4\),\(1 \le m \le 2n - 1\);
  • 对于所有 \(1 \le i \le n\),均有 \(1 \le a_i \le 10^9\)。

::cute-table{tuack}

测试点编号 \(n \le\) \(N \le\) \(m\) 特殊性质
\(1\sim 3\) \(5\) \(5{,}000\) \(\le 2n - 1\) 无
\(4,5\) \(10\) ^ ^ ^
\(6\) \(40\) ^ ^ ^
\(7\sim 9\) \(300\) ^ \(=2\) ^
\(10\sim 12\) ^ ^ \(\le 2n - 1\) B
\(13\) ^ ^ ^ 无
\(14,15\) \(10^3\) \(10^4\) \(=2\) ^
\(16\) ^ ^ \(=2n - 1\) ^
\(17\) ^ ^ \(=2n - 2\) ^
\(18\) ^ ^ \(\le 2n - 1\) A
\(19,20\) ^ ^ ^ B
\(21\sim 23\) ^ ^ ^ 无
\(24,25\) \(5{,}000\) \(5 \times 10^4\) ^ ^

特殊性质 A:\(a_1 = a_2 = \cdots = a_n\)。

特殊性质 B:对于所有 \(1 \le i \le n\),均有 \(a_i > 5 \times 10^8\)。

题解:

分析一下题目,可以得到若这个策略无法得到最优,则一定存在一个 \(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

题目描述

Farmer John 有 \(N\)(\(1≤N≤3000\))头各种大小的奶牛。他原本为每头奶牛量身定制了牛棚,但现在某些奶牛长大了,使得原先的牛棚大小不够用。具体地说,FJ 原来建造了 \(N\) 个牛棚的大小为 \(t_1,t_2,…,t_N\),现在奶牛的大小为 \(s_1,s_2,…,s_N\)(\(1≤s_i,t_i≤10^9\))。

每天晚上,奶牛们都会按照某种方式寻找睡觉的牛棚。奶牛 \(i\) 可以睡在牛棚 \(j\) 中当且仅当她的大小可以进入牛棚(\(s_i≤t_j\))。每个牛棚中至多可以睡一头奶牛。

我们称奶牛与牛棚的一个匹配是极大的,当且仅当每头奶牛可以进入分配给她的牛棚,且对于每头未被分配牛棚的奶牛无法进入任何未分配的空牛棚。

计算极大的匹配的数量模 \(10^9+7\) 的结果。

输入格式

输入的第一行包含 \(N\)。

第二行包含 \(N\) 个空格分隔的整数 \(s_1,s_2,…,s_N\)。

第三行包含 \(N\) 个空格分隔的整数 \(t_1,t_2,…,t_N\)。

输出格式

输出极大的匹配的数量模 \(10^9+7\) 的结果。

输入输出样例 #1

输入 #1

4
1 2 3 4
1 2 2 3

输出 #1

9

说明/提示

以下是全部九种极大的匹配。有序对 \((i,j)\) 表示奶牛 \(i\) 被分配到了牛棚 \(j\)。

(1, 1), (2, 2), (3, 4)
(1, 1), (2, 3), (3, 4)
(1, 1), (2, 4)
(1, 2), (2, 3), (3, 4)
(1, 2), (2, 4)
(1, 3), (2, 2), (3, 4)
(1, 3), (2, 4)
(1, 4), (2, 2)
(1, 4), (2, 3)
  • 测试点 2-3 中,\(N≤8\)。
  • 测试点 4-12 中,\(N≤50\)。
  • 测试点 13-20 没有额外限制。

供题:Nick Wu

题解:

我们先将 \(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; 
}
posted @ 2026-01-22 13:40  EzSun599  阅读(10)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3