《算法竞赛进阶指南》数论篇(3)-组合计数,Lucas定理,Catalan数列,容斥原理,莫比乌斯反演,概率与数学期望,博弈论之SG函数

176 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

@[TOC] 磨磨唧唧写了半个月,想起来就写点喽,ACM必备数论基础知识已全部讲完了,这只是我对数论的总结和理解,发现文章中有错误,可以指正,仅供参考,谢谢。撒花啦。

组合计数

排列数nn个不同的元素中依次取出mm个元素排成一列,产生的不同排列的数量为:AnmA_n^m=n!(nm)!\frac{n!}{(n-m)!} ### 组合数 从n个不同的元素中取出m个组成一个集合(不考虑顺序),产生不同集合的数量为:CnmC_n^m=n!(nm)!m!\frac{n!}{(n-m)!m!} 一些简单性质: 1.Cnm=CnnmC_n^m=C_n^{n-m} 2.Cnm=Cn1m+Cn1m1C_n^m=C_{n-1}^m+C_{n-1}^{m-1} 3.Cn0+Cn1+Cn2+.....+Cnn=2nC_n^0+C_n^1+C_n^2+.....+C_n^n=2^n 多重集的排列数 设S=S={ n1a1,n2a2,n3a3,.....,nkakn_1\cdot a_1, n_2\cdot a_2, n_3\cdot a_3,....., n_k\cdot a_k}是由n1n_1a1a_1n2n_2a2a_2..nkn_kaka_k组成的多重集。 SS 的全排列个数为:n!n1!n2!n3!....nk!\frac{n!}{n_1!n_2!n_3!....n_k!} 多重集的组合数 在这里插入图片描述 在这里插入图片描述 这里有一个小小的扩展:在上面的约束条件中再添加选取a1a_1个数不少于n0n_0,并保证n0<rn_0<r,问可以产生多重集的数量为: 思路: 选取不少于n0n_0 ,意味着什么呢:在插入(k-1)个挡板时有了约束,我们不妨把插完挡板后, 第一个挡板前的元素个数视为从a1a_1选取的个数。 这样就有了 [n0n_0 和 剩下的(rn0)(r-n_0)], 然后挡板要求只能被落在"剩下的(rn0)(r-n_0)"。 这样就满足了选取a1a_1个数不少于n0n_0的约束。 然后就能迎刃而解啦。 (rn0)+(k1)=rn0+k1(r-n_0)+(k-1)=r-n_0+k-1 产生多重集的数量是:Crn0+k1k1C_{r-n_0+k-1}^{k-1}
这个结论在求更为一般的r的情况,会用到。

例题:Counting swaps

传送门 题意:给定一个n的排列P,问进行若干次操作,每次选择两个整数x,yx,y交换Px,PyP_x,P_y 问用最少的操作次数将给定排列变成单调上升的序列1,2....n1,2....n有多少种方式。对结果1e9+91e9+9取模。 思路

最少操作次数??怎么实现?对于P排列,可以看成一个图。 花出来的图,对于每个点都是入度和出度是1.可知道这个图一定一个环或者多个环。 而且环是一个完整的环,不存在环包含环。而得到单调上升序列,那么图就成了每个点成一个自环。 现在我们需要考虑 原图变成各个点成自环的过程。 两个位置i,ji,j交换,对图的效果是怎么样的呢。 对没有错,i,ji,j指向的点交换了。 如图: 在这里插入图片描述 对位置2,42,4交换一下,得到的图是这样的。 在这里插入图片描述 这里交换了两个位置,使得一个环变成了两个环。数学归纳法,到最后各个点都成了自环。一个环的点有k个。 那么要交换k1k-1次后,k个点成了闭环。(这里可能有人就要问了,为啥我要选一个环上的两点呢,我选不在一个环两个点 进行不行吗?题意有说明,执行交换操作的最小次数,所以我们的操作时要最优的)。

