组合问题记录
核心:
- 组合问题的常见分类:
现在我们假设我们要对一个组合对象
-
判定: 判断
中是否有满足条件 的集合或元素。这是组合问题中最基础的问题。 -
构造: 找到
中一个满足条件 的集合或元素。此类问题基于判定问题,但是灵活性更高,需要一定的思维(有时候可能会很难)。 -
计数: 统计
中满足条件 的集合或元素数量。 -
最优化: 给
中每一个集合或元素定义一个价值,问 中满足条件 的集合或元素的价值最大 / 最小的价值。
- 组合问题的常见技巧:
-
调整法: 通过证明一个情况可以通过调整到另一个情况使得答案不劣,将问题转化为特殊情况求解。本技巧常用于最优化问题中。
-
局部观察法: 抓住问题判定的本质,从局部入手观察问题的情况,常常和调整法结合。
-
构造双射法: 通过一个双射对应到另一个问题上,简化或明晰问题。
- 计数中的映射问题:
计数中常常会遇到一类操作问题,即一个初始状态
解决这类问题时常常会有一个问题:重复。这个时候可能要用到容斥之类的计数技巧。还可能遇到
练习:
CF442C Artem and Array
注意到一次操作只与相邻的三个元素相关,于是对这个
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:
假如有
- Case 2:
假如有两个点
- Case 3:
假如有几个
- Case 4:
假如上面的情况都不满足而且
- Case 5:
假如上面一个都不满足,显然存在至少一个
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
首先可以把没有配对的数转化为与
-
调整后
成为最大值,那么显然 ,显然使得答案更优, 成为最大值的情况同理。 -
调整后
成为最小值,那么显然 ,显然使得答案更优, 成为最小值的情况同理。
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
经典的映射计数题。
直接计数是困难的,我们考虑设计一个中介量来简化计数,即构造双射转化问题。注意到操作的对象实际上是行和列,于是我们考虑将原操作序列映射到两个描述行和列状态的序列。具体的,我们将操作序列映射到两个序列
接下来我们检查反射,即观察是否构成一个双射。但是很不幸的,我们会发现可能会有重复的情况。具体如何呢?考虑两组不同的操作序列
接下来开始计数,先算出总方案数:显然行和列互不相关,而且我们可以浪费掉偶数次操作,于是可得:
重复的方案数也是一样的算就行。
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人でカードゲーム
首先考虑枚举游戏结束的轮数
-
首先我们只知道前
个元素的情况,后面随便排列都可以,方案数为 。 -
接着前
个元素中除了最后一位的 之外的 个一都是自由的,于是可以前面 个 和非 可以乱排,方案数为 。 -
最后我们需要确定
个非 元素的情况,显然可以枚举 的个数 ,方案数为 。
复杂度为
提前计算
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,而且注意到期望的线形性,我们可以分别考虑每条线作为缩小后的边界的概率加起来就是答案。我们称两个黑格子夹着的区域是“死区”,考虑“死区”上下左右的边。考虑在左侧的一条边作为边界时的充要条件:
-
经过死区的边没有被切开。
-
该边右侧到死区的边没有被切开。
换句话说,该边是这些边种第一个被切开的,假设有
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; } /* */
本文作者:Little_corn
本文链接:https://www.cnblogs.com/little-corn/p/18337235
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步