组合问题记录

this and this

核心:

  • 组合问题的常见分类:

现在我们假设我们要对一个组合对象 \(U\) 进行考虑并分析一些关于组合的问题。

  1. 判定: 判断 \(U\) 中是否有满足条件 \(p\) 的集合或元素。这是组合问题中最基础的问题。

  2. 构造: 找到 \(U\) 中一个满足条件 \(p\) 的集合或元素。此类问题基于判定问题,但是灵活性更高,需要一定的思维(有时候可能会很难)。

  3. 计数: 统计 \(U\) 中满足条件 \(p\) 的集合或元素数量。

  4. 最优化:\(U\) 中每一个集合或元素定义一个价值,问 \(U\) 中满足条件 \(p\) 的集合或元素的价值最大 / 最小的价值。


  • 组合问题的常见技巧:
  1. 调整法: 通过证明一个情况可以通过调整到另一个情况使得答案不劣,将问题转化为特殊情况求解。本技巧常用于最优化问题中。

  2. 局部观察法: 抓住问题判定的本质,从局部入手观察问题的情况,常常和调整法结合。

  3. 构造双射法: 通过一个双射对应到另一个问题上,简化或明晰问题。


  • 计数中的映射问题:

计数中常常会遇到一类操作问题,即一个初始状态 \(U_s\) 可以通过一系列的操作映射 \(f\) 到最终状态 \(U_t\)。问法常常是知二求一:即给定 \(U_s, U_t, f\) 三者中的两者,问剩下一个不同的个数。

解决这类问题时常常会有一个问题:重复。这个时候可能要用到容斥之类的计数技巧。还可能遇到 \(f\) 难以进行判断的问题,此时可以寻找中介以及判断的充要条件。

练习:

CF442C Artem and Array

注意到一次操作只与相邻的三个元素相关,于是对这个 \(\rm Pattern\) 进行观察。可以关注到一种特殊情况:\(a_{i - 1} \ge a_i \le a_{i + 1}\),即一个下凸的部分,猜想不论如何可以先删 \(a_i\),否则一定可以调整到该情况使答案不劣。于是最后删完之后会变成一个"倒 \(V\)"的形式。考虑最后部分的答案如何计算,显然最大的两个已经去不到了,于是就是除了两边的数剩下的 \(n - 4\) 个数了。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5e5 + 10;

int n, a[N], stk[N], top, ans;

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n; i++){
		while(top >= 1 && stk[top] <= stk[top - 1] && a[i] >= stk[top]) ans += min(a[i], stk[top - 1]), top--;
		stk[++top] = a[i];
	}
	sort(stk + 1, stk + top + 1);
	for(int i = top - 2; i >= 0; i--) ans += stk[i];
	cout << ans;
	
	return 0;
}

CF1239F Swiper, no swiping!

毒瘤分讨题。

  • Case 1:

假如有 \(0\) 度点,留着即可。

  • Case 2:

假如有两个点 \(1\) 度点相连,即 \(1 - 1\),留着即可。

  • Case 3:

假如有几个 \(2\) 度点组成一个 不可约环,即是一个简单环且没有包含其他的环,这显然是合法的。如何寻找呢?注意到这是一张无向图,于是有一个套路:考虑原图的一个 \(\rm DFS\) 生成树,容易发现该图中只存在树边和返祖边。于是找到一条两端深度差最小的一条返祖边,然后暴力跳即可。

  • Case 4:

假如上面的情况都不满足而且 \(1\) 度点个数 \(\ge 2\),我们考虑一个 \(1 - 2 - ... - 2 - 1\) 这种情况。显然这是符合条件的,这类情况直接从任意 \(1\) 开始 \(\rm BFS\) 并记录路径上的点即可。

  • Case 5:

假如上面一个都不满足,显然存在至少一个 \(1\) 度点,而且剩下的 \(2\) 度点会组成一个森林。找到两颗不同的树,直接将叶子和对应的 \(\rm LCA\) 都选上即可。用反证法可以证明这种情况一定存在。那为啥会无解呢?因为可能构造出来的东西是整张图。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5e5 + 10, INF = 1e18;