这里我们知道了最少的操作次数,现在我们就来求结果吧。 令一个环长度是n,将该换拆成长度为xxyy的两个环。 那么我们拆成长度x,yx,y的环的方式有nn种。 (n2)!(x1)!(y1)!\frac{(n-2)!}{(x-1)!(y-1)!},为啥呢。xx环,需要交换x1x-1次,yy环,需要交换y1y-1次,然后现在我们已经具体一种方式了, 在一种具体方式中,就是多重集排列数了。 F(n)=nn2F(n)=n^{n-2}我们可以通过打表得到结果。 在这里插入图片描述 ACcode

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+9;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
ll qpow(ll a,ll x){
	ll ans=1;
	while(x){
		if(x&1){
			ans=(ans*a)%P;
		}
		x>>=1;
		a=(a*a)%P;
	}
	return ans;
}
ll jie[N],nv[N];
void pre(){
	jie[0]=1;
	for(int i=1;i<=1e5;i++){
		jie[i]=(jie[i-1]*i)%P;
	}
}
int a[N];
bool flag[N];
int cnt,c[N];
void solve(){
	int n=read();
    rep(i,1,n){
		a[i]=read();flag[i]=false;
	}
	cnt=0;
	rep(i,1,n){
		if(flag[i]==0){
		    int k=0;
			int j=i;
			while(flag[j]==0){
				flag[j]=1;
				k++;
				j=a[j];
			}
			c[++cnt]=k;
		}
	}
	// rep(i,1,cnt){
	// 	printf("%d\n",c[i]);
	// }
	// f_n=n^{n-2}
    ll sum=jie[n-cnt];
	rep(i,1,cnt){
		// sum=sum*nv[c[i]-1]%P;
		sum=sum*qpow(jie[c[i]-1],P-2)%P;
	}
	rep(i,1,cnt){
		if(c[i]==1) continue;
		sum=sum*qpow(c[i],c[i]-2)%P;
	}	
     printf("%lld\n",sum);
	 return ;
}
int main (){
//   freopen("in.txt","r",stdin);
//   freopen("out.txt","w",stdout);
  pre();
  int T=read();
  while(T--)
  solve();
  getchar();
  getchar();
  return 0;
}

Lucas定理 CnmCn mod pm mod pCn/pm/p(mod p)C_n^m\equiv C_{n\ mod\ p}^{m\ mod\ p}*C_{n/p}^{m/p}(mod\ p)

若p是质数,则对于任意整数1<=m<=n1<=m<=n,有: CnmCn mod pm mod pCn/pm/p(mod p)C_n^m\equiv C_{n\ mod\ p}^{m\ mod\ p}*C_{n/p}^{m/p}(mod\ p) 证明: ![在这里插入图片描述](img-blog.csdnimg.cn/3f7156d32ce… =650x500)

例题:古代猪文

