一些总结
1、异或问题
处理异或的问题有一种常用技巧:就是把每个二进制位拆开单独处理,这样只有不同的才会有贡献。
这样就将异或问题转换为了是否为不同的数。
例:魔卡少女
给定一个序列,可以修改一个数或查询某一个区间内所有连续子串的异或和的和。
首先,求出前缀和,将问题转变为区间内任取两个数的异或和。
然后,把位拆开处理,这样就变成了一段区间内取两个不同数的方法,这就使区间内1的个数乘以区间内0的个数。然后就可以使用线段树来维护了。
修改时考虑每个二进制位是否变化即可。
2、线段树代替treap
使用权值线段树,配合离散化或动态开点线段树,操作方法如下:
插入,删除在对应位置上加1,减1即可。
查询排名就是查询前缀和。
查询排名为k的数,在线段树上二分即可(要特判不存在的情况)。
查询前驱或后继:
以后继为例,将x的后缀分解为若干个不相交区间,找到第一个和不为0的区间,并进行二分,找到其中第1个数。
但是,不能解决区间操作(例如区间翻转,区间增加)
代码:
#include <stdio.h>
#include <stdlib.h>
struct SPx
{
int z,i;
};
SPx px[100010];
int cmp(const void*a ,const void*b)
{
return ((SPx*)a)->z-((SPx*)b)->z;
}
int he[400010],lx[100010],dy[100010],ls[100010];
void pu(int i)
{
he[i]=he[i<<1]+he[(i<<1)|1];
}
void xiugai(int i,int l,int r,int j,int x)
{
if(l+1==r)
{
he[i]+=x;
return;
}
int m=(l+r)>>1;
if(j<m)
xiugai(i<<1,l,m,j,x);
else
xiugai((i<<1)|1,m,r,j,x);
pu(i);
}
int chaxun(int i,int l,int r,int j)
{
if(l+1==r)
return he[i];
int m=(l+r)>>1;
if(j<m)
return chaxun(i<<1,l,m,j);
else
return he[i<<1]+chaxun((i<<1)|1,m,r,j);
}
int findk(int i,int l,int r,int k)
{
if(l+1==r)
return l;
int m=(l+r)>>1,s=he[i<<1];
if(k<=s)
return findk(i<<1,l,m,k);
else
return findk((i<<1)|1,m,r,k-s);
}
int qianqu(int i,int l,int r,int j)
{
if(he[i]==0)//这里很关键,避免对空区间进行分解,保证复杂度
return -1;
if(l+1==r)
return he[i]==0?-1:l;
int m=(l+r)>>1,t;
if(j>=m&&(t=qianqu((i<<1)|1,m,r,j))!=-1)
return t;
else
return qianqu(i<<1,l,m,j);
}
int houji(int i,int l,int r,int j)
{
if(he[i]==0)//这里很关键,避免对空区间进行分解,保证复杂度
return -1;
if(l+1==r)
return he[i]==0?-1:l;
int m=(l+r)>>1,t;
if(j<m&&(t=houji(i<<1,l,m,j))!=-1)
return t;
else
return houji((i<<1)|1,m,r,j);
}
int main()
{
int n,m=0,s=0;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d%d",&lx[i],&px[s].z);
if(lx[i]!=4)
{
px[s].i=i;
s+=1;
}
else
ls[i]=px[s].z;
}
qsort(px,s,sizeof(SPx),cmp);
for(int i=0;i<s;i++)
{
if(i!=0&&px[i].z!=px[i-1].z)
m+=1;
ls[px[i].i]=m;
dy[m]=px[i].z;
}
for(int i=0;i<n;i++)
{
if(lx[i]==1)
xiugai(1,0,m+1,ls[i],1);
else if(lx[i]==2)
xiugai(1,0,m+1,ls[i],-1);
else if(lx[i]==3)
printf("%d\n",ls[i]==0?1:chaxun(1,0,m+1,ls[i]-1)+1);
else if(lx[i]==4)
printf("%d\n",dy[findk(1,0,m+1,ls[i])]);
else if(lx[i]==5)
printf("%d\n",dy[qianqu(1,0,m+1,ls[i]-1)]);
else
printf("%d\n",dy[houji(1,0,m+1,ls[i]+1)]);
}
return 0;
}
缺点:
不能实现区间的操作
如果动态添加,空间比treap多一个log。
3、状压分组背包
分组背包就是有若干组物品,每组只能选一个。
但有的时候一个物品可能在多个组中,这时在选择一个物品时就要记录它对其它组的影响,就是记录其它的组是否已经选择。
例如:一个物品同时在i ,j 这两个组里,那么考虑到第i组时如果选择了这个物品,那么第j组也就选择了这个物品。
例题:求从1到n中选择k个互质的正整数的方案数。
思路:将每个数质因数分解,若分解后包含i则它包含在第i组中。dp时,只考虑它所在的编号最大的组。
可以发现若一个数同时在两个组中,它所在的另一个组的编号一定小于等于19(8个质数)。这样就可以状压了。
代码:
#include <stdio.h>
int dp[256][510],md=1000000007;
int ss[510],sl=0,zt[510];
bool sh[510];
int sc[510][510],gs[510];
void ycl(int n)
{
for(int i=0;i<sl;i++)
gs[i]=0;
for(int i=2;i<=n;i++)
{
bool z=false;
for(int j=0;ss[j]*ss[j]<=i;j++)
{
if(i%ss[j]==0&&(i/ss[j])%ss[j]==0)
{
z=true;
break;
}
}
if(z)
continue;
zt[i]=0;
for(int j=0;j<8;j++)
{
if(i%ss[j]==0)
zt[i]=zt[i]|(1<<j);
}
int zd;
for(int j=sl-1;j>=0;j--)
{
if(i%ss[j]==0)
{
zd=j;
break;
}
}
sc[zd][gs[zd]++]=i;
}
}
int main()
{
for(int i=2;i<=500;i++)
{
if(!sh[i])
ss[sl++]=i;
for(int j=0;j<sl;j++)
{
if(i*ss[j]>500)
break;
sh[i*ss[j]]=true;
if(i%ss[j]==0)
break;
}
}
int T;
scanf("%d",&T);
while(T--)
{
int n,m;
scanf("%d%d",&n,&m);
ycl(n);
for(int i=0;i<=sl;i++)
{
for(int j=0;j<256;j++)
{
for(int k=m;k>=0;k--)
{
if(i==0)
{
if(k==0)
dp[j][k]=1;
else
dp[j][k]=0;
}
else if(k>0)
{
for(int a=0;a<gs[i-1];a++)
{
int z=sc[i-1][a];
if((j&zt[z])==0)
dp[j][k]=(dp[j][k]+dp[j+zt[z]][k-1])%md;
}
}
}
}
}
int jg=1;
for(int i=1;i<=m;i++)
{
jg=(jg+dp[0][i])%md;
if(i+1<=m)
jg=(jg+dp[0][i])%md;
}
printf("%d\n",jg);
}
return 0;
}
例题:寿司晚宴。
和上题一样,只不过这里不是背包,但这个思路仍然适用。
代码:
#include <stdio.h>
#define ll long long
#define re register
ll dp[256][256][3],jh[256][256][3];
int fj[510][20],ma[510];
int dy[510][510],sl[510],zt[510];
int wz[510],ss[510],gs=0,px[510];
bool sa[510];
void fenjie(int x)
{
int s=0,t=x;
for(int i=2;i*i<=x;i++)
{
if(x%i==0)
{
fj[t][s++]=i;
while(x%i==0)
x/=i;
}
}
if(x!=1)
fj[t][s++]=x;
ma[t]=-1;
for(int i=0;i<s;i++)
{
if(fj[t][i]>ma[t])
ma[t]=fj[t][i];
}
dy[ma[t]][sl[ma[t]]++]=t;
int z=0;
for(int i=0;i<s;i++)
{
if(fj[t][i]<=19)
z|=(1<<wz[fj[t][i]]);
}
zt[t]=z;
}
int main()
{
int n;
ll p;
scanf("%d%lld",&n,&p);
for(int i=2;i<=n;i++)
{
if(!sa[i])
ss[++gs]=i;
for(int j=1;j<=gs;j++)
{
if(i*ss[j]>n)
break;
sa[i*ss[j]]=true;
if(i%ss[j]==0)
break;
}
}
for(int i=1;i<=gs;i++)
wz[ss[i]]=i-1;
for(int i=2;i<=n;i++)
fenjie(i);
for(int i=2,s=0;i<=n;i++)
{
for(int j=0;j<sl[i];j++)
px[++s]=dy[i][j];
}
for(re int i=0;i<=n-1;i++)
{
for(re int x=0;x<256;x++)
{
for(re int y=0;y<256;y++)
{
if(i==0)
{
dp[x][y][0]=1;
continue;
}
if(ma[px[i]]!=ma[px[i-1]])
{
dp[x][y][2]=dp[x][y][0]=jh[x][y][0]+jh[x][y][1]-jh[x][y][2];
if((zt[px[i]]&y)==0)
dp[x][y][0]+=jh[x|zt[px[i]]][y][0]+jh[x|zt[px[i]]][y][1]-jh[x|zt[px[i]]][y][2];
dp[x][y][1]=jh[x][y][0]+jh[x][y][1]-jh[x][y][2];
if((zt[px[i]]&x)==0)
dp[x][y][1]+=jh[x][y|zt[px[i]]][0]+jh[x][y|zt[px[i]]][1]-jh[x][y|zt[px[i]]][2];
}
else
{
dp[x][y][2]=jh[x][y][2];
dp[x][y][0]=jh[x][y][0];
if((zt[px[i]]&y)==0)
dp[x][y][0]+=jh[x|zt[px[i]]][y][0];
dp[x][y][1]=jh[x][y][1];
if((zt[px[i]]&x)==0)
dp[x][y][1]+=jh[x][y|zt[px[i]]][1];
}
dp[x][y][0]%=p;
dp[x][y][1]%=p;
dp[x][y][2]%=p;
}
}
for(re int x=0;x<256;x++)
{
for(re int y=0;y<256;y++)
jh[x][y][0]=dp[x][y][0],jh[x][y][1]=dp[x][y][1],jh[x][y][2]=dp[x][y][2];
}
}
printf("%lld",((dp[0][0][0]+dp[0][0][1]-dp[0][0][2])%p+p)%p);
return 0;
}
4、网格图最小割
由于是平面图,不需要网络流(网络流太慢)。
可以使用最短路,求一条从左下到右上的最短路即可。
考虑有向图的情况,若最短路中走的是向上向右的方向,则说明在原图中切掉的是向右向下的边,否则切掉的就是向左向上的边。
例题:NOI2010海拔。
海拔只能是0或1,且0,1一定连通。
所以,使用最小割,由于是网格图,可以采用这种方法(有向图)。
代码:
#include <stdio.h>
#include <queue>
using namespace std;
#define ll long long
struct SJd
{
int x;
int y;
ll jl;
SJd(){}
SJd(int X,int Y,int Jl)
{
x=X;
y=Y;
jl=Jl;
}
};
bool operator<(const SJd a,const SJd b)
{
return a.jl>b.jl;
}
int qz[510][510][4],n;
bool bk[510][510];
ll jl[510][510],inf=99999999999999;
priority_queue <SJd> pq;
void update(int x,int y,ll z)
{
if(z<jl[x][y])
{
jl[x][y]=z;
pq.push(SJd(x,y,z));
}
}
ll dij()
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
jl[i][j]=inf;
}
for(int i=0;i<n;i++)
update(i,0,qz[i][0][1]);
for(int i=0;i<n;i++)
update(n-1,i,qz[n][i][3]);
while(!pq.empty())
{
SJd jd=pq.top();
pq.pop();
if(bk[jd.x][jd.y])
continue;
bk[jd.x][jd.y]=true;
int x=jd.x,y=jd.y;
if(jd.x>0)
update(x-1,y,jd.jl+qz[x][y][3]);
if(jd.y+1<n)
update(x,y+1,jd.jl+qz[x][y+1][1]);
if(jd.x+1<n)
update(x+1,y,jd.jl+qz[x+1][y+1][2]);
if(jd.y>0)
update(x,y-1,jd.jl+qz[x+1][y][0]);
}
ll jg=inf;
for(int i=0;i<n;i++)
{
ll t=jl[0][i]+qz[0][i][3];
if(t<jg)
jg=t;
}
for(int i=0;i<n;i++)
{
ll t=jl[i][n-1]+qz[i][n][1];
if(t<jg)
jg=t;
}
return jg;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<=n;i++)
{
for(int j=0;j<n;j++)
{
int a;
scanf("%d",&a);
qz[i][j][3]+=a;
}
}
for(int i=0;i<n;i++)
{
for(int j=0;j<=n;j++)
{
int a;
scanf("%d",&a);
qz[i][j][1]+=a;
}
}
for(int i=0;i<=n;i++)
{
for(int j=0;j<n;j++)
{
int a;
scanf("%d",&a);
qz[i][j+1][2]+=a;
}
}
for(int i=0;i<n;i++)
{
for(int j=0;j<=n;j++)
{
int a;
scanf("%d",&a);
qz[i+1][j][0]+=a;
}
}
printf("%lld",dij());
return 0;
}
5、可持久化并查集
有时,我们的可持久化并查集只需要查询历史版本,不需要在历史版本上修改(例如添加边后,询问之前的连通性)。
这时,我们有一个log的做法。
维护一个只按秩合并,不路径压缩的并查集。
对于每条边,记录它出现的时间,判断连通性找根时,如果它到它的父节点的边出现了,就找父节点的根,否则它就是根。
这样,不路径压缩,边的状态只有两种,存在/不存在,若存在就是唯一的。
6.、矩阵快速幂优化
例题:
甜品 (dessert.cpp) 题目描述
这天上午风和日丽、阳光明媚。于是,除了在迷阵里受伤后在家休养的XCJ之外,其余所有的机房老人们来到了学校旁边的北陵公园。众所周知,公园里一共有n个景点(编号从1到n),景点之间由若干条有向道路相连,每个景点都有一个甜品店。
机房老人们当然喜欢吃甜品,他们在编号为s的景点买了许多好吃的甜品。但是由于制作甜品需要时间,所以他们必须等待时间t。为了防止吃了甜品会发胖,在等待甜品的过程中,机房老人们每过一个单位时间都必须沿着当前所在景点的任意一条道路(出边)移动到那条道路指向的景点去。可能有道路连接一个点和它本身,也可能两个景点之间有多条道路相连。在甜品做完时,他们必须恰好回到出发点,不能提前也不能延后。这也就是说,他们必须恰好走了t条道路后,回到出发点s。
现在机房老人的贪吃本性已经发作,他们已经馋到无法编程进行计算。现在他们找到你,让你告诉他们,对于每个出发点s(s=1,2...n),以及每个等待时间t(t=1,2,...Q),从s出发经过t时间后又回到s的方案数是多少。特殊的,如果走到了一个没有出边的点,那将被逐出公园,自然也吃不到甜品了。
为了便于检验,你只需要输出所有情况(共nQ种)的方案数分别对给定正整数P取模后的异或和即可(即: (ans1 % P ) xor (
ans2 % P ) xor ( ans3 % P ) ... xor ( ans tot % P ) (tot=nQ) )。输入 第一行三个正整数n,Q,P,分别表示公园的景点数、等待时间的上限以及给定的模数。
接下来一个n*n的矩阵(n行,每行n个数),矩阵第i行第j列的数表示从i出发到j的单向道路的数目。输出 一个整数,表示所有情况的方案数对给定正整数P取模后的异或和。
样例输入 2 2 1000000207 2 3 4 5
样例输出 50
数据范围与约定: 对于前10%的数据:n=2 , Q=20 对于另外10%的数据:n=3 , Q=12 对于前50%的数据:n<=20
,Q<=100 对于100%的数据: 1<=n<=100 , Q<=10000 , P<=2^30 ,
对于任意一个点对(u,v),u到v的道路数不超过2^30 。时间限制: 测试点1-5: 2 s 测试点6-10:4 s
首先,两点间路径条数显然是矩阵乘法。
但是这题需要对每个t都求一遍,每次乘法是\(O(n^3)\)的,如果暴力是\(O(Qn^3)\)的,显然超时。
但是,其实我们只需要主对角线上的元素。
如果两个矩阵相乘,就可以\(O(n^2)\)时间算出对角线。
三个及以上则不行。
所以,如果能把1~Q的每个数都表示成两个数的和,就能优化了。
显然,使用BSGS的表示即可。
例如Q=25时,
预处理出如下次幂:
0,1,2,3,4,5,10,15,20,25。
然后,就能把每个幂转化为两个已知矩阵的积的形式。
时间复杂度:预处理:\(O(n^3\sqrt{q})\)
查询:\(O(qn^2)\),总共\(O(n^3\sqrt{q}+qn^2)\)
代码:
#include <stdio.h>
#include <math.h>
#define ll unsigned long long
#define ma 10000000000000000000ull
int mi1[102][102][102],mi2[102][102][102],n,p,sl[102][102];
void fuz(int a[102][102],int b[102][102])
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
a[i][j]=b[i][j];
}
}
void chf(int a[102][102],int b[102][102],int c[102][102])
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
ll t=0;
for(int k=0;k<n;k++)
{
t+=(ll)a[i][k]*b[k][j];
if(t>ma)
t%=p;
}
c[i][j]=t%p;
}
}
}
void djx(int a[102][102],int b[102][102],int c[102][102])
{
for(int i=0;i<n;i++)
{
int j=i;
ll t=0;
for(int k=0;k<n;k++)
{
t+=(ll)a[i][k]*b[k][j];
if(t>ma)
t%=p;
}
c[i][j]=t%p;
}
}
int main()
{
freopen("dessert.in","r",stdin);
freopen("dessert.out","w",stdout);
int q;
scanf("%d%d%d",&n,&q,&p);
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
scanf("%d",&sl[i][j]);
}
for(int i=0;i<n;i++)
mi1[0][i][i]=mi2[0][i][i]=1;
int sq=int(sqrt(q)+0.5);
int sq2=q/sq;
if(sq2*sq<q)
sq2+=1;
for(int i=1;i<=sq2;i++)
chf(mi2[i-1],sl,mi2[i]);
for(int i=1;i<=sq;i++)
chf(mi1[i-1],mi2[sq2],mi1[i]);
int t[102][102];
int jg=0;
for(int i=1;i<=q;i++)
{
djx(mi1[i/sq2],mi2[i%sq2],t);
for(int j=0;j<n;j++)
jg=(jg^t[j][j]);
}
printf("%d",jg);
return 0;
}
7、 求解类似“l~r区间内出现过的数”的问题的方法
- 求出每个数的后继(在它后面第一个与它相同的数的位置,没有为inf)。
- 由于每个数只算一次,所以只算后继>r的数。
- 于是,问题就转化为处理l~r区间内,>r的数。离线:树状数组/线段树(将所有数排序,询问排序,扫描求解)。
在线:主席树。
例题1:HH的项链
离线处理。
#include <stdio.h>
#include <stdlib.h>
int sz[500010],la[1000010];
struct SHj
{
int z,i;
};
SHj hj[500010];
int cmp1(const void*a,const void*b)
{
return ((SHj*)a)->z-((SHj*)b)->z;
}
struct SCx
{
int L,R,i;
};
SCx cx[200010];
int cmp2(const void*a,const void*b)
{
return ((SCx*)a)->L-((SCx*)b)->L;
}
int C[500010],N,jg[200010];
void add(int i,int x)
{
for(int j=i;j<=N;j+=(j&(-j)))
C[j]+=x;
}
int sum(int i)
{
int jg=0;
for(int j=i;j>0;j-=(j&(-j)))
jg+=C[j];
return jg;
}
int main()
{
int M;
scanf("%d",&N);
for(int i=1;i<=N;i++)
scanf("%d",&sz[i]);
for(int i=1;i<=N;i++)
{
hj[i-1].z=la[sz[i]];
la[sz[i]]=i;
hj[i-1].i=i;
}
qsort(hj,N,sizeof(SHj),cmp1);
scanf("%d",&M);
for(int i=0;i<M;i++)
{
scanf("%d%d",&cx[i].L,&cx[i].R);
cx[i].i=i;
}
qsort(cx,M,sizeof(SCx),cmp2);
int t=0;
for(int i=0;i<M;i++)
{
if(i==0||cx[i].L!=cx[i-1].L)
{
while(t<N&&hj[t].z<cx[i].L)
{
add(hj[t].i,1);
t+=1;
}
}
jg[cx[i].i]=sum(cx[i].R)-sum(cx[i].L-1);
}
for(int i=0;i<M;i++)
printf("%d\n",jg[i]);
return 0;
}
例题2:bzoj4026
根据欧拉函数的求法,求出区间内出现过的质因数即可。
强制在线,主席树。
代码:
#include <stdio.h>
#define ll long long
ll ji[14700010],md=1000777,ny[1000777],hj[50010];
int cl[14700010],cr[14700010],sl=0;
void pushup(int i)
{
ji[i]=(ji[cl[i]]*ji[cr[i]])%md;
}
int jianshu(int l,int r)
{
ji[sl]=1;
if(l+1==r)
return sl++;
int rt=sl++,m=(l+r)>>1;
cl[rt]=jianshu(l,m);
cr[rt]=jianshu(m,r);
return rt;
}
int xiugai(int i,int l,int r,int j,ll x)
{
if(l+1==r)
{
ji[sl]=(ji[i]*x)%md;
return sl++;
}
int rt=sl++,m=(l+r)>>1;
cl[rt]=cl[i],cr[rt]=cr[i];
if(j<m)
cl[rt]=xiugai(cl[rt],l,m,j,x);
else
cr[rt]=xiugai(cr[rt],m,r,j,x);
pushup(rt);
return rt;
}
ll chaxun(int i,int l,int r,int L,int R)
{
if(r<=L||R<=l)
return 1;
if(L<=l&&r<=R)
return ji[i];
int m=(l+r)>>1;
return (chaxun(cl[i],l,m,L,R)*chaxun(cr[i],m,r,L,R))%md;
}
void ycl()
{
ny[1]=1;
for(int i=2;i<md;i++)
ny[i]=((md-md/i)*ny[md%i])%md;
}
int sz[350010],st[50010],en[50010],gs=1;
int ne[350010],la[1000010],gen[350010];
void fenj(int u,int x)
{
st[u]=gs-1;
for(int i=2;i*i<=x;i++)
{
if(x%i==0)
{
sz[gs++]=i;
while(x%i==0)
x/=i;
}
}
if(x!=1)
sz[gs++]=x;
en[u]=gs-1;
}
int main()
{
ycl();
int N,Q;
scanf("%d%d",&N,&Q);
for(int i=0;i<N;i++)
{
int x;
scanf("%d",&x);
fenj(i,x);
if(i==0)
hj[i]=x;
else
hj[i]=(x*hj[i-1])%md;
}
for(int i=0;i<=1000000;i++)
la[i]=gs+1;
for(int i=gs;i>0;i--)
{
ne[i]=la[sz[i]];
la[sz[i]]=i;
}
gen[0]=jianshu(1,gs+2);
for(int i=1;i<=gs;i++)
gen[i]=xiugai(gen[i-1],1,gs+2,ne[i],(ny[sz[i]]*(sz[i]-1))%md);
int lan=0;
for(int i=0;i<Q;i++)
{
int l,r;
scanf("%d%d",&l,&r);
l=(l^lan)-1;
r=(r^lan)-1;
ll j=hj[r];
if(l>0)
j=(j*ny[hj[l-1]])%md;
j=(j*chaxun(gen[en[r]],1,gs+2,en[r]+1,gs+2))%md;
j=(j*ny[chaxun(gen[st[l]],1,gs+2,en[r]+1,gs+2)])%md;
lan=j;
printf("%d\n",lan);
}
return 0;
}
8、用两种方式表达计数DP,列方程求解
例题:
小 D 正在研究一场锦标赛。
这场锦标赛共有 n 位选手参加,他们被依次编号为 1, 2, · · · , n。
选手两两之间会进行一场比赛,在两名选手比赛时,编号较小的那个有 p 的概率
取胜,而较大的那个有 \(1 − p\) 的概率取胜。
换言之,对于选手 i, j(i < j)之间的比赛,
有 p 的概率是选手 i 获胜,而剩余的概率是选手 j 获胜。
最终,锦标赛主办方会给其中若干选手颁奖。为了显得公平公正,主办方需要保
证这些人胜过了其余的所有人。形式化地,若主办方给集合 S 中的选手颁奖,则主办
方需要保证对于任意 \(i ∈ S\) 和 \(j 不∈ S,在 i 和 j 的比赛中是 i 胜出。
小 D 想要知道,对于每个\) 1 \leq k < n$,最终主办方可以给 k 个选手颁奖的概率是
多少。
题解:
首先,使用枚举子集并记录大小的DP,转移时就能知道编号的大小关系。
设 F n,k 表示 n 个人中存在这样 k 个人的概率,考虑转移。
考虑如何计算 F n+1,k ,如果 n + 1 在集合外,那么他一定要输给前面的这 k 个人,
而这 k 个人的编号都比他小,因此概率为 \(p^k\) 。如果他在集合内,那么他要赢前面的
n − k + 1 个人,因此概率为 \(q^{n−k+1}\) ,其中 q = 1 − p。
因此,我们有F n+1,k = F n,k · \(p^k\) + F n,k−1 · \(q^{n−k+1}\)
同时,我们也可以通过考虑 1 这个人来做出转移。如果 1 在集合外,那么他要输
给后面的这 k 个人,而这 k 个人的编号都比他大,因此概率为 \(q^k\) 。如果他在集合内,
那么他要赢后面的 n − k + 1 个人,因此概率为 \(p^{n−k+1}\) 。
因此,我们又有 F n+1,k = F n,k · \(q^k\) + F n,k−1 · \(p^{n−k+1}\)
这样,我们就找到了一条等量关系: F n,k · \(q^k\) + F n,k−1 · \(p^{n−k+1}\) = F n,k · \(p^k\) + F n,k−1 · \(q^{n−k+1}\) 。
此外 F n,0 = 1,因此我们可以在 Θ(n) 的时间内算出所有的 F n,k ,从而得到答案。
代码略。