组合问题记录
核心:
- 组合问题的常见分类:
现在我们假设我们要对一个组合对象 \(U\) 进行考虑并分析一些关于组合的问题。
-
判定: 判断 \(U\) 中是否有满足条件 \(p\) 的集合或元素。这是组合问题中最基础的问题。
-
构造: 找到 \(U\) 中一个满足条件 \(p\) 的集合或元素。此类问题基于判定问题,但是灵活性更高,需要一定的思维(有时候可能会很难)。
-
计数: 统计 \(U\) 中满足条件 \(p\) 的集合或元素数量。
-
最优化: 给 \(U\) 中每一个集合或元素定义一个价值,问 \(U\) 中满足条件 \(p\) 的集合或元素的价值最大 / 最小的价值。
- 组合问题的常见技巧:
-
调整法: 通过证明一个情况可以通过调整到另一个情况使得答案不劣,将问题转化为特殊情况求解。本技巧常用于最优化问题中。
-
局部观察法: 抓住问题判定的本质,从局部入手观察问题的情况,常常和调整法结合。
-
构造双射法: 通过一个双射对应到另一个问题上,简化或明晰问题。
- 计数中的映射问题:
计数中常常会遇到一类操作问题,即一个初始状态 \(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\),观察一下容易发现有可能它们完全相反。
接下来开始计数,先算出总方案数:显然行和列互不相关,而且我们可以浪费掉偶数次操作,于是可得:
重复的方案数也是一样的算就行。
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\),并进行裂项后整理。
提前计算 \(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;
}
/*
*/