struct edge{
	int v, next;
}edges[N << 1];
int head[N], idx = 1;
int n, m, deg[N], ans[N], tot, dep[N], cirsiz = INF, fa[N], vis[N], leafvec[N], lsiz, tag[N], tag2[N];

void add_edge(int u, int v){
	edges[++idx] = {v, head[u]};
	head[u] = idx;
}
void clrall(){
	tot = lsiz = 0; idx = 1; cirsiz = INF;
	for(int i = 1; i <= n; i++) deg[i] = head[i] = fa[i] = dep[i] = vis[i] = tag[i] = tag2[i] = 0;
}
void clr(){
	for(int i = 1; i <= n; i++) dep[i] = vis[i] = fa[i] = 0;
}

void getout(){
	clr();
	if(tot == n) cout << "No" << "\n";
	else{
		cout << "Yes" << "\n" << n - tot << "\n"; 
		for(int i = 1; i <= tot; i++) vis[ans[i]] = 1;
		for(int i = 1; i <= n; i++) if(!vis[i]) cout << i << " ";
		cout << "\n";
	}
}
bool Case1(){
	for(int i = 1; i <= n; i++) if(!deg[i]){ans[++tot] = i, getout(); return true;} 
	return false;
} 

bool Case2(){
	for(int i = 1; i <= n; i++){
		if(deg[i] == 1){
			for(int j = head[i]; j; j = edges[j].next){ 
				int v = edges[j].v; if(deg[v] == 1){ans[++tot] = i, ans[++tot] = v, getout(); return true;}
			}
		}
	}
	return false;
}

void dfs1(int u, int faid){
	vis[u] = 1;
	for(int i = head[u]; i; i = edges[i].next){
		int v = edges[i].v; if(i == (faid ^ 1)) continue;
//		cout << u << " " << v << " " << i << " " << faid << "\n";
//		cout << u << " " << v << "\n";
		if(deg[v] == 2){
			if(!vis[v]) dep[v] = dep[u] + 1, fa[v] = u, dfs1(v, i);
			else if(dep[u] - dep[v] >= 1) cirsiz = min(cirsiz, dep[u] - dep[v]);//, cout << u << " " << v << " " << dep[u] << " " << dep[v] << "\n";
		} 
	}
}

bool _dfs1(int u, int faid){
	for(int i = head[u]; i; i = edges[i].next){
		int v = edges[i].v; if(i == (faid ^ 1)) continue;
		if(deg[v] == 2){
			if(dep[v] == dep[u] + 1 && _dfs1(v, i)) return true;
			else if(cirsiz == dep[u] - dep[v]){
				ans[++tot] = v; int p = u;
				while(p != v){
					ans[++tot] = p; p = fa[p];
				}
				return true;
			} 
		}
	}
	return false;
}

bool Case3(){
	clr();
	for(int i = 1; i <= n; i++) if(deg[i] == 2 && (!vis[i])){
		dfs1(1, 0); if(cirsiz != INF){assert(_dfs1(1, 0)), getout(); return true;}
	}
//	cout << 3 << "\n";
	return false;
}

bool Case4(){
	clr();
	int cnt1 = 0, p1 = 0; for(int i = 1; i <= n; i++) if(deg[i] == 1) cnt1++, p1 = i;
	if(cnt1 == 1) return false; for(int i = 1; i <= n; i++) dep[i] = INF; 
	dep[p1] = 0; queue<int> Q; Q.push(p1);
	while(!Q.empty()){
		int u = Q.front(); Q.pop();
		for(int i = head[u]; i; i = edges[i].next){
			int v = edges[i].v;
			if(dep[v] > dep[u] + 1){
				dep[v] = dep[u] + 1, fa[v] = u;
				if(deg[v] == 2) Q.push(v);
			} 
		}
	}
	for(int i = 1; i <= n; i++){
		if(deg[i] == 1 && p1 != i && dep[i] != INF){
			int p = i; while(p != p1) ans[++tot] = p, p = fa[p];  
			ans[++tot] = p1; getout(); return true;
		}
	}
	return false;
}