题意:给定整数q,n(1<=q,n<=1e9)题意:给定整数q,n(1<=q,n<=1e9)计算qdnCndmod999911659q^{\sum_{d|n} C_n^d}mod999911659. 思路: q^{\sum_{d|n} C_n^d}mod999911659=$$q^{\sum_{d|n} C_n^d\ mod\ 999911658}mod999911659 首先质因数分解9999116658=234679*35617. 然后根据中国剩余定理: 求x mod 9999116658x\ mod\ 9999116658,当然这里x很被计算出来。如果模的是质数就能快得出结果。 M=999911658,Mi=MP[i]M=999911658, M_i=\frac{M}{P[i]} xa1(mod 2)x\equiv a_1(mod\ 2) xa2(mod 3)x\equiv a_2(mod\ 3) xa3(mod 4679)x\equiv a_3(mod\ 4679) xa4(mod 35617)x\equiv a_4(mod\ 35617) Miti1(mod mi)M_it_i\equiv 1(mod\ m_i) 这里的ti=Mimi2t_i=M_i^{m_i-2} 这里不多赘述啦。 直接 x=i=1naiMitimodMx=\sum_{i=1}^n a_iM_it_i\mod M。 然后这里注意一下细节千万别想当然的认为Miti=Mimi1M_it_i=M_i^{m_i-1} 。 因为这里的模数不是同一个,Miti=Mimi1M_it_i=M_i^{m_i-1}这样写,我们就默认模数是mim_i 可是实际的MitiM_it_i的模数是MM.所以ti=Mimi2t_i=M_i^{m_i-2}还是老老实实写上。 拆解了四个质数,那就分开来进行计算咯。 ACcode

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=999911659;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
ll qpow(ll a,ll x,ll mod){
	ll ans=1;
	while(x){
		if(x&1){
			ans=ans*a%mod;
		}
		a=a*a%mod;
		x>>=1;
	}

	return ans;
}
ll jie[N];
ll a[4];
ll mo[4]={2,3,4679,35617};
void init(int m){
	jie[0]=1;
	for(int i=1;i<=m;i++){
		jie[i]=jie[i-1]*i%m;
	}
	return ;
}
ll C(ll n,ll m,ll mod){
	if(n<m) return 0; // 注意一下这里。小心一点。lucas(8,2,7)=0; 
	return (jie[n]*qpow(jie[m],mod-2,mod))%mod * qpow(jie[n-m],mod-2,mod)%mod;
}
ll lucas(ll n,ll m,ll mod){
	if(n<mod and m<mod){
		return C(n,m,mod);
	}
	return C(n%mod,m%mod,mod)*lucas(n/mod,m/mod,mod)%mod;
}
ll M=P-1;
void solve(){
	 ll q,n;
	n=read();
	q=read();
	 if(q%P==0){
        printf("0\n");
		return ;
	 }
	 for(int i=0;i<4;i++){
		 init(mo[i]);
		 ll ans=0;
		//  cout<<"i:"<<i+1<<endl;
         for(int j=1;j*j<=n;j++){
			 //n的约数j,n/j
           if(n%j==0){
			//  cout<<j<<" "<<lucas(n,j,mo[i])<<endl;
             ans=(ans+lucas(n,j,mo[i]))%mo[i];
			 if(n/j!=j){
				//  cout<<n/j<<" "<<lucas(n,n/j,mo[i])<<endl;
				 ans=(ans+lucas(n,n/j,mo[i]))%mo[i];
			 }
		    }
		 }
		//  cout<<"ans:"<<ans<<endl;
		 a[i]=ans;  //存放在不同模数下的结果。
	 }
	 ll sum=0;
	 // 中国剩余定理
	 for(int i=0;i<4;i++){
		 ll Mi=M/mo[i];
		 // 注意模数  mo[i] 和M。
		 ll t=Mi*qpow(Mi,mo[i]-2,mo[i])%M;
		//  cout<<t<<endl;
		//  cout<<qpow(Mi,mo[i]-1,mo[i])<<endl;
		 sum= (sum+a[i]*t)%M;
		 //Mi%M*t%M
	 }
	//  cout<<sum<<endl;
	 printf("%lld\n",qpow(q,sum,P));
	 return ;
}
int main (){
//   freopen("in.txt","r",stdin);
//   freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

Catalan数列

给定n0n1,排成长度为2n的序列给定n个0和n个1,排成长度为2n的序列 满足序列任意前缀0的个数都不少于1的个数的序列的数量为:Catn=C2nnn+1Cat_n=\frac{C_{2n}^n}{n+1} 证明:n个0和n个1,排成长度为2n的序列,若S不满足序列任意前缀0的个数都不少于1,则存在一个最小的 位置2P+12P+1 ,使得前2P+12P+1 有P个0和(P+1)个1.而把S[2P+2,2n]S[2P+2,2n] 的所有数字去反,得到2n的序列有(n-1)个0 和(n+1)个1. 同理对于(n-1)个0和(n+1)个1任意排成的一个长度的2n序列,必然存在一个位置(2P+12P+1 )使得,PP个0和P+1P+1个1, 对后面数字取反,得到的2n序列是n个0和n个1。 在上面中就可以得到了一一对应的关系, 即是"S不满足序列任意前缀0的个数都不少于1"=="(n1)0(n+1)1任意排成的一个长度的2n序列个数""S不满足序列任意前缀0的个数都不少于1" == "(n-1)个0和(n+1)个1任意排成的一个长度的2n序列个数" 而对于(n-1)个0和(n+1)个1的排列个数是C2nn1C_{2n}^{n-1} 所以给定n个0和n个1,排成长度为2n的序列, 满足序列任意前缀0的个数都不少于1的个数的序列的数量是 C2nnC2nn1=(2n)!n!n!(2n)!(n1)!(n+1)!C_{2n}^{n}-C_{2n}^{n-1}=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}=(2n)!(n+1n)n!(n+1)!\frac{(2n)!(n+1-n)}{n!(n+1)!}=C2nnn+1\frac{C_{2n}^n}{n+1}

