【冲刺NOI】计数题专训

前言

写这篇博客的时候犯困,所以有的繁杂解释直接跳过了,留给读者思考。

感觉计数题几乎都是DP耶,那为什么不直接放到DP专题里呢?

AGC026D-Histogram Coloring

如果一个矩阵中确定了上面一行的染色方案,那么下面一行就只有两种选择:

  • 上面一行的颜色取反,这样一定合法;

  • 与上面一行颜色相同,只有在上面一行红蓝相间的时候合法。

于是可以得到一个笛卡尔树的做法:每次把高度最小的列提出来,把分出的区间往下递归,返回两个值: a a a——底层颜色红蓝相间的方案数, b b b——底层任意染色的方案数。

然后我们要把得到的 a i , b i a_i,b_i ai,bi 转移到当前层的 a ′ , b ′ a',b' a,b

对于 a ′ a' a 来讲,要保证红蓝相间,所以每层恰有两种染色方案,所以 a ′ = 2 Δ h ∗ ∏ a i a'=2^{\Delta h}*\prod a_i a=2Δhai
对于 b ′ b' b,我们可以先确定下面的第一行,也就是有 2 w ∗ ∏ ( a i ( 相 同 ) + b i ( 相 反 ) ) 2^w*\prod(a_{i(相同)}+b_{i(相反)}) 2w(ai()+bi()),其中 w w w 是这一层提出来的列的数量,显然是任意取的。然后得到的这些方案由于不一定红蓝相间只能每行取反,所以还要加上不每行取反的方案数也就是 a ′ a' a。此时 ∏ a i \prod a_i ai 被重复统计了两遍,所以要减一减。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define END putchar('\n')
#define lowbit(x) ((x)&-(x))
#define inline jzmyyds
using namespace std;
const int MAXN=233;
const ll INF=1e18;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt>0)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
const ll MOD=1e9+7;
ll ksm(ll a,ll b,ll mo){
	ll res=1;
	for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
	return res;
}
int n,h[MAXN];
#define pll pair<ll,ll>
pll solve(int l,int r){
	if(l>r)return pll(1,0);
	int x=2e9,pr=l-1,w=0;
	for(int i=l;i<=r;i++)x=min(x,h[i]);
	for(int i=l;i<=r;i++)h[i]-=x;
	ll a=1,b=1;
	for(int i=l;i<=r+1;i++)if(i>r||!h[i]){
		pll y=solve(pr+1,i-1);
		pr=i,w+=(i<=r&&!h[i]),(a*=y.fi)%=MOD,(b*=y.fi+y.se)%=MOD;
	}return pll(a*ksm(2,x,MOD)%MOD,(a*(ksm(2,x,MOD)+MOD-2)+b*ksm(2,w,MOD))%MOD);
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++)h[i]=read();
	print(solve(1,n).se);
	return 0;
}

CF708E-Student’s Camp

注意到一种可能的连通的最终状态唯一对应着一种贴着左边缘走的路线,所以我们可以考虑以行走路线为状态DP,统计每种路线成立的概率。

路线在大部分情况下是不交的,只有在某一行的格子比相邻行往左凸出的时候会发生折返。我们一行一行地DP转移,不折返的情况都可以顺推,折返的情况记录一下前缀后缀和即可。状态可能要多设计几个,复杂度 O ( n m ) O(nm) O(nm)