void dfs2(int u, int father){
	bool fl = 1; vis[u] = 1; fa[u] = father;
	for(int i = head[u]; i; i = edges[i].next){
		int v = edges[i].v; if(v == father || deg[v] == 1) continue;
		dfs2(v, u);
	}
}

void dfs3(int u, int father){
	bool fl = 1; vis[u] = 1; fa[u] = father;
	if(father && tag2[u]){
		leafvec[++lsiz] = u;
		return;
	}
	for(int i = head[u]; i; i = edges[i].next){
		int v = edges[i].v; if(v == father || deg[v] == 1) continue;
		dfs3(v, u);
	}
}

void addpath(int x, int y){
	for(int i = 1; i <= n; i++) tag[i] = 0;
	int p = x; //cout << "6: " << x << " " << y << "\n";
	do{
		tag[x] = 1;
		x = fa[x];
	}while(x);
	x = p;
//	cout << p << " " << y << "\n";
	while(!tag[y]){
//		cout << y << " " << fa[y] << "\n";
		ans[++tot] = y;
		y = fa[y];
	}
	while(x != y){
		ans[++tot] = x; x = fa[x];
	}
	ans[++tot] = y;
}

bool Case5(){
	int p1 = 0, cnt = 0; clr();
	for(int i = 1; i <= n; i++) if(deg[i] == 1) p1 = i;
	ans[++tot] = p1;
	for(int i = head[p1]; i; i = edges[i].next){
		int v = edges[i].v; tag2[v] = 1;
	}
	for(int i = head[p1]; i; i = edges[i].next){
		int v = edges[i].v; if(!vis[v]){
			dfs2(v, 0); dfs3(v, 0); addpath(leafvec[1], v); cnt++; lsiz = 0;
//			cout << v << " " << leafvec[1] << "\n";
			if(cnt == 2){getout(); return true;}
		}
	}
	return false;
}

void solve(){
	clrall(); cin >> n >> m; 
	for(int i = 1; i <= m; i++){
		int x, y; cin >> x >> y;
		add_edge(x, y); add_edge(y, x);
		deg[x]++; deg[y]++;
	}
	for(int i = 1; i <= n; i++) deg[i] %= 3;
	if(Case1()) return; // deg = 0
	if(Case2()) return; // deg = 1 -> 1 -> back
	if(Case3()) return; // deg = 2 -> 2 -> 2 -> back
	if(Case4()) return; // deg = 1 -> 2 -> 2 -> 1
	if(Case5()) return; // deg = 2-leaf -> 1 -> 2-leaf
}

signed main(){
//	freopen("lsy.in", "r", stdin);
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int T; cin >> T; while(T--) solve();

	return 0;
}
/*
don't UKE love you codeforces and luogu
*/

ARC121D 1 or 2

首先可以把没有配对的数转化为与 \(0\) 进行配对。那么如何最优の进行配对呢?直觉告诉我们应该是最小配最大,次大配次小...如何证明呢?考虑使用调整法。假设我们有四个数 \(a \le b \le c \le d\),在 \(a - c, b - d\) 的状态下最大值和最小值分别是 \(mx, mn\)。假如 \(mn \le a + d, b + c \le mx\),不会产生影响,于是考虑以下两种情况:

  • 调整后 \(a + d\) 成为最大值,那么显然 \(b + d \ge a + d\),显然使得答案更优,\(b + c\) 成为最大值的情况同理。

  • 调整后 \(a + d\) 成为最小值,那么显然 \(a + c \le a + d\),显然使得答案更优,\(b + c\) 成为最小值的情况同理。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e4 + 10, INF = 1e18;

int n, a[N], ans = INF;

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int len = 0; len <= n; len++){
		int minn = INF, maxn = -INF;
		sort(a + 1, a + len + n + 1, greater<int>());
		if((len + n) & 1) continue;
		for(int i = 1; i <= n + len; i++){
			maxn = max(maxn, a[i] + a[n + len - i + 1]);
			minn = min(minn, a[i] + a[n + len - i + 1]);
		}
		ans = min(ans, maxn - minn);
	}
	cout << ans;
	
	return 0;
}

