组合计数杂题
ABC276G
【题意】
给定 \(n, m\),求出满足以下条件的数列的个数:
- 数列长度为 \(n\)。
- 数列的每一个数都在 \([0,m]\) 之间。
- 数列的相邻两个数模 \(3\) 不同余。
【思想】
组合计数问题,需要利用一些一一映射,将要算的东西改写成能算的东西,比如 \(\binom{n}{k}, \begin{Bmatrix}n\\k\end{Bmatrix},\begin{bmatrix}n\\k\end{bmatrix}\)(选择,子集,轮换)。
【分析】
“模 \(3\) 不同余”这个条件首先一看就不是很好直接算,我们考虑构造差分数组 \(b\),满足对于 \(i \in [2,n]\) 有 \(3 \nmid b_i\)。
然后显然考虑拆分成 \(b_i / 3\) 和 \(b_i \% 3\)。记 \(x_i = b_i \% 3, y_i = b_i / 3\),则有对于 \(i = 1\),\(x_i \in \{0,1,2\}\);对于 \(i > 1\),\(x_i \in \{1,2\}\)。
我们先考虑整块,也就是钦定了一套 \(x_i\) 之后,\(y_i\) 的个数怎么算?也就是计算使得 \(\sum \limits_{i = 1} ^ n y_i \le \lceil \cfrac{m - \sum x_i}{3} \rceil\) 的方案数。可以发现这个东西只和 \(\sum x_i\) 有关,那么如果计算上述式子的时间为 \(T\),那么我们可以枚举 \(x_i(i \le [2,n])\) 存在多少个 \(1\),从而得到存在多少个 \(2\)。相同的方案数可以二项式求出,那么可以 \(O(n) \times T\)。
那么问题转化为:计算使得 \(\sum \limits_{i = 1} ^ n y_i \le t\) 的方案数。我们知道 \(\sum \limits_{i = 1} ^ n y_i = t\) 的方案数是插板法算的,那么一种方法是预处理出所有 \(t\) 的答案,那么每次询问可以 \(O(1)\) 得到解。总时间复杂度 \(O(n \log t_{\max} + n) = O(n \log t_{\max})\),其中 \(t_{\max} \sim m\)。
还有一种方法:
引理:\(y_i\) 都是自然数,使得 \(\sum \limits_{i = 1}^n y_i \le t\) 的方案数为 \(\binom{n + t}{n}\)。
证明:考虑插板法的过程。对于 \(\sum \limits_{i = 1}^n y_i = t\) 的方案数,也就相当于 \(t + n\) 个球,插 \(n - 1\) 个板,分成 \(n\) 个区域。
那么我们可以增加一个区域,这个区域表示把这些球扔掉,其他 \(n\) 个区域表示分成的 \(n\) 个区域。那么也就是把 \(\le t\) 个球分成 \(n\) 个区域。因此是一共 \(t + n + 1\) 个球,插 \(n\) 个板,分成 \(n + 1\) 个区域,答案为 \(\binom{n + t}{n}\)。
2022.11.10 NOIP Monisai Round #1 A
【题意】
有 \(n\) 个景点和 \(m\) 个游客。这些景点横向一字排开。每个乘客都会选择一个景点到达并选择向左或者右行走。每一个景点都有一个小礼物。若一个人走到一个景点,满足这个人没有收到过礼物并且这个景点的小礼物没有被送出,那么这个景点发一份礼物给这个人。求每个人都拿到一个礼物的方案数。
【思想】
巧妙地增补方案构造一一对应,利用美妙的对称性寻找性质。
人为增设对称性!
【分析】
考虑增加一个景点,使得这些景点组成一个长度为 \(n + 1\) 的环。从环上任意一点可以向左或向右走。考虑这个模型和原题的差异:
- 若 \(n+1\) 景点的礼物被送出去,那么就失败。
- 这个模型中,可以从 \(n+1\) 景点出发。
- 这个模型中,增加了一些横跨过 \(n+1\) 的路线。
这三个差异其实是统一的。也就是当 \(n+1\) 景点的礼物没有被送出去时,才是成功的,并且方案与题目中一一对应。考虑选出这样的方案的概率:
由于每个点都对称,所以概率为 \(\cfrac{\C_{m}^n}{\C_{m}^{n+1}} = \cfrac{n+1}{n-m+1}\)。
所有方案的个数为 \((2(n+1))^m\)。
答案为所有方案个数乘以概率。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int mod = 998244353;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int qpow(int x,int k){
int ans=1;
while(k){
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
freopen("breeze.in","r",stdin);
freopen("breeze.out","w",stdout);
//think twice,code once.
//think once,debug forever.
int n, m; cin >> n >> m;
int ans = 1;
f(i, 1, m) {
ans = (ans * (2 * (n+1) % mod)) % mod;
}
ans = (ans * (n + 1 - m)) % mod;
// cout << ans << endl;
ans = (ans * qpow(n + 1, mod - 2)) % mod;
cout << ans << endl;
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
CF1761D
【题意】
求 \((a,b)\) 的个数,其中 \(0 \le a,b < 2^n\),并且 \(a + b\) 在二进制下加法的进位个数为 \(k\)。
\(k < n \le 10^6\)
【分析】
考虑上一位如果进位了,那么这一位有三种选法也进位,一种选法不会进位。没进位的话,三种选法不进位,一种选法会进位。
然后 DP?不好意思,不太好转移。矩阵快速幂试过了不行。
这么优美的式子怎么不想想组合方法。我们从这个方向往下走。
考虑 \(d_i\) 表示该位有没有进位(假设二进制下分别为第 \(1 \sim n\) 位),特别地 \(d_0 = 0\),那么这总共 \(n + 1\) 位数,如果存在 \(i\) 个连续段,那么方案数就是 \(3^{n - i}\)。(连续段的开头只有一种选择方法,后面每一位都有三种)
于是变成了数 \(n + 1 - k\) 个 \(0\),\(k\) 个 \(1\),组成 \(i\) 个连续段,一共有几种情况?(数连续段模型)考虑第一段一定为 \(0\),那么 \(0\) 和 \(1\) 的段数是确定的。然后考虑 \(p\) 个 \(0\) 分成 \(x\) 段的个数(插板法)乘以 \(q\) 个 \(1\) 分成 \(y\) 段的个数即可。
注意 \(0\) 个数分成 \(0\) 段的方案数是 \(1\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int jc[1000010], ny[1000010], pow3[1000010];
const int mod = 1e9 + 7;
int qpow(int x,int k){
int ans=1;
while(k){
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
int c(int n, int m) {
if(n == -1 && m == -1) return 1;
else if(m > n) return 0;
else if(m < 0 || n < 0) return 0;
else return jc[n] * ny[m] % mod * ny[n - m] % mod;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
jc[0]=ny[0]=pow3[0]=1;
int n, k; cin >> n >> k;
f(i,1,n){
jc[i]=jc[i-1]*i%mod;
ny[i]=qpow(jc[i],mod-2);
pow3[i]=pow3[i-1]*3%mod;
}
int ans = 0;
f(i,1 ,n+1 ) {
int t = 1;
//分的段数
int p = i /2 ,q = i - p;
t *= c(n + 1 - k-1,q-1);
t *= c(k - 1, p - 1);
t %= mod;
t *= pow3[n + 1 - i];
t %= mod;
ans += t;
ans %= mod;
}
cout << ans << endl;
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
有没有标号?
做题时,一定要思考清楚我这个求的是有没有标号的。怎么考虑?例如有向图,\(\cfrac{n(n-1)}{2}\) 条边,一共有 \(2^{\frac{n(n-1)}{2}}\) 种生成图。这样就算同构的两张图,如果节点不一样也会被算两次。这就是有标号的。我们从 \(m\) 个点的图中,选 \(n\) 个点,有多少生成图?\(\dbinom{m}{n} \times 2^{\frac{n(n-1)}{2}}\),这还是有标号的。选出 \(n\) 个点,然后再赋值 \(1 \sim n\),发现这种情况下任意两张有标号下不同的图都是算上了的。\(\dbinom{m}{n}\) 中,选 \((1,2)\) 和 \((1,3)\) 这两种都算上了,也是有标号的。所以做题的时候要分清楚,不要搞糊涂了。