不要忘了预处理一行的某一边被扣掉多少格的概率。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define END putchar('\n')
#define lowbit(x) ((x)&-(x))
#define inline jzmyyds
using namespace std;
const int MAXN=2333;
const ll INF=1e18;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt>0)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
const ll MOD=1e9+7;
ll ksm(ll a,ll b,ll mo){
	ll res=1;
	for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
	return res;
}
ll fac[114514],inv[114514];
int init(int n){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
	inv[n]=ksm(fac[n],MOD-2,MOD);
	for(int i=n-1;i>1;i--)inv[i]=inv[i+1]*(i+1)%MOD;
	return 1919810;
}
int cbddl=init(1e5+233);
ll C(int n,int m){
	if(m>n||m<0)return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int n,m,k;
ll dp[2][MAXN][4],f[MAXN],g[MAXN],h[MAXN],q[MAXN],p,ans;
int main()
{
	n=read(),m=read(),p=read(),p=p*ksm(read(),MOD-2,MOD)%MOD,k=read();
	for(int i=0;i<=min(m,k);i++)
		g[i]=ksm(p,i,MOD)*ksm(MOD+1-p,k-i,MOD)%MOD*C(k,i)%MOD;
	f[0]=g[0];
	for(int i=1;i<=m;i++)f[i]=(f[i-1]+g[i])%MOD;
	for(int i=1;i<=m;i++)dp[0][i][3]=1;
	for(int x=1;x<=n;x++){
		bool e=x&1,t=e^1;
		for(int i=1;i<=m;i++){
			dp[e][i][0]=dp[e][i][1]=dp[e][i][2]=dp[e][i][3]=0;
			ll cg=g[i-1]*f[m-i]%MOD,&d=dp[e][i][0]=dp[t][i][0]*cg%MOD;
			(d+=dp[t][i][1]*f[m-i])%=MOD,(d+=dp[t][i][2]*g[i-1])%=MOD;
			(d+=dp[t][i][3])%=MOD;
		}
		for(int i=2;i<=m;i++)
			dp[e][i][1]=(dp[e][i-1][1]+dp[e][i-1][0]*g[i-2])%MOD;
		for(int i=1;i<=m;i++)
			h[i]=(dp[t][i][0]*g[i-1]%MOD*f[m-i]+dp[t][i][2]*g[i-1])%MOD;
		for(int i=m-1;i>0;i--)dp[e][i][2]=(dp[e][i+1][2]+h[i+1]*f[m-i-1])%MOD;
		q[1]=0;
		for(int i=2;i<=m;i++)q[i]=(q[i-1]+h[i]*f[i-2])%MOD;
		for(int i=m-1;i>1;i--)
			dp[e][i][3]=(dp[e][i+1][3]+h[i+1]*f[m-i-1])%MOD;
		for(int i=2;i<=m;i++)dp[e][i][3]=(dp[e][i][3]*f[i-2]+q[i]*f[m-i])%MOD;
	}
	for(int i=1;i<=m;i++)
		(ans+=dp[n&1][i][0]*g[i-1]%MOD*f[m-i]%MOD+dp[n&1][i][2]*g[i-1])%=MOD;
	print(ans);
	return 0;
}

ARC096C-Everything on It

搞了一阵发现不会做,滚去看永神的题解

考虑容斥,记 f ( c , k ) f(c,k) f(c,k) 表示 c c c 个元素,选出大小为 k k k 的子集族,每个元素出现次数 ≤ 1 \le 1 1 的方案数,那么答案即为
∑ c = 0 n ( n c ) 2 2 n − c ∑ k = 0 c ( 2 n − c ) k f ( c , k ) \sum_{c=0}^n {n\choose c}2^{2^{n-c}}\sum_{k=0}^c (2^{n-c})^kf(c,k) c=0n(cn)22nck=0c(2nc)kf(c,k)这个 f ( c , k ) f(c,k) f(c,k) 可以用类似斯特林数的方式递推求出:
f ( c , k ) = f ( c − 1 , k − 1 ) + f ( c − 1 , k ) ∗ ( k + 1 ) f(c,k)=f(c-1,k-1)+f(c-1,k)*(k+1) f(c,k)=f(c1,k1)+f(c1,k)(k+1)此处乘 k + 1 k+1 k+1 是因为有出现次数为0的情况。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long//God JZM!!
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=3e3+4;
const ll INF=1e16;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
ll MOD,f[MAXN][MAXN],C[MAXN][MAXN],ans;
ll ksm(ll a,ll b,ll mo){
	ll res=1;
	for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
	return res;
}
int n;
int main()
{
	n=read(),MOD=read(),C[0][0]=f[0][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=C[i][0]=f[i][0]=1;j<=i;j++){
			C[i][j]=C[i-1][j]+C[i-1][j-1];
			if(C[i][j]>=MOD)C[i][j]-=MOD;
			f[i][j]=(f[i-1][j-1]+f[i-1][j]*(j+1))%MOD;
		}
	for(int s=0;s<=n;s++){
		ll cg=ksm(2,s,MOD),mi=1,ad=0;
		for(int k=0;k<=n;k++)(ad+=f[n-s][k]*mi)%=MOD,(mi*=cg)%=MOD;
		(ad*=C[n][s]*ksm(2,ksm(2,s,MOD-1),MOD)%MOD)%=MOD;
		if((n-s)&1)ad=MOD-ad;
		ans+=ad;
	}print(ans%MOD);
	return 0;
}

AGC009E-Eternal Average

终于做到我见过的题了,贴一份我以前的题解

AGC019F-Yes or No

分情况讨论一下:如果此时 Yes 和 No 的数量不等,那么肯定猜多的那边;如果数量相等,那么随便选一边。

于是当我们把问题转化成平面上从 ( n , m ) (n,m) (n,m) 走到 ( 0 , 0 ) (0,0) (0,0),每次随机往下或者往左走的问题时,可以发现一条路径的贡献就是 max ⁡ ( n , m ) + 路 径 与 y = x 交 点 个 数 − 1 2 \max(n,m)+\frac{路径与y=x交点个数-1}{2} max(n,m)+2y=x1,枚举 O ( n ) O(n) O(n) 个交点统计一下贡献即可。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define END putchar('\n')
#define lowbit(x) ((x)&-(x))
#define inline jzmyyds
using namespace std;
const int MAXN=1e6+5;
const ll INF=1e18;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt>0)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
const ll MOD=998244353;
ll ksm(ll a,ll b,ll mo){
	ll res=1;
	for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
	return res;
}
ll fac[MAXN],inv[MAXN];
void init(int n){
	fac[0]=inv[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
	inv[n]=ksm(fac[n],MOD-2,MOD);
	for(int i=n-1;i>0;i--)inv[i]=inv[i+1]*(i+1)%MOD;
}
ll C(int n,int m){
	if(m>n||m<0)return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int n,m;
ll ans,cg;
int main()
{
	n=read(),m=read(),init(n+m);
	if(n>m)swap(n,m);
	ans=m,cg=ksm(C(n+m,n)<<1,MOD-2,MOD);
	for(int i=1;i<=n;i++)
		(ans+=C(i<<1,i)*C(n+m-(i<<1),n-i)%MOD*cg)%=MOD;
	print(ans);
	return 0;
}

AGC018E-Sightseeing Plan

如果我讲得不详细可以看这篇博客

计算两点间的路径数可以组合数快速算;

计算一个点到一个矩形内的路径数可以表示成点到矩形四角的路径数的带权和,如从 ( 0 , 0 ) (0,0) (0,0) 到某矩形: ( x 2 + y 2 + 2 x 2 + 1 ) − ( x 2 + y 1 + 1 x 2 + 1 ) − ( x 1 + y 2 + 1 x 1 ) + ( x 1 + y 1 x 1 ) {x_2+y_2+2\choose x_2+1}-{x_2+y_1+1\choose x_2+1}-{x_1+y_2+1\choose x_1}+{x_1+y_1\choose x_1} (x2+1x2+y2+2)(x2+1x2+y1+1)(x1x1+y2+1)+(x1x1+y1),证明略;

矩形到矩形的方案数可以枚举两边的四角,然后带权路径加起来。

现在考虑三个矩形,我们枚举第一个和第三个矩形的四角,统计经过中间的矩形的方案数。我们发现一条路径到中间矩形必定有一个边界上的进入点和一个离开点,而每个进入点(a,b)和离开点(c,d)的组合对应着矩形中 c − a + d − b + 1 c-a+d-b+1 ca+db+1 个点的选点方案。我们把其中的 c + d + 1 c+d+1 c+d+1 − a − b -a-b ab 分离,那么就可以分别算每个进入点的贡献和每个离开点的贡献。

进入点和离开点个数都是 O ( n ) O(n) O(n) 的,所以总复杂度 O ( n ) O(n) O(n)

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define END putchar('\n')
#define lowbit(x) ((x)&-(x))
#define inline jzmyyds
using namespace std;
const int MAXN=114514+1919810;
const ll INF=1e18;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt>0)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
const ll MOD=1e9+7;
ll ksm(ll a,ll b,ll mo){
	ll res=1;
	for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
	return res;
}
ll fac[MAXN],inv[MAXN];
int init(int n){
	fac[0]=inv[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
	inv[n]=ksm(fac[n],MOD-2,MOD);
	for(int i=n-1;i>0;i--)inv[i]=inv[i+1]*(i+1)%MOD;
	return 0xcbdd1;
}
int cbddl=init(2e6+233);
ll C(int n,int m){
	if(m>n||m<0)return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
ll F(int u,int v,int p,int q){return C(p-u+q-v,p-u);}
int n,m;
ll ans;
struct itn{
	int x,y,c;itn(){}
	itn(int X,int Y,int C){x=X,y=Y,c=C;}
}s[2],t[2];
vector<itn>S,T;
int a,b,c,d;
int main()
{
	s[0].x=read()-1,s[1].x=read(),a=read(),b=read(),t[0].x=read(),t[1].x=read()+1;
	s[0].y=read()-1,s[1].y=read(),c=read(),d=read(),t[0].y=read(),t[1].y=read()+1;
	S.emplace_back(itn(s[0].x,s[0].y,1)),S.emplace_back(itn(s[0].x,s[1].y,MOD-1));
	S.emplace_back(itn(s[1].x,s[1].y,1)),S.emplace_back(itn(s[1].x,s[0].y,MOD-1));
	T.emplace_back(itn(t[0].x,t[0].y,1)),T.emplace_back(itn(t[0].x,t[1].y,MOD-1));
	T.emplace_back(itn(t[1].x,t[1].y,1)),T.emplace_back(itn(t[1].x,t[0].y,MOD-1));
	for(auto&x:S)for(auto&y:T){
		const ll cg=1ll*x.c*y.c%MOD;
		for(int i=a;i<=b;i++){
			(ans+=F(x.x,x.y,i,c-1)*F(i,c,y.x,y.y)%MOD*cg%MOD*(MOD-i-c))%=MOD;
			(ans+=F(x.x,x.y,i,d)*F(i,d+1,y.x,y.y)%MOD*cg%MOD*(i+d+1))%=MOD;
		}
		for(int i=c;i<=d;i++){
			(ans+=F(x.x,x.y,a-1,i)*F(a,i,y.x,y.y)%MOD*cg%MOD*(MOD-a-i))%=MOD;
			(ans+=F(x.x,x.y,b,i)*F(b+1,i,y.x,y.y)%MOD*cg%MOD*(b+i+1))%=MOD;
		}
	}print(ans);
	return 0;
}

难题太多了,只能写到这么多

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值