快来踩爆这个蒟蒻吧|

Little_corn

园龄:1年1个月粉丝:11关注:17

2024-08-02 10:26阅读: 24评论: 0推荐: 0

组合问题记录

this and this

核心:

  • 组合问题的常见分类:

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

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

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

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

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


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

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

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


  • 计数中的映射问题:

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

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

练习:

CF442C Artem and Array

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

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 度点相连,即 11,留着即可。

  • Case 3:

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

  • Case 4:

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

  • Case 5:

假如上面一个都不满足,显然存在至少一个 1 度点,而且剩下的 2 度点会组成一个森林。找到两颗不同的树,直接将叶子和对应的 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 进行配对。那么如何最优の进行配对呢?直觉告诉我们应该是最小配最大,次大配次小...如何证明呢?考虑使用调整法。假设我们有四个数 abcd,在 ac,bd 的状态下最大值和最小值分别是 mx,mn。假如 mna+d,b+cmx,不会产生影响,于是考虑以下两种情况:

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

  • 调整后 a+d 成为最小值,那么显然 a+ca+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

经典的映射计数题。

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

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

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

(i=1,ik(mod2)n(in))(j=1,jk(mod2)m(jm))

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

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, n2m, n3k) 个 a。于是可以计算出非 1 牌的个数 k,显然一个长为 n1+n2+n3 取牌序列一定和构造出来的牌堆构成双射,此时可以直接进行计数了。

  • 首先我们只知道前 n1+k 个元素的情况,后面随便排列都可以,方案数为 3n2+n3k

  • 接着前 n1+k 个元素中除了最后一位的 1 之外的 n11 个一都是自由的,于是可以前面 n1+k11 和非 1 可以乱排,方案数为 Cn1+k1n11

  • 最后我们需要确定 k 个非 1 元素的情况,显然可以枚举 2 的个数 i,方案数为 i=kn3n2Cki

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

S(k)=i=kn3n2Cki=i=kn3n2Cki1+Ck1i1=i=kn3n2Ck1i+i=kn31n21Ck1i=2S(k1)Ck1kn31Ck1n2

提前计算 S(n) 即可做到 O(n2)

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 这样的边,则概率为 1x。其他的三种情况同理。

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 中国大陆许可协议进行许可。

posted @   Little_corn  阅读(24)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起