P5857 「SWTR-3」Matrix

经典的映射计数题。

直接计数是困难的,我们考虑设计一个中介量来简化计数,即构造双射转化问题。注意到操作的对象实际上是行和列,于是我们考虑将原操作序列映射到两个描述行和列状态的序列。具体的,我们将操作序列映射到两个序列 \(h_1, h_2, ..., h_n\), \(w_1, w_2, ..., w_m\) 分别描述每个行和列是否操作过。

接下来我们检查反射,即观察是否构成一个双射。但是很不幸的,我们会发现可能会有重复的情况。具体如何呢?考虑两组不同的操作序列 \((h, w), (h', w')\) 它们满足 \(\forall (x, y) h_x \oplus w_y = h'_x \oplus w'_y\),观察一下容易发现有可能它们完全相反。

接下来开始计数,先算出总方案数:显然行和列互不相关,而且我们可以浪费掉偶数次操作,于是可得:

\[\left( \sum_{i = 1, i \equiv k\pmod 2}^n (^n_i)\right)\left( \sum_{j = 1, j \equiv k\pmod 2}^m (^m_j)\right) \]

重复的方案数也是一样的算就行。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 2e5 + 10, mod = 998244353;

int n, m, k, jc[N], jcinv[N], inv2;

int qpow(int x, int y){
	int ret = 1;
	for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
	return ret;
}

int C(int x, int y){return jc[x] * jcinv[y] % mod * jcinv[x - y] % mod;}

void solve(){
	cin >> n >> m >> k;
	int ans1 = 0, ans2 = 0, ans;
	for(int i = 0; i <= min(n, k); i++) if((k - i) % 2 == 0) ans1 = (ans1 + C(n, i)) % mod;
	for(int i = 0; i <= min(m, k); i++) if((k - i) % 2 == 0) ans2 = (ans2 + C(m, i)) % mod;
	ans = ans1 * ans2 % mod; ans1 = ans2 = 0;
	if(n % 2 == 0 && m % 2 == 0){
		for(int i = max(n - k, 0ll); i <= min(n, k); i++) if((k - i) % 2 == 0) ans1 = (ans1 + C(n, i)) % mod;
		for(int i = max(m - k, 0ll); i <= min(m, k); i++) if((k - i) % 2 == 0) ans2 = (ans2 + C(m, i)) % mod;
		ans = (ans - (ans1 * ans2 % mod * inv2 % mod) + mod) % mod;
	}
	cout << ans << "\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	jc[0] = jcinv[0] = 1; inv2 = qpow(2, mod - 2);
	for(int i = 1; i < N; i++) jc[i] = jc[i - 1] * i % mod, jcinv[i] = qpow(jc[i], mod - 2);
	int T; cin >> T; while(T--) solve();

	return 0;
}

ARC061F 3人でカードゲーム

首先考虑枚举游戏结束的轮数 \(r\),容易发现此时最后一个一定是 \(1\) 而且恰有 \(n1\)(\(n1\) 为原题中的 \(n\), \(n2\)\(m\), \(n3\)\(k\)) 个 \(a\)。于是可以计算出非 \(1\) 牌的个数 \(k\),显然一个长为 \(n1 + n2 + n3\) 取牌序列一定和构造出来的牌堆构成双射,此时可以直接进行计数了。

  • 首先我们只知道前 \(n1 + k\) 个元素的情况,后面随便排列都可以,方案数为 \(3^{n2 + n3 - k}\)

  • 接着前 \(n1 + k\) 个元素中除了最后一位的 \(1\) 之外的 \(n1 - 1\) 个一都是自由的,于是可以前面 \(n1 + k - 1\)\(1\) 和非 \(1\) 可以乱排,方案数为 \(C_{n1 + k - 1}^{n1 - 1}\)

  • 最后我们需要确定 \(k\) 个非 \(1\) 元素的情况,显然可以枚举 \(2\) 的个数 \(i\),方案数为 \(\sum_{i = k - n3}^{n2} C_k^i\)