该推论的一些性质还有: 在这里插入图片描述 解释一下第四个推论: 要求有些不一样,因为行动的时候不能经过y=x直线,意思就是要么0>1或1<0的有多少次数。 那么我们在对角线画上他旁边的对角线,就可以得知封闭的三角形即是可以有Catalan性质的, 然后0和1是(n-1)个即是(2n-2)序列,两条对角线,所以两种情况, 乘2. 得到2Catn12Cat_{n-1} 在这里插入图片描述

容斥原理

S1,S2,....SnS_1,S_2,....S_n为有限集合,S|S|表示S的大小,则: USi=i=1nSi1<=i<j<=nSiSj+1<=i<j<k<=nSiSjSk+....+(1)n+1S1.......Sn|U_{S_i}|=\sum_{i=1}^n|S_i|-\sum_{1<=i<j<=n}|S_i \cap S_j|+\sum_{1<=i<j<k<=n}|S_i \cap S_j \cap S_k|+....+(-1)^{n+1}|S_1 \cap .... \cap ...S_n| 在这里插入图片描述在这里插入图片描述 在这里插入图片描述 解释一下SiSj|S_i\cap S_j|,还是挡板的思路,不过一点小区别是,[ni+nj+2,剩下的元素][n_i+n_j+2,剩下的元素]. 那么[第一个挡板前的个数(nj+1)][第一个挡板前的个数-(n_j+1)]就是aia_i的个数,然后第一个和第二个挡板之间的个数+nj+1第一个和第二个挡板之间的个数+n_j+1就是aja_j的个数。 后面的SiSjSk|S_i\cap S_j \cap S_k|等,都是这样得出来的。

莫比乌斯反演 (大部分和容斥原理一起用 )

设正整数N按照算术基本定理分解质因数为N=p1c1p2c2....pmcmN=p_1^{c_1}p_2^{c_2}....p_m^{c_m},定义函数