复杂度为 \(O(n^3)\),复杂度瓶颈在于后面组合数求和的部分,注意到这个形式非常像朱世杰恒等式的形式,于是我们考虑使用 \(\rm Pascal\) 公式裂项求递推式。具体的,我们令 \(S(k) = \sum_{i = k - n3}^{n2} C_k^i\),并进行裂项后整理。

\[\begin{aligned} S(k) &= \sum_{i = k - n3}^{n2} C_k^i \\ &= \sum_{i = k - n3}^{n2} C_k^{i - 1} + C_{k - 1}^{i - 1} \\ &= \sum_{i = k - n3}^{n2} C_{k - 1}^i + \sum_{i = k - n3 - 1}^{n2 - 1}C_{k - 1}^i \\ &= 2S(k - 1) - C_{k - 1}^{k - n3 - 1} - C_{k - 1}^{n2} \end{aligned} \]

提前计算 \(S(n)\) 即可做到 \(O(n^2)\)

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 6e5 + 10, mod = 1e9 + 7;

int n1, n2, n3, jc[2 * N], jcinv[2 * N], S[N], thr = 1, ans;

int qpow(int x, int y){
	int ret = 1;
	for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod; 
	return ret;
}
int C(int x, int y){
	if(x < 0 || y < 0 || x < y) return 0;
	return jc[x] * jcinv[x - y] % mod * jcinv[y] % mod;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n1 >> n2 >> n3; jc[0] = jcinv[0] = S[0] = 1;
	for(int i = 1; i < 2 * N; i++) jc[i] = jc[i - 1] * i % mod, jcinv[i] = qpow(jc[i], mod - 2);
	for(int k = 1; k <= n2 + n3; k++) S[k] = (2 * S[k - 1] % mod - C(k - 1, k - n3 - 1) - C(k - 1, n2)) % mod;
	for(int k = n2 + n3; k >= 0; k--) ans = (ans + thr * C(n1 + k - 1, k) % mod * S[k] % mod) % mod, thr = thr * 3 % mod;
	cout << (ans + mod) % mod;

	return 0;
}

ARC114E Paper Cutting 2

首先对问题进行一个转化,可以发现操作数 = 纸片缩小的次数 + 1,而且注意到期望的线形性,我们可以分别考虑每条线作为缩小后的边界的概率加起来就是答案。我们称两个黑格子夹着的区域是“死区”,考虑“死区”上下左右的边。考虑在左侧的一条边作为边界时的充要条件:

  • 经过死区的边没有被切开。

  • 该边右侧到死区的边没有被切开。

换句话说,该边是这些边种第一个被切开的,假设有 \(x\) 这样的边,则概率为 \(\frac{1}{x}\)。其他的三种情况同理。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int mod = 998244353;

int qpow(int x, int y){
	int ret = 1;
	for(; y; y >>= 1, x = x *x % mod) if(y & 1) ret = ret * x % mod;
	return ret; 	
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int h, w, x1, y1, x2, y2, ans = 0;
	cin >> h >> w >> x1 >> y1 >> x2 >> y2;
	int xn = min(x1, x2), yn = min(y1, y2), xm = max(x1, x2), ym = max(y1, y2), mid = xm - xn + ym - yn;
	for(int i = 1; i <= xn - 1; i++){
		int cnt = (mid + xn - i - 1) % mod; 
		ans = (ans + qpow(cnt + 1, mod - 2)) % mod;
	}
	for(int i = xm; i <= h - 1; i++){
		int cnt = (mid + i - xm) % mod;
		ans = (ans + qpow(cnt + 1, mod - 2)) % mod;
	}
	for(int i = 1; i <= yn - 1; i++){
		int cnt = (mid + yn - i - 1) % mod;
		ans = (ans + qpow(cnt + 1, mod - 2)) % mod;
	}
	for(int i = ym; i <= w - 1; i++){
		int cnt = (mid + i - ym) % mod; 
		ans = (ans + qpow(cnt + 1, mod - 2)) % mod;
	}
	cout << (ans + 1) % mod << "\n";
	
	return 0;
} 
/*

*/

posted @ 2024-08-02 10:26  Little_corn  阅读(15)  评论(0编辑  收藏  举报