0 & \exists i\in[1,m],c_i>1 \\ 1 & m\equiv 0(mod\ 2),\forall i \in[1,m],c_i=1 \\ -1 & m\equiv 1(mod\ 2),\forall i\in[1,m],c_i=1\\ \end{cases}$$ 称$\mu$为莫比乌斯函数。 通俗地讲,当$N$包含相等的质因子时,$\mu(N)=0$. 当$N$的所有的质因子各不相等时. 若N有偶数个质因子,$\mu(N)=1$,若$N$有奇数个质因子,$\mu(N)=-1$ ### 模板 ```cpp int V[N],prime[N],miu[N] //最小质因数 素数 μ(N) void get_miu(int n){ // 用欧拉筛的方式筛 int m=0; miu[1]=1; for(int i=2;i<=n;i++){ if(v[i]==0){ v[i]=i,prime[++m]=i; miu[i]=-1; } for(int j=1;j<=m;j++){ if(prime[j]>v[i] || prime[j]*i>n) break; v[i*prime[j]]=prime[j]; if(miu[i]==0) miu[i*prime[j]]=0; if(v[i]==prime[j]) miu[i*prime[j]]=0; else miu[i*prime[j]]=-miu[i]; } } rep(i,1,100){ printf("i:%d miu:%d\n",i,miu[i]); } return ; } ``` ### 例题:Zap [传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-1101) > **题意**: ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a509fae6255d4292bc18ced243ea7610~tplv-k3u1fbpfcp-zoom-1.image) > **思路**:根据题意要求:可以等价为:多少对二元组$(x,y)$,满足$x<=a/k,y<=b/k$,并且x,y互质。 用到容斥原理:(ps:怎么用容斥呢?当发现求解的问题中,转为至少要多少的时候,很得出。就可以往容斥方向思考。) > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f127bf815ce14b65a138aa7874124037~tplv-k3u1fbpfcp-zoom-1.image) ACcode ```cpp #include<bits/stdc++.h> #define ll long long #define ld long double #define ull unsigned long long #define rep(i,a,b) for(int i=a;i<=b;i++) ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;} const int N=2e5+10; const ll P=1e9+7; ll read(){ ll s = 0, f = 1; char ch = getchar(); while(!isdigit(ch)){ if(ch == '-') f = -1; ch = getchar(); } while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar(); return s * f; } using namespace std; int v[N],prime[N],miu[N]; void get_miu(int n){ int m=0; miu[1]=1; for(int i=2;i<=n;i++){ if(v[i]==0){ v[i]=i,prime[++m]=i; miu[i]=-1; } for(int j=1;j<=m;j++){ if(prime[j]>v[i] || prime[j]*i>n) break; v[i*prime[j]]=prime[j]; if(miu[i]==0) miu[i*prime[j]]=0; if(v[i]==prime[j]) miu[i*prime[j]]=0; else miu[i*prime[j]]=-miu[i]; } } // 求和 rep(i,1,n){ miu[i]+=miu[i-1]; } return ; } void solve(){ ll x,y,d; x=read(); y=read(); d=read(); ll a,b; a=x/d; b=y/d; ll sum=0; ll mins=min(a,b); ll r=0; for(int i=1;i<=mins;i=r+1){ // a,b区间的左端点为i // 在两个区间中,得到右端点最小的那个。 r=min(a/(a/i),b/(b/i)); sum+=(miu[r]-miu[i-1])*(a/i)*(b/i); } printf("%lld\n",sum); return ; } int main (){ // freopen("in.txt","r",stdin); // freopen("zap.txt","w",stdout); get_miu(5e4+10); int T; T=read(); while(T--) solve(); getchar(); getchar(); return 0; } ``` ## 概率与数学期望 ### 例题:Rainbow的信号 [传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-3054) > 题意:在$1~N的N个数中,等概率的选取两个数l和r,如果l>r,则交换l,r$,把$[l,r]的数取出来构成一个P数列$ > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24010d3f5c244f299b95fc459fa0480c~tplv-k3u1fbpfcp-zoom-1.image) > 思路:数据在$1e9$的范围内,按位来思考,最多32位。对每一位分析结果,和概率期望值。 > 这里需要明确一点,**等概率选取l,r**,事件的总和是$N^2$,选取的$l==r$,概率是$\frac{1}{N^2}$ > 当l!=r时,这里的l,r并没有要求谁左谁右,概率为$\frac{2}{N^2}$(ps:假如我等概率选取三个数。$l=r!=k$确定位置的概率为$\frac{A_3^3}{N^3}$。又或者在前提条件下,$l=r!=k$,确定位置的概率是$\frac{C_3^2}{N^3})$ > (以下都是对位操作来实现,最终结果就是32次位操作之和)操作第k位。$(k\in[0,31])$ > 1.考虑或操作,假如确定了右端点r,想知道那些l可以得到值呢,结果很显然,假设距离r最近的1的位置就是L。 那么$[1,L]$都可以是l. 还要思考一点r本身就是l。那么前$[1,r-1]$的概率期望是$P_{or}+=(r-1)*\frac{2}{N^2}*2^k$ > 然后是l==r.即是本身$P_{or}+=\frac{1}{N^2}*2^k$ > 2.考虑且操作,同理,固定$r$,考虑$l$,$[L,r]$连续的1是,那么$l$可以为$l\in[L,r-1]$ 同理$l=r$本身. > 3.异或操作,固定r,寻找合适的l,满足条件. 如:001**0001**001**0**1考虑最后一个1是r,黑色加粗的即是符合的l.呈现出一个 > 很显然的规律,这个时候呢,**l会是以1为间隔的出现。即是奇数段或者偶数段** Accode ```cpp #include<bits/stdc++.h> #define ll long long #define ld long double #define ull unsigned long long #define rep(i,a,b) for(int i=a;i<=b;i++) ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;} const int N=2e5+10; const ll P=1e9+7; ll read(){ ll s = 0, f = 1; char ch = getchar(); while(!isdigit(ch)){ if(ch == '-') f = -1; ch = getchar(); } while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar(); return s * f; } using namespace std; ll a[N]; ld suma,sumo,sumx; ll c1,c2; ll last[2]; ld p1,p2; ll n; void pre(int k,ll ans){ last[0]=last[1]=0; c1=c2=0; int t=1; ll cnt=0; for(int i=1;i<=n;i++){ int flag; if(a[i]&(1<<k)) flag=1; else flag=0; // and if(flag==1){ suma+=ans*p1+cnt*p2*ans; cnt++; }else{ cnt=0; } // or if(flag==1){ sumo+=p1*ans+ans*p2*(i-1); }else{ sumo+=last[1]*p2*ans; } // xor if(flag==1){ sumx+=ans*p1; if(t==0) sumx+=c2*p2*ans,c2++; else sumx+=c1*p2*ans,c1++; t^=1; }else{ if(t==0) sumx+=c1*p2*ans,c2++; else sumx+=c2*p2*ans,c1++; } //位置 if(flag==1) last[1]=i; else last[0]=i; } // printf("%.3LF %.3LF %.3LF\n",sumx,suma,sumo); } void solve(){ n=read(); rep(i,1,n){ a[i]=read(); } p1=1.0/(n*n); p2=p1*2; ll bits=1; rep(i,0,31){ if(i==0) bits=1; else bits*=2; pre(i,bits); } printf("%.3LF %.3LF %.3LF\n",sumx,suma,sumo); } int main (){ // freopen("in.txt","r",stdin); // freopen("out.txt","w",stdout); solve(); getchar(); getchar(); return 0; } ``` ### 例题: 绿豆蛙的归宿 [传送门](https://vjudge.net/problem/%E9%BB%91%E6%9A%97%E7%88%86%E7%82%B8-3036) > 题意: > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9e8b436f518848e6a39e3c9412371a2a~tplv-k3u1fbpfcp-zoom-1.image) > 思路: > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/31a9a684a8154508b80addc229ea8739~tplv-k3u1fbpfcp-zoom-1.image) > 总结,随便敲敲图的前向性构图方式,图中取反,执行拓扑排序的思想,入队列的点的要求是已经得出了该点u到终点n的期望。 Accode ```cpp #include<bits/stdc++.h> #define ll long long #define ld long double #define ull unsigned long long #define rep(i,a,b) for(int i=a;i<=b;i++) ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;} const int N=2e5+10; const ll P=1e9+7; ll read(){ ll s = 0, f = 1; char ch = getchar(); while(!isdigit(ch)){ if(ch == '-') f = -1; ch = getchar(); } while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar(); return s * f; } using namespace std; ll n,m; ll ver[N],edge[N],nex[N],head[N]; ll cnt; void addedge(int x,int y,int value){ ver[++cnt]=y; edge[cnt]=value; nex[cnt]=head[x]; head[x]=cnt; return ; } ll in[N],deg[N]; double value[N]; void pre(){ queue<int> q; q.push(n); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=nex[i]){ int v=ver[i]; // printf("v:%d in:%d\n",v,in[v]); value[v]+=(value[u]+edge[i])/deg[v]; in[v]--; if(in[v]==0){ // printf("%d\n",v); q.push(v); } } } printf("%.2f\n",value[1]); return ; } void solve(){ // n=read(); m=read(); ll u,v; rep(i,1,m){ u=read(); v=read(); addedge(v,u,read()); deg[u]++; in[u]++; } value[n]=0; pre(); return ; } int main (){ // freopen("in.txt","r",stdin); // freopen("out.txt","w",stdout); solve(); getchar(); getchar(); return 0; } ``` ## 博弈论之SG函数 ### NIM博弈 > 用在一推事件是相等的,且博弈的双方都是同等机会公平的选择 > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12918feae1ea40f58eaf07789b84c823~tplv-k3u1fbpfcp-zoom-1.image) > 结论&定理:NIM博弈先手必胜,当且仅当$A_1\ xor A_2\ xor.......xor\ A_n\neq 0$ > 证明:首先知道a^b=x, 可以得到 x^b=a; > 1.当全是0时,0 ^ 0 ^ 0 ^ 0....^ =0 > 2.如果当前a1^ a2^ a3^ ai...^ an=x; 则可通过一次操作从某堆中取出石子将其转变为 a1^ a2^ a3^ ai'...^an=0; > 3.如果当前a1^ a2^ a3^ ai'...^ an=0;则不能通过一次操作再使a1^ a2^ a3^ ai'...^ an=0。 对②证明:由于异或值为x,那么对于a1~an中一定存在ai(二进制)的最高位为1(因为对于异或只有存在1和0才能得1). > 那么我们就可以去ai堆,使ai堆只剩下ai^ x(显然:ai>ai^ x 因为ai和x的最高位都为1,异或后为0,比如:a^ > b=x,a^ b^x=0,) > 现在我们由以上的3个定理,给出石子a1~an,假设此时a1^a2^a3..an=x(非0);根据定理②我们就可以 > 操作一步使a1^a2^a3..an=0;根据定理③对手只能将当前结果变为x(非0);重复执行以上的操作,一定会存在我执行完操作后,每堆石子都为0;对手不能操作,我们胜利。 ### SG函数 > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d6ab25edfab4b288b7ea56fbb9ddbc0~tplv-k3u1fbpfcp-zoom-1.image) > 注意:这里的mex(S)是得到**不属于集合S的最小非负整数** > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0662c267639344a5a93b08a44fd50284~tplv-k3u1fbpfcp-zoom-1.image) > ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d5af46785c6646d79ed352b5cd55838f~tplv-k3u1fbpfcp-zoom-1.image) #### 例题 Cutting game [传送门](https://vjudge.net/problem/POJ-2311) 题意: ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25565fcb33144526897b0bb0f4caf523~tplv-k3u1fbpfcp-zoom-1.image) 思路:典型的SG算法。必败的初始有(2,2) or (2,3) or (3,2). ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe8bdc1721e54645aa201a6d73069010~tplv-k3u1fbpfcp-zoom-1.image) ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae18f86b970242cea759e5e5979637d5~tplv-k3u1fbpfcp-zoom-1.image) ACcode ```cpp #include<iostream> #include<cstring> #define ll long long #define ld long double #define ull unsigned long long #define rep(i,a,b) for(int i=a;i<=b;i++) ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;} const int N=2e5+10; const ll P=1e9+7; ll read(){ ll s = 0, f = 1; char ch = getchar(); while(!isdigit(ch)){ if(ch == '-') f = -1; ch = getchar(); } while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar(); return s * f; } using namespace std; ll n,m; int a[205][205]; int dfs(int x,int y){ if(a[x][y]!=-1) return a[x][y]; if((x==2 and (y==2 or y==3)) or (x==3 and y==2)){ a[x][y]=0; return 0; } bool b[250]; memset(b,0,sizeof(b)); for(int i=2;i<=x-i;i++){ b[dfs(i,y)^dfs(x-i,y)]=true; } for(int j=2;j<=y-j;j++){ b[dfs(x,j)^dfs(x,y-j)]=true; } for(int i=0;;i++){ if(b[i]==0){ a[x][y]=i; return i; } } } void solve(){ memset(a,-1,sizeof(a)); // cout<<a[2][3]<<endl; while(scanf("%d %d",&n,&m)!=EOF){ dfs(n,m); if(a[n][m]) printf("WIN\n"); else printf("LOSE\n"); } } int main (){ // freopen("in.txt","r",stdin); // freopen("out.txt","w",stdout); solve(); getchar(); getchar(); return 0; } ```