计数 DP 学习笔记
比较纯粹且富有教益的计数 DP 归在此处。
I.[AHOI2018初中组]球球的排列
论DP的百种用法之一
因为DP必须有一种全面的状态,但是这道题……似乎排列等等问题都不是DP擅长处理的地方。
首先分析性质。我们发现,这种不能放在一起的关系具有传递性。因为如果xy=a2,xz=b2,那么yz=(xy)(yz)x2=a2b2x2=(abx)2。
具有传递性的话,我们就会发现,所有不能放在一起的位置,构成了多个团(完全图)。
我们就想着把每个团里的所有球都染上同一种颜色,则相同颜色的球不能紧贴在一起。
则我们现在将问题转换为:给你n个染了色的球,相同的球不能放一起,求排列数。
考虑将这些球按照颜色排序,这样便有了一个合理的(可以抽象出状态的)DP顺序。
我们设f[i][j][k]表示:
当前DP到第i位,
有两个球放在一起,它们的颜色相同,并且颜色与第i位的球不同,这样的对共有j个,
有两个球放在一起,它们的颜色相同,并且颜色与第i位的球相同,这样的对共有k个,
的方案数。
因为我们已经排过序,因此颜色相同的球必定紧贴。
1.第 i 位的球和第 i−1 位的球颜色不同。
则DP状态的第三维(即k)必为0,因为不存在在它之前并且和它颜色相同的球。我们只需要枚举第二维j。
1.1.我们将这个球放在两个颜色不同的球之间。
我们枚举一个k′,表示上一个球所代表的颜色中颜色相同且紧贴的对共有k′个(k′∈[0,j])。
则有f[i][j][0]+=f[i-1][j-k'][k']*(i-j),因为共有j-k'个相邻且相同且和上一个球的颜色不同的位置,并且共有i-j个可以放球的位置。
1.2.我们将这个球放在两个颜色相同的球之间。
我们仍然枚举一个k′,意义相同。这时,k′∈[0,j+1]。
则有f[i][j][0]+=f[i-1][j-k'+1][k']*(j+1)。因为放入这个球后就拆开了一对球,因此原来共有 j+1 对这样的球。
2.第 i 位的球和第 i−1 位的球颜色相同。
我们需要枚举剩余两维j,k。并且,设在第i位之前,有cnt个和第i位相同的位置。
2.1.我们将这个球放在某个和这个球颜色相同的球旁边。
则共有2∗cnt−(k−1)个这样的位置。
因此有f[i][j][k]+=f[i-1][j][k-1](2cnt-(k-1))。
2.2.我们将这个球放在两个颜色相同的球之间。
同1.2,有f[i][j][k]+=f[i-1][j+1][k]*(j+1)。
2.3.我们将这个球放在两个颜色不同且与这个球颜色不同的球之间。
这次操作没有添加或删除任何对,并且共有i−(2∗cnt−k)−j个位置。
因此有f[i][j][k]+=f[i-1][j][k](i-(cnt2-k)-j)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
int n,num[310],dsu[310],f[2][310][310];
vector
int find(int x){
return dsu[x]x?x:dsu[x]=find(dsu[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(xy)return;
dsu[x]=y;
}
bool che(ll ip){
ll tmp=sqrt(ip)+0.5;
return tmptmp==ip;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&num[i]),dsu[i]=i;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(che(1llnum[i]num[j]))merge(i,j);
for(int i=1;i<=n;i++)dsu[i]=find(i);
sort(dsu+1,dsu+n+1);
f[0][0][0]=1;
for(int i=1,cnt;i<=n;i++){
memset(f[i&1],0,sizeof(f[i&1]));
if(dsu[i]!=dsu[i-1]){
cnt=0;
for(int j=0;j<i;j++){
for(int k=0;k<=j;k++)f[i&1][j][0]=(1llf[!(i&1)][j-k][k](i-j)+f[i&1][j][0])%mod;//if we put it between two balls of different colours
for(int k=0;k<=j+1;k++)f[i&1][j][0]=(1llf[!(i&1)][j-k+1][k](j+1)+f[i&1][j][0])%mod;//if we put it between two balls of the same colours
}
}else{
for(int j=0;j<i;j++){
for(int k=1;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j][k-1](cnt2-(k-1))+f[i&1][j][k])%mod;//if we put it next to a ball of the same colour
for(int k=0;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j+1][k](j+1)+f[i&1][j][k])%mod;//if we put it between two balls of the same colours
for(int k=0;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j][k](i-(cnt*2-k)-j)+f[i&1][j][k])%mod;//if we put it between two balls of different colours
}
}
cnt++;
}
printf("%d\n",f[n&1][0][0]);
return 0;
}
II.[SCOI2008]着色方案
双倍经验,双倍快乐
可以看出这题直接是上一题的无编号版,直接套上一题的板子,乘上逆元的倒数直接水过,还轻轻松松完虐正解(五维暴力DP)
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
int n,m,num[310],dsu[310],f[2][310][310],res,fac[310];
int ksm(int x,int y){
int z=1;
for(;y;x=(1llxx)%mod,y>>=1)if(y&1)z=(1llxz)%mod;
return z;
}
int main(){
scanf("%d",&m);
for(int i=1,x;i<=m;i++){
scanf("%d",&num[i]);
for(int j=1;j<=num[i];j++)dsu[++n]=i;
}
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=(1llfac[i-1]i)%mod;
f[0][0][0]=1;
for(int i=1,cnt;i<=n;i++){
memset(f[i&1],0,sizeof(f[i&1]));
if(dsu[i]!=dsu[i-1]){
cnt=0;
for(int j=0;j<i;j++){
for(int k=0;k<=j;k++)f[i&1][j][0]=(1llf[!(i&1)][j-k][k](i-j)+f[i&1][j][0])%mod;//if we put it between two balls of different colours
for(int k=0;k<=j+1;k++)f[i&1][j][0]=(1llf[!(i&1)][j-k+1][k](j+1)+f[i&1][j][0])%mod;//if we put it between two balls of the same colours
}
}else{
for(int j=0;j<i;j++){
for(int k=1;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j][k-1](cnt2-(k-1))+f[i&1][j][k])%mod;//if we put it next to a ball of the same colour
for(int k=0;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j+1][k](j+1)+f[i&1][j][k])%mod;//if we put it between two balls of the same colours
for(int k=0;k<=cnt;k++)f[i&1][j][k]=(1llf[!(i&1)][j][k](i-(cnt2-k)-j)+f[i&1][j][k])%mod;//if we put it between two balls of different colours
}
}
cnt++;
}
res=f[n&1][0][0];
for(int i=1;i<=m;i++)res=(1llresksm(fac[num[i]],mod-2))%mod;
printf("%d\n",res);
return 0;
}
当然这么就水过一道题好像有点不好意思哈
因此额外再介绍一种方法,复杂度最劣应该是O(n3)的,其中n是木块数量。
我们设num[i]表示第i种颜色有多少个,sum[i]为前i种颜色有多少个。
因为这题无编号,我们可以考虑简化一维:设f[i][j]表示:
前i种颜色(!!!)
涂了前sum[i]块,
并且有j对相邻同色对的方案数。
我们采取刷表法进行DP。
考虑由f[i][j]推出f[i+1][?]。
我们枚举一个k∈[1,num[i+1]],表示我们将num[i+1]分成k段连续的同色木块。
我们再枚举一个l∈[0,min(j,k)],表示我们从这k段木块中,抽出l段木块塞在两段颜色相同但相邻的木块中。
首先,依据隔板法(小学奥数),共有Cnum[i+1]−1k−1中合法的分割方案;
一共有sum[i]−j+1个颜色不同的相邻位置,因此这j−l段放入颜色相同的位置的木块共有Csum[i]−j+1j−l种放法。
一共有j个颜色相同的相邻位置,共有Cjl种放法。
则总方案数为Cnum[i+1]−1k−1∗Csum[i]−j+1j−l∗Cjl∗f[i][j]。
等等,这一大坨是往哪里更新去的?
是往f[i+1][j+num[i+1]−k−l]更新的。原本应该增加num[i+1]−1段相邻的,现在聚合成了k段,减少了k−1段;有l段放偏了,又减少了l段。
代码(压 行 带 师):
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int f[20][100],n,num[20],sum[20],C[100][100];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&num[i]),sum[i]=sum[i-1]+num[i];
for(int i=0;i<=sum[n];i++)C[i][0]=1;
for(int i=1;i<=sum[n];i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
f[1][num[1]-1]=1;
for(int i=1;i<n;i++)for(int j=0;j<sum[i];j++)for(int k=1;k<=num[i+1];k++)for(int l=0;l<=min(k,j);l++)f[i+1][j-k+num[i+1]-l]=(1llf[i][j]C[num[i+1]-1][k-1]%modC[sum[i]-j+1][k-l]%modC[j][l]%mod+f[i+1][j-k+num[i+1]-l])%mod;
printf("%d\n",f[n][0]);
return 0;
}
III.[HAOI2011]Problem c
这题还是挺简单的~~~
关于每个位置i,在一种合法的方案 a 中,必有
(∑j=1n[aj≥i])≤n−i+1。
因为,每一个aj≥i都会占据i以后的某个位置,而i后面共有n−i+1个位置,因此这是充分必要条件。
因此我们发现,这个入座的顺序对答案并无影响——因为上面的判别式并没有对下标的操作。因此,对于那贿赂上司的m个人,我们只需要关注那个qi即可。
我们设numi=(n−i+1)−∑j=1m[qj≥i],即先减去已经确定的部分。
然后,我们从后往前确认每个位置填什么。
设f[i][j]表示:
后i个位置,
还有j个a没有确定,
的方案数。
初始状态为f[n+1][n−m]=1,其它都为0。
则有f[i][j−k]=∑f[i+1][j]∗Cjk。
状态的含义:我们从i+1位置剩下的j个人中,挑选出k个人令他们的ax=i。因为顺序无关,所以是Cjk。
至于这个k,k∈[0,min(numi−(n−m−j),j)],因为在位置i前面已经填入的(n−m−j)个人也会对i造成影响。
我们最后要判断f[1][0]是否被更新过,而不是判断它是否非0,因为可能会出现答案是模数倍数的情况。
代码:
include<bits/stdc++.h>
using namespace std;
int n,m,T,mod,num[310],f[310][310],C[310][310];
bool upd[310][310];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d%d",&n,&m,&mod),memset(f,0,sizeof(f)),memset(upd,0,sizeof(upd)),memset(C,0,sizeof(C)),memset(num,0,sizeof(num));
for(int i=1;i<=n;i++)num[i]=n-i+1;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
for(int j=1;j<=y;j++)num[j]--;
}
f[n+1][n-m]=upd[n+1][n-m]=1;
for(int i=n;i>=1;i--)for(int j=0;j<=n-m;j++){
if(num[i]-((n-m)-j)<0)continue;
for(int k=0;k<=min(num[i]-((n-m)-j),j);k++)upd[i][j-k]|=upd[i+1][j],f[i][j-k]=(1llf[i+1][j]C[j][k]+f[i][j-k])%mod;
}
if(!upd[1][0])puts("NO");
else printf("YES %d\n",f[1][0]);
}
return 0;
}
IV.[SHOI2012]随机树
q=1:
考虑令fi表示:一棵有i个叶节点的树,叶节点平均深度的期望值。
则fi=fi−1+2i。
证明:
我们随便从i−1个叶子中选一个出来,展开它,
则这次展开期望能为叶子的深度和增加2∗(fi−1+1)−fi−1。
但是还要重新取平均;
于是
fi=fi−1∗(i−1)+2∗(fi−1+1)−fi−1i
化简就得到了
fi=fi−1+2i
q=2:
考虑令f[i][j]表示:
一棵有i个叶节点的树,深度≥j的概率。
显然,有f[i][j]=∑k=1i−1f[k][j−1]+f[i−k][j−1]−f[k][j−1]∗f[i−k][j−1]?
释义:我们枚举左子树中放进去k个子节点。则左/右节点至少有一个深度≥j−1的状态都是合法的。但是,左右节点深度都≥j−1的情况在两个中都被算进去了;因此需要减去这种可能。
这个分母上的?,就是出现这种左右子树size分配的可能性。
考虑这种可能性的大小。
我们设gi表示构成一棵有i个叶节点的树的方案数。则有gi=(i−1)!,因为每“扩展”一个点就相当于从i个叶子中选了一个叶子出来,有i种选法;则有gi=∏j=1i−1j=(i−1)!。
显然,这种可能性应该等于gk∗gi−k∗?(这个?是一个新的?)。我们将两棵子树合并,首先两棵子树自己内部扩展的顺序已经被g决定了;但是合并的顺序可是可以随便指定的;合并顺序的种数等于Ci−2k−1,因为i个叶节点就意味着i−2次合并(当前节点自己本身就占用一次合并),这i−2次合并选出k−1次合并在左子树上。
则可能性的大小为gk∗gi−k∗Ci−2k−1=(k−1)!∗(i−k−1)!∗(i−2)!(k−1)!∗(i−2−k+1)!=(i−2)!
然后分母上的?就是gi(i−2)!=(i−1)!(i−2)!=(i−1)。
于是我们现在有
f[i][j]=∑k=1i−1f[k][j−1]+f[i−k][j−1]−f[k][j−1]∗f[i−k][j−1]i−1
有一个式子:
E(x)=∑i=1∞P(i)
其中E(x)表示x的期望,P(i)表示i≤x的概率。
证明:
设p(i)表示i=x的概率,P(i)表示i≤x的概率,
则E(x)=∑i=1∞p(i)∗i
而P(i)=∑j=i∞p(j)
则
∑i=1∞P(i)=∑i=1∞∑j=i∞p(j)=∑i=1∞p(i)∗i=E(x)
证毕。
依据此式,则答案为∑i=1n−1f[n][i]。
代码:
include<bits/stdc++.h>
using namespace std;
int n,m;
namespace T1{
double f[110];
double work(){
f[1]=0;
for(int i=2;i<=n;i++)f[i]=f[i-1]+2.0/i;
return f[n];
}
}
namespace T2{
double f[110][110];
double work(){
for(int i=1;i<=n;i++)f[i][0]=1;
for(int i=2;i<=n;i++)for(int j=1;j<i;j++){
for(int k=1;k<i;k++)f[i][j]+=f[i-k][j-1]+f[k][j-1]-f[i-k][j-1]*f[k][j-1];
f[i][j]/=i-1;
}
double res=0;
for(int i=1;i<n;i++)res+=f[n][i];
return res;
}
}
int main(){
scanf("%d%d",&m,&n);
if(m1)printf("%lf\n",T1::work());
if(m2)printf("%lf\n",T2::work());
return 0;
}
V.CF285E Positions in Permutations
神题orz……
我也是第一次听说有个叫二项式反演的神奇东西……
它具体有两个形式:
F(n)=∑i=0n(−1)i(ni)G(i)⇔G(n)=∑i=0n(−1)i(ni)F(i)
F(n)=∑i=0n(ni)G(i)⇔G(n)=∑i=0n(−1)n−i(ni)F(i)
F(n)=∑i=n?(−1)i(in)G(i)⇔G(n)=∑i=n?(−1)i(in)F(i)
F(n)=∑i=n?(in)G(i)⇔G(n)=∑i=n?(−1)i−n(in)F(i)
这题可以考虑设G(i)表示“完美数”恰好为i的方案数,再设F(i)表示“完美数”≥i的方案数。
肯定有F(m)=∑i=mn?×G(i),其中?是某个系数。
则对于G(i)中的某种方案,我们需要从i个位置中挑出m个位置,然后只观察这m个位置而忽略其它地方。显然,共有(im)种方法。
因此有F(m)=∑i=mn(im)G(i)。
套用4,得到G(m)=∑i=mn(−1)i−m(im)F(i)。
考虑DP求F。
我们设f[i][j][k=0/1][l=0/1]表示:
前i位,有j个完美数,并且数字i选没选的状态是k,数字i+1选没选的状态是l的方案数。
需要注意的是,我们只注意完美的位置,至于其它位置填什么吗……最后阶乘一下。
因此有:
第i位是完美位
1.1. 填入i−1
则有
f[i][j][0][0]+=f[i−1][j−1][0][0]
f[i][j][1][0]+=f[i−1][j−1][0][1]
1.2.填入i+1
则有
f[i][j][0][1]+=f[i−1][j−1][0][0]+f[i−1][j−1][1][0]
f[i][j][1][1]+=f[i−1][j−1][0][1]+f[i−1][j−1][1][1]
第i位空置
则有
f[i][j][0][0]+=f[i−1][j][0][0]+f[i−1][j][1][0]
f[i][j][1][0]+=f[i−1][j][0][1]+f[i−1][j][1][1]
然后特殊转移:
1.第1位:
1.1.空置:f[1][0][0][0]=1
1.2.放i+1:f[1][1][0][1]=1
(注意,这里不需要特别讨论放i的情况——这就是为什么F(i)的定义是≥i的方案数)
2.第n位
废去1.2.填入i+1的方案即可。
最终有F(i)=(n−i)!(f[n][i][0][0]+f[n][i][1][0])
因为除了完美位外其它的位置都是可以阶乘随便填的。
然后套我们之前的式子,
G(m)=∑i=mn(−1)i−m(im)F(i)
即可。
(如果要求所有G(m)可以直接FFT卷积,不过这题不需要罢了)
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[1010][1010][2][2],fac[1010],inv[1010],F[1010],res;
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=(1llxx)%mod)if(y&1)z=(1llzx)%mod;
return z;
}
int C(int x,int y){
return 1llfac[x]inv[y]%modinv[x-y]%mod;
}
int main(){
scanf("%d%d",&n,&m),f[1][0][0][0]=f[1][1][0][1]=1;
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=(1llfac[i-1]i)%mod;
inv[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>=0;i--)inv[i]=(1llinv[i+1](i+1))%mod;
for(int i=2;i<=n;i++)for(int j=0;j<=i;j++){
if(j){
f[i][j][0][0]=f[i-1][j-1][0][0];
f[i][j][1][0]=f[i-1][j-1][0][1];
if(i<n)f[i][j][0][1]=(f[i-1][j-1][0][0]+f[i-1][j-1][1][0])%mod;
if(i<n)f[i][j][1][1]=(f[i-1][j-1][0][1]+f[i-1][j-1][1][1])%mod;
}
f[i][j][0][0]=(0ll+f[i][j][0][0]+f[i-1][j][0][0]+f[i-1][j][1][0])%mod;
f[i][j][1][0]=(0ll+f[i][j][1][0]+f[i-1][j][0][1]+f[i-1][j][1][1])%mod;
}
for(int i=0;i<=n;i++)F[i]=1llfac[n-i](f[n][i][0][0]+f[n][i][1][0])%mod;
for(int i=m;i<=n;i++)(res+=(((i-m)&1?-1ll:1ll)(1llC(i,m)F[i]%mod)+mod)%mod)%=mod;
printf("%d\n",res);
return 0;
}
VI.CF559C Gerald and Giant Chess
DP只要一与排列组合或是容斥等等东西结合在一起就会变得极其毒瘤……
我们设fi表示:走到第i个黑格子上,且之前没有走到任何一个黑格子时的方案数。
则我们如果将棋盘的右下角看作是第n+1个黑格子,fn+1就是答案。
我们将黑格子按照行优先,如果行相同列优先的顺序排序,这样就明确了DP顺序。
则我们有fi=CXi+YiXi−∑i=1i−1fj∗C(Xi−Xj)+(Yi−Yj)Xi−Xj。
代码:
include<bits/stdc++.h>
using namespace std;
const int N=2e5;
const int mod=1e9+7;
int n,fac[200100],inv[200100],f[2010];
pair<int,int>p[2010];
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=(1llxx)%mod)if(y&1)z=(1llzx)%mod;
return z;
}
int C(int x,int y){
return 1llfac[x]inv[y]%modinv[x-y]%mod;
}
int main(){
fac[0]=1;
for(int i=1;i<=N;i++)fac[i]=(1llfac[i-1]i)%mod;
inv[N]=ksm(fac[N],mod-2);
for(int i=N-1;i>=0;i--)inv[i]=(1llinv[i+1](i+1))%mod;
scanf("%d%d%d",&p[1].first,&p[1].second,&n),n++,p[1].first--,p[1].second--;
for(int i=2;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second),p[i].first--,p[i].second--;
sort(p+1,p+n+1);
for(int i=1;i<=n;i++){
f[i]=C(p[i].first+p[i].second,p[i].first);
for(int j=1;j<i;j++)if(p[j].first<=p[i].first&&p[j].second<=p[i].second)f[i]=(f[i]-1llf[j]*C(p[i].first-p[j].first+p[i].second-p[j].second,p[i].first-p[j].first)%mod+mod)%mod;
}
printf("%d\n",f[n]);
return 0;
}
VII.[AGC013D] Piling Up
一个很naive的思路就是设f[i][j]表示当前进行了i步,并且盒子中剩下了j个白球的方案数,然后直接DP即可。
但是这样是有问题的——它没有考虑到重复计算的问题。
我们不妨令+符号表示取出黑球,−符号表示取出白球。
则一种方式是6→+−6→−−5,其中数字表示剩余白球数。
另一种方式是4→+−4→−−3。很明显,两者即使盒中球数不同,但是序列是相同的。
为了避免重复计算,我们可以强制要求只有过程中出现过0的序列才是合法序列。
于是我们可以设f[i][j][0/1]表示进行i步,盒子中剩下j个白球,且(没有/有)到过0的方案数。则答案即为∑i=0nf[m][i][1]。
要注意的是,这里的转移过程必须保证任意时刻球的数量必须在[0,n]范围之内,因此对于不合法的状态要记得特判掉。
复杂度O(nm)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[3010][3010][2],res;//0: haven't reached 0; 1:have reached 0
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)f[0][i][i0]=1;
for(int i=0;i<m;i++)for(int j=0;j<=n;j++){
if(j)(f[i+1][j][j1]+=f[i][j][0])%=mod,(f[i+1][j][1]+=f[i][j][1])%=mod;//-+
if(j)(f[i+1][j-1][j==1]+=f[i][j][0])%=mod,(f[i+1][j-1][1]+=f[i][j][1])%=mod;//--;
if(j<n)(f[i+1][j][0]+=f[i][j][0])%=mod,(f[i+1][j][1]+=f[i][j][1])%=mod;//+-;
if(j<n)(f[i+1][j+1][0]+=f[i][j][0])%=mod,(f[i+1][j+1][1]+=f[i][j][1])%=mod;//++;
}
for(int i=0;i<=n;i++)(res+=f[m][i][1])%=mod;
printf("%d\n",res);
return 0;
}
VIII.[AGC024E] Sequence Growing Hard
首先,我们肯定能想到从第一个序列开始,依次加入一个新数得到下一个序列,同时还要保证字典序递增。我们如果让新数递增的话,就可以DP了。
我们首先观察往一个序列中加入一个不大于最大值的数会有多少种可能:
我们在1323中加入一个3,
位置 结果
开头 31323
第一个数后 13323
第二个数后 13323
第三个数后 13233
第四个数后 13233
明显所有结果全都符合要求,但是有重复计算的地方。
我们可以强制要求加数必须加在连续一段相同的数的后面,在上例中就是你无法在第一个、第三个数后面添加3。
我们可以设f[i][j][k]表示当前处理完成了前i个串,计划往第i+1个串里加入一个数j,并且有k个位置可以加入j的方案数。
则f[i][j][k]可以转移到:
如果k>0,可以转移到f[i][j][k−1],它的意义是我们跳过第k个位置不加。
如果k=0,可以转移到f[i][j+1][i],它的意义是第j个数已经全部加完,可以尝试j+1了。它有i个位置可以填,因为没有任何一个数与j+1相同,它可以直接加到任何数后面。
无论何时,都可以转移到f[i+1][j][k],意义是我们在第k个位置加入一个数。这共有k+1种加法,因为我们还有一种在开头的加法是一直可以的。
复杂度O(n3)。
代码:
include<bits/stdc++.h>
using namespace std;
int n,m,p,f[310][310][310];
//f[i][j][k]:we've finished constructing the first i sequences, now we're going to add the number j into the i+1-th sqeuence, and there're k places to add j into
int main(){
scanf("%d%d%d",&n,&m,&p),f[0][1][0]=1;
for(int i=0;i<=n;i++)for(int j=1;j<=m;j++)for(int k=i;k>=0;k--){
if(k)(f[i][j][k-1]+=f[i][j][k])%=p;//we decide not to add j to the k-th place, so we could add it to the (k-1)-th place.
else(f[i][j+1][i]+=f[i][j][k])%=p;//we have tried every place j could be added to, now it's time to try j+1, which could be added into any place
(f[i+1][j][k]+=1llf[i][j][k](k+1)%p)%=p;//we decide to add j to the k-th place, and there are (k+1) places for us to add (including the last one)
}
printf("%d\n",f[n][m+1][n]);//all n sequences've been constructed, and all number've been tried
return 0;
}
IX.[SHOI2009]舞会
之前一直在往二项式反演去想,没想到最后居然成了……
我们考虑将男生和女生全部按照高度递减排序,则对于第i个男生,能与他构成特殊对的女生必定是一个前缀,设前缀长度为numi。显然,numi是单调不降的。
然后,我们考虑设fi表示钦定i对匹配,其余随意的方案数。则fi作一个二项式反演即可得到恰好匹配i对的方案数,再做一个前缀和就是匹配至多i对的方案数。
我们考虑DP。设fi,j表示钦定前i个男生匹配j对特殊对的方案数,则我们要求的就是fn的数组。
考虑第i人。因为他要么匹配一对特殊对,共有 numi−j 种方案(前面的人匹配的目标当前的人也一定能匹配,所以就只剩numi−j对了),要么干脆就不是钦定的对。
于是就有fi−1,j×(numi−j)→fi,j+1且fi−1,j→fi,j。
然后,最终得到fn,i数组,再乘上一个(n−i)!就是我们一开始提出的f数组,因为剩下的位置没有被钦定,可以随便排。
套上高精度,时间复杂度
高
精
度
复
杂
度
O(n2×高精度复杂度)。
代码:
include<bits/stdc++.h>
using namespace std;
int n,m,a[310],b[310],num[310];
struct Wint:vector
Wint(){clear();}
Wint(int x){clear();while(x)push_back(x%10),x/=10;}
void operator ~(){
for(int i=0;i+1<size();i++){
(this)[i+1]+=(this)[i]/10,(this)[i]%=10;
while((this)[i]<0)(this)[i]+=10,(this)[i+1]--;
}
while(!empty()&&back()>9){
int tmp=back()/10;
back()%=10;
push_back(tmp);
}
while(!empty()&&!back())pop_back();
}
void operator +=(const Wint &x){
if(size()<x.size())resize(x.size());
for(int i=0;i<x.size();i++)(this)[i]+=x[i];
~this;
}
void operator -=(const Wint &x){
for(int i=0;i<x.size();i++)(this)[i]-=x[i];
~this;
}
friend Wint operator +(Wint x,const Wint &y){
x+=y;
return x;
}
friend Wint operator (const Wint &x,const Wint &y){
Wint z;
if(!x.size()||!y.size())return z;
z.resize(x.size()+y.size()-1);
for(int i=0;i<x.size();i++)for(int j=0;j<y.size();j++)z[i+j]+=x[i]y[j];
~z;
return z;
}
void print()const{
if(empty()){putchar('0');return;}
for(int i=size()-1;i>=0;i--)putchar('0'+(this)[i]);
}
}C[310][310],f[310][310],fac[310];
signed main(){
scanf("%d%d",&n,&m);
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]i;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
sort(a+1,a+n+1),reverse(a+1,a+n+1);
for(int i=1;i<=n;i++)scanf("%d",&b[i]);
sort(b+1,b+n+1),reverse(b+1,b+n+1);
for(int i=1,j=1;i<=n;i++){
while(j<=n&&b[j]>a[i])j++;
num[i]=j-1;
}
f[0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=i;j++){
if(j<=num[i+1])f[i+1][j+1]+=f[i][j](num[i+1]-j);
f[i+1][j]+=f[i][j];
}
for(int i=0;i<=n;i++)f[n][i]=f[n][i]fac[n-i];
for(int i=n;i>=0;i--)for(int j=i+1;j<=n;j++)f[n][i]-=C[j][i]*f[n][j];
// for(int i=0;i<=n;i++)printf("%d ",f[n][i]);puts("");
for(int i=1;i<=n;i++)f[n][i]+=f[n][i-1];
f[n][m].print();
return 0;
}
X.CF917D Stranger Trees
这里是本题的DP解法。矩阵树定理解法详见矩阵树定理学习笔记中重题III.TopCoder13369-TreeDistance。
首先,一个基础结论是,如果一张 n 个点的图,被连成一棵森林,则继续加边连成一棵树的方案数是 nk−2∏i=1kci,其中森林中共有 k 棵树,第 i 棵树的大小是 ci。
然后,考虑二项式反演。设 gi 是至多有 i 条边在原树上的方案数。二项式反演一下即可得到恰好有 i 条边在原树上的方案数。则问题被转化为求出 g 数组。g 数组中 nk−2 的部分是可以提出来统一计算的。考虑使用DP来求出 g 的另一部分。设 fi,j,k 表示 i 的子树中被截成 j 棵树,且 i 节点自身所在树大小为 k 的权值(不包括 i 自身的树)和。合并节点 i 和儿子 i′ 时,只需对二者处于同一连通块或是不同连通块进行树上背包分开转移即可。
时间复杂度 O(n4),是树上背包复杂度。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[110][110][110],g[110],sz[110],h[110][110],fac[110],inv[110];
vector
int ksm(int x,int y=mod-2){
int z=1;
for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;
return z;
}
void dfs(int x,int fa){
sz[x]=1,f[x][1][1]=1;
for(auto y:v[x]){
if(y==fa)continue;
dfs(y,x);
for(int i=1;i<=sz[x];i++)for(int I=1;I<=sz[y];I++)for(int j=1;j<=sz[x];j++)for(int J=1;J<=sz[y];J++){
(h[i+I][j]+=1llf[x][i][j]f[y][I][J]%modJ%mod)%=mod;
(h[i+I-1][j+J]+=1llf[x][i][j]f[y][I][J]%mod)%=mod;
}
for(int i=1;i<=sz[x]+sz[y];i++)for(int j=1;j<=sz[x]+sz[y];j++)f[x][i][j]=h[i][j],h[i][j]=0;
sz[x]+=sz[y];
}
}
int C(int x,int y){return 1llfac[x]inv[y]%modinv[x-y]%mod;}
int main(){
scanf("%d",&n);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n-1;i>=0;i--)inv[i]=1llinv[i+1](i+1)%mod;
dfs(1,0);
for(int i=0,k=ksm(n);i<n;i++,k=1llkn%mod){
for(int j=1;j<=n;j++)(g[i]+=1llf[1][i+1][j]j%mod)%=mod;
g[i]=1llg[i]k%mod;
}
for(int i=0;i<n;i++)for(int j=0;j<i;j++)(g[i]+=mod-1llg[j]C(n-j-1,i-j)%mod)%=mod;
for(int i=0;i<n;i++)printf("%d ",g[n-i-1]);
return 0;
}
XI.[Topcoder16346]TwoPerLine
跟一年半以前就刷过的经典老题[AHOI2009]中国象棋完全一致,道理非常simple,设 fi,j,k 表示DP到第 i 列,其中有 j 行内恰有 2 枚棋,k 行里恰有 1 枚棋,然后就依据第 i 列里填的东西填到哪进行转移即可。复杂度 O(n3)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
class TwoPerLine{
private:
int f[210][210][210];
void add(int &x,int y){x+=y;if(x>=mod)x-=mod;}
int sqr(int x){return (1llx(x-1)/2)%mod;}
public:
int count(int n,int m){
f[0][0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=n;j++)for(int k=0;j+k<=n;k++)if(j2+k<=m){
add(f[i+1][j][k],f[i][j][k]);//put nothing on this colomn
if(k)add(f[i+1][j+1][k-1],1llf[i][j][k]k%mod);//put one chess on an 1-row
if(k>=2)add(f[i+1][j+2][k-2],1llf[i][j][k]sqr(k)%mod);//put both chesses on 1-rows
if(j+k+1<=n)add(f[i+1][j][k+1],1llf[i][j][k](n-j-k)%mod);//put one chess on a 0-row
if(j+k+2<=n)add(f[i+1][j][k+2],1llf[i][j][k]sqr(n-j-k)%mod);//put both chesses on 0-rows
if(k&&j+k+1<=n)add(f[i+1][j+1][k],1llf[i][j][k]k%mod(n-j-k)%mod);//put one chess on an 1-row, the other on a 0-row
}
int res=0;
for(int j=0;j<=n;j++)for(int k=0;j+k<=n;k++)if(j2+k==m)add(res,f[n][j][k]);
return res;
}
}my;
/
int main(){
// printf("%d\n",my.count(3,6));
// printf("%d\n",my.count(7,0));
// printf("%d\n",my.count(100,3));
// printf("%d\n",my.count(6,4));
return 0;
}
*/
XII.[AGC030F] Permutation and Minimum
看到 300 的数据范围,再加上计数题,很容易就往计数DP方向去想。
为方便,我们将 n 乘二。
因为是两个位置取 min,于是我们便想到从小往大把每个数填入序列。于是DP数组第一维的意义便出来了:当前已经填入了前 i 小个数。
考虑当前填入一个数。这个数有两种可能:一是与比它小的数匹配——此时最小值已经确定了,是那个更小的数,而更小数已经被填入序列,所以当前数与哪个比它小的数匹配根本不影响。二是与比它大的数匹配——此时最小值不确定,因此与不同的比它大的数在不同位置匹配会有不同结果。
于是我们便依次设出了剩余两维的意义:前 i 位中有 j 个东西是确定的(指与它配对的东西以及这一对数所在的位置都被确定,不论这确定的对是后来填出的还是初始序列中就已经给出的;若初始序列中只给出了一半的数,不算作此类),k 个东西是固定的(指其所在的位置固定,但与其配对的东西尚未被确定,显然此种情形下与其匹配的东西应是一个未在原序列中出现的东西),则剩余 i−j−k 个东西匹配了原序列中出现的东西,且该另一半必比 i 大。(注意这里我们把确定和固定这两个词黑体了,因为我们接下来还要多次使用它们)
我们考虑分情况从 fi,j,k 转移到 fi+1,j′,k′。
情形1. 若数 i+1 在序列中被给出了:
情形1.1. 若数 i+1 在序列中被给出了,且其配对也被给出了:
则此时显然其已被唯一确定,直接划归 j 类。故此种情形唯一可行转移是 fi,j,k→fi+1,j+1,k。
情形1.2. 若数 i+1 在序列中被给出,但其配对未被给出:
有两种情形:其与比它小的东西匹配,或者与比它大的东西匹配。
情形1.2.1. 与比它小的东西匹配。
则依照定义,其应与 i−j−k 中某个东西匹配,且与其中不同东西匹配有影响(因为匹配的另一半是此段的最小值)。匹配完后 j 类多出了两个。则转移是 fi,j,k×(i−j−k)→fi+1,j+2,k。
情形1.2.2. 与比它大的东西匹配。
则依照定义,其应划归 k 类,留待以后处理。故 fi,j,k→fi+1,j,k+1。
情形2. 若数 i+1 在序列中未被给出:
情形2.1. 作为 k 类中某个东西的另一半。
则此时与 k 类中哪个东西匹配根本不影响,因为每一对的最小值已经被确定了。因此若 k 非零,则有 fi,j,k→fi,j+2,k−1。
情形2.2. 作为 i−j−k 类。
则直接划归即可,因为方案数已在1.2.1中被计算。故 fi,j,k→fi+1,j,k。
情形2.3. 作为 k 类。
依照定义,k 类中元素的位置都是固定的。故 i+1 应被填到一个空余区间里。考虑计算此种空余区间的数量。
显然,总序列中,若我们设 suri 表示填入前 i 个数后序列中剩余的确定的数(即原序列中给出的已经确定好的数对),则目前一共有 j+suri 个数已经确定。k 类中已有的东西每个都占掉了 2 个格子,故还得减去 2k;再令 sigi 表示填入前 i 个数后剩余的固定的数(即原序列中给出的确定好一半的数对),显然其也各占去 2 个格子;又因为同一个区间里两个数的顺序无影响,所以还得除以 2。所以我们最终得到了空余区间的数量为 n−(j+suri)−2k−2sigi2。若设其为 spr,则有 fi,j,k×spr→fi+1,j,k+1。
显然转移 O(1);于是复杂度 O(n3) 解决。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,a[610],p[610],f[2][610][610],sig[610],mat[610],sur[610];
int main(){
scanf("%d",&n),n<<=1;
for(int i=1;i<=n;i++)mat[i]=((i-1)^1)+1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]),p[a[i]]=i;
for(int i=1;i<=n;i++)if(a[i]!=-1&&a[mat[i]]==-1)sig[a[i]-1]++;
for(int i=n;i>=0;i--)sig[i]+=sig[i+1];
for(int i=1;i<=n;i++)if(a[i]!=-1&&a[mat[i]]!=-1)sur[a[i]-1]++;
for(int i=n;i>=0;i--)sur[i]+=sur[i+1];
// for(int i=1;i<=n;i++)printf("%d ",sur[i]);puts("");
// for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
// for(int i=1;i<=n;i++)printf("%d ",p[i]);puts("");
f[0][0][0]=1;
for(int i=0;i<n;i++){
memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
for(int j=0;j<=i;j++)for(int k=0;j+k<=i;k++){
if(n-(j+sur[i])-k2-sig[i]2<0)continue;
if(!f[i&1][j][k])continue;
// if(j+k!=i)continue;
// printf("%d %d %d:%d\n",i,j,k,f[i&1][j][k]);
if(p[i+1]){
if(a[mat[p[i+1]]]!=-1){f[!(i&1)][j+1][k]=f[i&1][j][k];continue;}
(f[!(i&1)][j][k+1]+=f[i&1][j][k])%=mod;//we left i+1 unmatched, which should be considered as k.
// printf("I-J-K:%d\n",i-j-k);
(f[!(i&1)][j+2][k]+=1ll(i-j-k)f[i&1][j][k]%mod)%=mod;//we match something unsure position
}else{
if(k)(f[!(i&1)][j+2][k-1]+=f[i&1][j][k])%=mod;//we match something of sure position
(f[!(i&1)][j][k]+=f[i&1][j][k])%=mod;//match it with an element in [i+2,n]
(f[!(i&1)][j][k+1]+=1ll(n-(j+sur[i])-k2-sig[i]2)/2f[i&1][j][k]%mod)%=mod;//put it in any empty space.
}
}
}
printf("%d\n",f[n&1][n][0]);
return 0;
}
XIII.[NOI2019] 机器人
首先发现每个点向左向右能到达的位置就类似笛卡尔树上一个点的代表区间,不同的是这里有多个最大值时选取最右的一个。于是我们可以想到一个DP,f[i,j],k 表示区间 [i,j] 的最大值恰为 k 或不大于 k,两种设的方法均可。我们先以第一种方法为例。
我们有转移式 f[l,r],k=∑i which satisfy the constraintk∈[ai,bi](∑k′<kf[i+1,r],k′)。直接上即可拿到 35 分,而如果观察到因为单次转移合法的 i 数量是 O(1) 的,因而总会被访问到的状态数不会很多(准确地说, 2100 多一点),因此直接记忆化搜索处理即可拿到 50 分。
满分做法要猜一个结论:假如把所有 bi 加一,得到的就都是左闭右开的区间 [ai,bi);然后就可以把整个大区间拆分成数量为 O(n) 级别的小区间 [c1,c2),[c2,c3),…,[cm−1,cm),使得每个 [ai,bi) 都可以由几个小区间拼接得来。可以被证明的是,f[l,r],对于每个 i,在 k∈[ci,ci+1) 时,均是一个不超过 r−l 次的多项式。
证明也很简单。首先,对于 l=r 的情形,这是显然成立的,对于 [ci,ci+1)⊆[a,b) 的情形,所有的 f[l,r],k 均为 1,是常数;对于 [ci,ci+1)⊈[a,b) 的情形,均为 0,也是常数。然后尝试归纳。假设对于长度 ≤p 的区间皆成立,则考虑长度为 p+1 的区间 [l,r],其函数值是一堆 i 的函数和,因而只需证明对于上式中所有 i 得到的式子次数都不大于 r−l 即可。因为对于每个 i,后面的一大坨都是前缀和的形式,而众所周知的是,一个 n 次多项式的前缀和是 n+1 次多项式,也因此两个括号里的东西的次数分别为 i−l,r−i,乘起来时加一块就得到了 r−l 次多项式。
为了方便转移,我们需要维护前缀和(这里就与一开始设的不大于 k 的情形殊途同归了)。因为前缀和是 r−l+1 次多项式,所以我们只需要挑出 r−l+2,即最多 n+1 个点,插个值就能轻松求出前缀和。
我们显然可以取 ci,ci+1,ci+2,…,ci+n 此 n+1 个点。这就意味着,我们只需DP出前 n+1 个点,剩下位置的前缀和就能够很快算出了。需要注意的是,ci+n 可能大于 ci+1,这时不能插值,但是我们已经知道了每个位置的值,就直接暴力算前缀和即可。并且,因为下标连续,插值可以做到 O(n)。
对于段 [ci,ci+1),i 以前的段的值,也要被计入前缀和,但是因为对一切 k∈[ci,ci+1) 它都是常数,所以直接算到前缀和多项式的常数项中即可。
我们来计算复杂度。首先,我们要顺序DP每一段 [ci,ci+1),一共 O(n) 段;其次,对于每一段,我们都需要访问所有合法区间各一次,约共 2100 次;再次,对于每个合法区间,我们都需要 O(n) 地DP出前 n+1 个值,再 O(n) 地插值更新前缀和,加起来仍是 O(n);三项乘一块,得到总复杂度 O(2100n2)。
然后就是卡常了。注意到拉插时,对于同一个 [ci,ci+1),不同的位置计算的插值下标都相同,因而得到的拉格朗日基本多项式 li 也相同,影响的只有系数,因此可以在一起计算,这样使我本地的常数减少了 1s,放到网站上就是吸口氧过与不过的区别(反正NOI也吸氧)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,a[310],b[310],fac[310],inv[310],tmp[310],lim;
void ADD(int &x,int y){x+=y;if(x>mod)x-=mod;}
int g[2210][310],pre[2210][610],vis[3010],tot,id[310][310];
void Lagrange(int D,int L,int R){
if(L+n>=R){for(int i=1;i<=tot;i++)pre[i][D]=g[i][lim];return;}
tmp[0]=1;for(int i=1;i<=lim;i++)tmp[i]=1lltmp[i-1](R-(L+i-1))%mod;
for(int j=1;j<=tot;j++)pre[j][D]=0;
for(int i=lim,j=1;i;j=1llj(R-(L+--i))%mod){
int pmt=1lltmp[i-1]j%modinv[i-1]%modinv[lim-i]%mod;
if((lim-i)&1)for(int j=1;j<=tot;j++)ADD(pre[j][D],mod-1llpmtg[j][i]%mod);
else for(int j=1;j<=tot;j++)ADD(pre[j][D],1llpmtg[j][i]%mod);
}
}
vector
void name(int l,int r){if(l>r||id[l][r])return;id[l][r]=++tot;if(l!=r)for(int i=(l+r-1)/2;i<=(l+r)/2+1;i++)name(l,i-1),name(i+1,r);}
int dfs(int l,int r,int D){
if(l>r)return 0;
int ID=id[l][r];
if(vis[ID]D)return ID;vis[ID]=D;
if(lr){
if(D<a[l]||D>=b[l]){for(int i=0;i<=lim;i++)g[ID][i]=pre[ID][D-1];pre[ID][D]=pre[ID][D-1];}
else{for(int i=0;i<=lim;i++)g[ID][i]=i+pre[ID][D-1];pre[ID][D]=v[D]-v[a[l]-1];}
return ID;
}
for(int i=1;i<=lim;i++)g[ID][i]=0;g[ID][0]=pre[ID][D-1];
for(int i=(l+r-1)/2;i<=(l+r)/2+1;i++){
if(D<a[i]||D>=b[i])continue;
int A=dfs(l,i-1,D),B=dfs(i+1,r,D);
for(int j=1;j<=lim;j++)ADD(g[ID][j],1llg[A][j]g[B][j-1]%mod);
}
for(int j=1;j<=lim;j++)ADD(g[ID][j],g[ID][j-1]);
return ID;
}
int main(){
// freopen("robot.in","r",stdin);
// freopen("robot.out","w",stdout);
scanf("%d",&n);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n-1;i>=0;i--)inv[i]=1llinv[i+1](i+1)%mod;
for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]),b[i]++,v.push_back(a[i]),v.push_back(b[i]);
sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
for(int i=1;i<=n;i++)a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1,b[i]=lower_bound(v.begin(),v.end(),b[i])-v.begin()+1;
// for(auto i:v)printf("%d ",i);puts("");
// for(int i=1;i<=n;i++)printf("[%d,%d)\n",a[i],b[i]);
name(1,n);
for(int i=0;i<=n+1;i++)g[0][i]=1;
for(int i=1;i<v.size();i++){
// printf("%d\n",i);
lim=min(n+1,v[i]-v[i-1]);
for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)if(id[l][r])dfs(l,r,i);
Lagrange(i,v[i-1],v[i]-1);
// for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)if(id[l][r]){printf("[%d,%d]",l,r);for(int j=0;j<lim;j++)printf("%d ",g[id[l][r]][j]);puts("");}
// puts("");
}
printf("%d\n",pre[id[1][n]][v.size()-1]);
return 0;
}
XIV.[NOI2018] 冒泡排序
结论1.交换次数压到下界,当且仅当不存在长度大于 2 的下降子序列。
证明很简单。众所周知的是,冒泡排序的交换次数等于序列逆序对数。要压到下界,与每个点有关的逆序对数都只能为 |i−pi|,因为从 i 到 pi 的过程中本身就要交换 |i−pi| 次。如果存在长度大于 2 的下降子序列的话,则逆序对就不仅是两两间贡献了,隔着中间一个数也能贡献,因而就压不到下界。
不存在长度大于 2 的下降子序列,等价于序列可以被划分为不超过 2 条上升子序列——我们抽出所有前缀 max 构成一条上升子序列,则剩余的东西必然也递增,不然如果出现递减的两个数,从前面挑一个前缀 max 就能构成长度为 3 的下降子序列。
要求出所有字典序 >q 的排列 p 个数,常见套路是枚举它们有长度为 i−1 的前缀相同,且第 i 位 p 更大。
首先,我们可以判一下 q 的长度为 i−1 的前缀是否合法:首先,要确保其本身的非前缀 max 元素递增;其次,要确保其未填入的元素全部 > 最后的非前缀 max 元素。这样的话,就可以设计DP状态:fi,j 表示有 i 个 < 前缀 max 的元素、j 个 > 前缀 max 的元素未填入时的方案数。则,fi,j=fi−1,j+∑k=1jfi+k−1,j−k,其中前半部分表示填入一个 <max 的元素,显然为了保证递增只能填最小的一个;后半部分是枚举填入 >max 的数中第 k 大的数填入,则前 k−1 个数都会被划归 <max 的数。两个转移式可以被合并为 fi,j=∑k=0jfi+k−1,j−k。到这里 80 分已经到手了,但我们还可以考虑优化。
如果在矩阵上画出来的话,会发现这转移的位置是一条斜线,不好处理;但是我们可以转变DP状态:设新的 fi,j,其中 i 是原本的 i+j(总序列长度),j 是原本的 i(<max 的数的数量)。则有 fi,j=∑k=0i−jfi−1,j+k−1。考虑换成枚举 j+k−1,得到 fi,j=∑k=max(0,j−1)i−1fi−1,k。到这里我们就发现之前贸然设的 k=0 的下界不合法,可能会出现负数,于是便重新设了 max(0,j−1) 的下界。但是,更好的方式是,我们直接令 j<0 时 fi,j=0 即可直接使用 fi,j=∑k=j−1i−1fi−1,k 的转移式。
下面我们考虑用此DP值来求答案。枚举前缀长度 i,并设 j 表示后缀 [i+1,n] 中有多少个数 <max。则此部分的贡献就是 ∑jfn−i,j。现在考虑有哪些 j 是合法的。设若位置 i 老老实实填上了 qi,剩余后缀中 <max 的数量是 k。
发现,就算 qi 并非前缀 max,即可以填入 <max 的数时,<max 的数也不能填,因为所有未填的 <max 的数中只能填最小的,而 qi 一定不会比最小数还小,因此只能填入一个前缀最大值。故若 qi 是前缀 max,贡献为 ∑j=k+1n−ifn−i,j;否则,即 qi 并非前缀 max,填入 i 位置的前缀 max 会自动将 qi 也归入非前缀 max 类中,故 k 还是至少会加一。所以两部分综合起来看,都是 ∑j=k+1n−ifn−i,j。
发现,无论是转移式还是求值式,我们都需要用到DP数组的后缀和。于是我们设 Sn,m 表示 ∑i=mnfn,i。
好耶!是大家daisuki的推式子时间!
Sn,m=∑i=mnfn,i=∑i=mn∑j=i−1n−1fn−1,j=∑j=m−1n−1∑i=mj+1fn−1,j=∑j=m−1n−1fn−1,j+∑j=m−1n−1∑i=m+1j−1fn−1,j=Sn−1,m−1+∑i=m+1j∑j=i−1n−1fn−1,j=Sn−1,m−1+∑i=m+1jfn,i=Sn−1,m−1+Sn,m+1
OK!这样我们就得出了简单的后缀和公式!
现在考虑其实际意义:从 S0,0=1 出发,每次要么向 n,m 正方向各走一步,要么向 m 的负方向走一步。同时,因为在合法的转移式中可能会出现 Sn,−1,但不会出现更负的数,因此我们便规定不能越过 m=−1 这条界线。
因为这个“不越过 −1 的界线”长得很像卡特兰数模型,因此我们考虑令 m 全体增加 1 再说。于是现在 S0,0=S0,1=1(原本有 S0,−1=1,现在加一得到了 S0,0=1)。两个起始状态觉得也不好做,因此我们再强制令全体 n 增加 1。于是现在 S1,0=S1,1=1。假如我们此时令 S0,0=1 的话,就会发现由上述操作刚好可以得出 S1,0=S1,1=1,因此我们这个操作是正确的。
发现只有一种操作在 n 方向上有移动不好做。考虑让后一种操作也在 n 轴正方向上走一步。因为原本方案中 n 轴上动了恰好 n 步,故 m 轴正方向实际上也动了 n 步,则 m 轴负方向实际上动了 n−m 步;因为 m 轴负方向的移动现在同时在 n 轴正方向上移动,因此就多移动了 n−m 步,因此终点现在是 (2n−m,m)。
发现现在是裸的卡特兰数模型。于是就直接得到 (2n−mn−m)−(2n−mn−m−1) 的卡特兰数。
因为我们之前强制令 n,m 各增加了 1,因此 Sn,m 中的 n,m 要比上式中的 n,m 各少一,所以此时要把它补回去,因此有 Sn,m=(2n−m+1n−m)−(2n−m+1n−m−1)。
时间复杂度可以做到 O(n)。同时,在依次处理 q 的每个前缀的时候,要记得判断后缀中所有元素是否都 > 非前缀 max 元素构成的序列中的 max。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=1200000;
int T,n,q[1201000],fac[1201000],inv[1201000],premax,secmax,res,k,sufmin[1201000];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int binom(int x,int y){if(x<y||y<0)return 0;return 1llfac[x]inv[y]%modinv[x-y]%mod;}
int S(int x,int y){if(y>x)return 0;return(binom(2x-y+1,x-y)-binom(2x-y+1,x-y-1)+mod)%mod;}
int main(){
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1llfac[i-1]i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1llinv[i]*i%mod;
scanf("%d",&T);
while(T--){
scanf("%d",&n),premax=secmax=res=k=0;
for(int i=1;i<=n;i++)scanf("%d",&q[i]);sufmin[n+1]=0x3f3f3f3f;
for(int i=n;i;i--)sufmin[i]=min(sufmin[i+1],q[i]);
for(int i=1;i<=n;i++){
if(secmax>sufmin[i])break;
if(q[i]>premax)k+=q[i]-premax-1,premax=q[i];else secmax=max(secmax,q[i]),k--;
(res+=S(n-i,k+1))%=mod;
}
printf("%d\n",res);
}
return 0;
}
XV.[CODE FESTIVAL 2017 qual C]Three Gluttons
题解
XVI.[AGC046D]Secret Passage
稍微观察一下就能发现,任一时刻,我们剩下的东西必然是一段定死了的后缀,加上一些可以任意塞位置的 0 与 1。考虑任意一个由上述时刻生成的串,就会发现它与该后缀的最长公共子序列长度即为后缀长度,且还剩余一些 0 与 1。
于是考虑模拟最长公共子序列的过程。设 gi,j,k 表示长度为 n−i+1 的后缀,所有与其LCS就是该后缀本身,且多余 j 个 0、k 个 1 的串数。为了不重复计数,我们强制 0 只能插在原后缀的 1 前面,1 只能插在原后缀的 0 前面。倒序转移即可。
并非所有 (i,j,k) 都是合法的。我们还需要求出合法的状态。设 fi,j,k 表示其是否合法。则,一个状态合法,当且仅当其通过一步LCS匹配能够到达另一个合法状态,或者其通过删除再插入操作能够到达另一个合法状态。两种方案分别转移即可。
时间复杂度 O(n3)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
char s[310];
int n,g[310][310][310],res,h[310][310];
bool f[310][310][310];
int main(){
scanf("%s",s+1),n=strlen(s+1);
f[0][0][0]=true;
for(int i=1;i<=n;i++){
for(int j=n;j>=0;j--)for(int k=n;k>=0;k--){
f[i][j][k]|=f[i-1][j][k];
if(s[i]'0')f[i][j][k]|=f[i][j][k+1];
if(s[i]'1')f[i][j][k]|=f[i][j+1][k];
}
if(i>1)for(int j=0;j<=n;j++)for(int k=0;k<=n;k++){
if((s[i-1]'0'||s[i]'0')&&j)f[i][j][k]|=f[i-2][j-1][k];
if(s[i-1]'0'&&s[i]'0'&&j)f[i][j][k]|=f[i-1][j-1][k+1];
if((s[i-1]'1'||s[i]'1')&&k)f[i][j][k]|=f[i-2][j][k-1];
if(s[i-1]'1'&&s[i]'1'&&k)f[i][j][k]|=f[i-1][j+1][k-1];
}
}
g[n][0][0]=1;
for(int i=n;i;i--)for(int j=0;j<=n;j++)for(int k=0;k<=n;k++){
(g[i-1][j][k]+=g[i][j][k])%=mod;
if(s[i]'0')(g[i][j][k+1]+=g[i][j][k])%=mod;
if(s[i]'1')(g[i][j+1][k]+=g[i][j][k])%=mod;
}
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)for(int k=0;k<=n;k++)if(f[i][j][k])(res+=g[i][j][k])%=mod;
printf("%d\n",(res+mod-1)%mod);
return 0;
}
XVII.CF814E An unavoidable detour for home
Observation 1.节点编号即为自 1 始的bfs序。
直接由最短路不降得出。
Observation 2.可以将图按照到 1 的距离分层。分层后,每点往前一层有且仅有一条连边,且其余边都是层内部的连边。
由最短路唯一得出。
Approach. 考虑设 f[l,r] 表示当前分层分到了区间 (l,r] 为一段的方案数。
该段中的 2 和 3 数量已经确定。但是,由于所有点都往前一层连了一条边,其分别变成了 1 与 2。
考虑设有 one 个 1,two 个 2。
我们考虑有多少点在本层内部连边。我们考虑设有 i 个节点在本层内部连了两条边,2j 个节点连了一条。考虑设 g[i][j] 表示此时的方案数。
考虑那些连 1 条边的点。它们唯一的连边方式就是 1-2-2-......-2-1,即中间一长串 2(可以为空)两边接着 1。
考虑最后一对被加入的 1。其有 2j−1 种 1-1 的配对方式,包括 2(j−1) 种拆开先前的一对 1-1 与它配对,以及此对 1-1 自相配对。
考虑为此对 1 插一些 2 在中央。若插 k 个 2,则有 i!(i−k)! 种方案。
于是DP式子就出来了:g[i][j]=∑k=0ig[i−k][j−1]×i!(i−k)!×(2j−1)。边界状态为 g[0][0]=1。
但是 2 并非仅有这一种连法。其还有自身成环的方法。
因为可能成不止一个环,所以可以考虑用EGF解决。我们知道单个环的OGF为 ∑i=3(i−1)!2xi(环的长度至少为 3,且有 2i 种同构方案)。于是单个环的EGF即为 ∑i=3xi2i。然后有标号背包的实质是 exp,于是计算 e∑i=3xi2i 便得到了环的EGF。
这里的 exp 可以手工暴力计算,具体而言是通过 (ex)′=ex 变成卷积的形式,也即把分治FFT的 exp 求法用暴力实现。
现在考虑把环加入 g 中。于是就把 g 和环再做一个EGF卷积(当然,可以手工进行),就得到了真正的 g。
明显,one 个 1 与 two 个 2 不一定全部在本层中用掉。我们考虑枚举用掉了 O 个 1,T 个 2,另外还有 OT 个 2 连了一条边。此时的方案数即为 (oneO)(twoT)(two−TOT)g[T][(O+OT)/2]。
还剩余 (one−O)+2(two−T)−OT 条边,设这一数量为 p。其全部连到下一层,故下一层的点数即为 p。则连边数量为 p!2T,因为 2 的情况两条边等价(考场上就因为忘除这个 2 去世了)
于是有 f[l,r]×(oneO)(twoT)(two−TOT)g[T][(O+OT)/2]×p!2T→f[r,r+p]。
枚举 l,r,O,T,OT 进行转移,复杂度为 O(n5),已经可以通过。
还可以通过组合意义求出 g 的通项公式后,整理式子做到 O(n3),但是我不会。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int N=3000;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,d[310],f[1010][1010],fac[3010],inv[3010],INV[3010],g[1010][1010],h[1010],hh[1010],sone[1010],stwo[1010],u[310][310][310];
int C(int x,int y){return 1llfac[x]inv[y]%modinv[x-y]%mod;}
//g:ways of i 2s and 2j 1s forming a graph
//h:ways of i 2s forming a graph
//#define RANDOM
int main(){
// freopen("graph.in","r",stdin);
// freopen("graph.out","w",stdout);
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1llfac[i-1]i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i>=0;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=1;i<=N;i++)INV[i]=1llfac[i-1]inv[i]%mod;
scanf("%d",&n);
for(int i=3;i<=n;i++)hh[i]=1llINV[i]INV[2]%mod;
h[0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++)(h[i]+=1lljhh[j]%modh[i-j]%mod)%=mod;
h[i]=1llh[i]INV[i]%mod;
}
for(int i=1;i<=n;i++)h[i]=1llh[i]*fac[i]%mod;
// for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
g[0][0]=1;
for(int i=0;i<=n;i++)for(int j=1;j<=n;j++)for(int k=0;k<=i;k++)(g[i][j]+=1ll*g[i-k][j-1]*fac[i]%mod*inv[i-k]%mod*(2*j-1)%mod)%=mod;
// for(int i=0;i<=n;i++){for(int j=0;j<=n;j++)printf("%d ",g[i][j]);puts("");}
for(int i=n;i>=0;i--)for(int j=0;j<=n;j++)for(int k=1;k<=i;k++)(g[i][j]+=1llg[i-k][j]C(i,k)%modh[k]%mod)%=mod;
// for(int i=0;i<=n;i++){for(int j=0;j<=n;j++)printf("%d ",g[i][j]);puts("");}
for(int i=1;i<=n;i++){
scanf("%d",&d[i]);
sone[i]=sone[i-1],stwo[i]=stwo[i-1];
if(d[i]==2)sone[i]++;else stwo[i]++;
}
for(int l=0;l<=n;l++)for(int r=0;r<=n;r++)for(int i=0;i<=l;i++)for(int j=0;j<=r;j++)for(int k=0;j+k<=r;k++){
if((i+k)&1)continue;
int p=(l-i)+2(r-j)-k;
(u[l][r][p]+=1llC(l,i)%modC(r,j)%modC(r-j,k)%modg[j][(i+k)/2]%modfac[p]%modksm(INV[2],r-j-k)%mod)%=mod;
}
f[1][d[1]+1]=1;
for(int r=1;r<=n;r++)for(int l=1;l<r;l++){
if(!f[l][r])continue;
int one=sone[r]-sone[l],two=stwo[r]-stwo[l];
for(int p=0;p<=n;p++)(f[r][r+p]+=1llf[l][r]u[one][two][p]%mod)%=mod;
}
printf("%d\n",f[n][n]);
return 0;
}
XVIII.CF17C Balance
考虑一个串 t 能被 s 转化得出的充要条件是什么:显然,如果对 t 和 s 中连续的相同字符缩成一个,则 t 得到的新串必定要是 s 得到的新串的子序列(可以不连续)。
于是我们强制 t 对应的子序列必定是字典序最小的一个,然后就可以DP,设 fi,j,k,l 表示位置 i,填了 j 个 a,k 个 b,l 个 c 时的方案数,转移时就枚举下一个字符填什么,进而直接转移到该字符在 s 中下次出现的位置即可。虽然是 4 维,但是后三维最大只需枚举到 n/3,因此开的下。
DP完后,我们就枚举 fn+1,i,j,k 计算答案即可。计算答案的时候涉及到将 p 次出现有序划分给 q 个位置的问题,这个直接用小学数学中的隔板法即可得出是 (p−1q−1)。
时间复杂度 kn(nk)k,其中 k 是不同字符数,本题为 3。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=51123987;
int n,f[160][52][52][52],nex[160][3],lim,res,C[160][160];
char s[160];
int c(int x,int y){
if(y-1)return x-1;
if(x-1)return 0;
return C[x][y];
}
int coeff(int i,int j,int k){
int N=n/3;
i--,j--,k--;
if(n%30)return 1llc(N-1,i)c(N-1,j)%modc(N-1,k)%mod;
if(n%3==1)return(1llc(N-1,i)c(N-1,j)%modc(N,k)%mod+1llc(N-1,i)c(N,j)%modc(N-1,k)%mod+1llc(N,i)c(N-1,j)%modc(N-1,k)%mod)%mod;
if(n%32)return(1llc(N,i)c(N,j)%modc(N-1,k)%mod+1llc(N,i)c(N-1,j)%modc(N,k)%mod+1llc(N-1,i)c(N,j)%mod*c(N,k)%mod)%mod;
}
int main(){
scanf("%d",&n),lim=(n-1)/3+1;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
scanf("%s",s+1);
for(int j=0;j<3;j++)nex[n][j]=n+1;
for(int i=n-1;i>=0;i--){
for(int j=0;j<3;j++)nex[i][j]=nex[i+1][j];
nex[i][s[i+1]-'a']=i+1;
}
for(int j=0;j<3;j++)f[nex[0][j]][0][0][0]=1;
for(int i=1;i<=n;i++)for(int j=0;j<=lim;j++)for(int k=0;k<=lim;k++)for(int l=0;l<=lim;l++){
if(!f[i][j][k][l])continue;
for(int p=0;p<3;p++){
if(ps[i]-'a')continue;
if(nex[i][p]n+1)continue;
if(s[i]-'a'0)(f[nex[i][p]][j+1][k][l]+=f[i][j][k][l])%=mod;
if(s[i]-'a'1)(f[nex[i][p]][j][k+1][l]+=f[i][j][k][l])%=mod;
if(s[i]-'a'2)(f[nex[i][p]][j][k][l+1]+=f[i][j][k][l])%=mod;
}
if(s[i]-'a'0)(f[n+1][j+1][k][l]+=f[i][j][k][l])%=mod;
if(s[i]-'a'1)(f[n+1][j][k+1][l]+=f[i][j][k][l])%=mod;
if(s[i]-'a'==2)(f[n+1][j][k][l+1]+=f[i][j][k][l])%=mod;
}
for(int i=0;i<=lim;i++)for(int j=0;j<=lim;j++)for(int k=0;k<=lim;k++)(res+=1llf[n+1][i][j][k]coeff(i,j,k)%mod)%=mod;
printf("%d\n",res);
return 0;
}
XIX.[Atcoder-Typical DP contest-O]文字列
设 fi,j,k,l 表示前 i 个字符,其中前 i−1 个字符已经全部填完、第 i 个字符已填了 k 个、这 k 个字符形成了 l 段,且当前有 j 对字符(不论哪种字符)是相邻且相同的。转移就枚举下一段字符填了几个、填在哪里(即有无截开一段)即可。在一个字符全部填完之后,要除以段数的阶乘(因为各段间是无序的,但我们的DP是有序的)。
如果我们设 n 为字符数、m 为单个字符的数量的最大值,则复杂度为 n2m4。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int n=26;
int a[30],s[30],f[30][310][20][20],fac[30],inv[30],res;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
//i characters, j invalid adjacents, k i-s has been in, they form l sections.
int main(){
for(int i=1;i<=n;i++)scanf("%d",&a[i]),s[i]=s[i-1]+a[i];
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
f[0][0][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=max(0,s[i-1]-1);j++)for(int l=0;l<=a[i-1];l++)(f[i][j][0][0]+=1ll*f[i-1][j][a[i-1]][l]*inv[l]%mod)%=mod;
for(int k=0;k<a[i];k++)for(int l=0;l<=k;l++)for(int j=0;j<=max(0,s[i-1]+k-1);j++)for(int K=1;k+K<=a[i];K++){
if(!f[i][j][k][l])continue;
// printf("%d,%d,%d,%d->%d,%d,%d,%d:%d\n",i,j,k,l,i,j+K-1,k+K,l+1,1llf[i][j][k][l](s[i-1]+1-j-2l)%mod);
(f[i][j+K-1][k+K][l+1]+=1llf[i][j][k][l](s[i-1]+k+1-j-2l)%mod)%=mod;
// printf("%d,%d,%d,%d->%d,%d,%d,%d:%d\n",i,j,k,l,i,j+K-2,k+K,l+1,1llf[i][j][k][l](j-(k-l))%mod);
if(j)(f[i][j+K-2][k+K][l+1]+=1llf[i][j][k][l](j-(k-l))%mod)%=mod;
}
// for(int j=0;j<=max(0,s[i]-1);j++)for(int l=0;l<=a[i];l++)printf("%d,%d,%d,%d:%d\n",i,j,a[i],l,f[i][j][a[i]][l]);
}
for(int l=0;l<=a[n];l++)(res+=1llf[n][0][a[n]][l]inv[l]%mod)%=mod;
printf("%d\n",res);
return 0;
}
XX.[LOJ#3395][2020-2021 集训队作业]Yet Another Permutation Problem
Observation 1.由原序列推可能的终局序列是很容易重复计算的,但是判定终局序列是否合法是简单且不重不漏的。
故我们要尝试判定方法。
Observation 2.一个终态合法,当且仅当其中最长上升子段长度不小于 n−k。
这是显然的。
于是我们就对于每个 k 计算最长上升子段长度不小于 k 的方案数,然后反过来就是答案。
考虑容斥,即钦定有若干段长度为 k 的区间 [li,ri] 是上升子段、其它位置任意时,此时的合法排列数。此时的容斥系数为 −1 的“上述区间个数减一”次幂。
这些段中可能有重叠部分。那么我们就取并后得到许多两两无交区间 [Li,Ri]。
考虑此时的排列数。显然每段区间内部的元素顺序唯一。故而此时方案数即为 n!∏(Ri−L1+1)!。
考虑对于每组 {[Li,Ri]},求出能得到其的所有 {[li,ri]} 集合,然后计算所有这些集合对应的容斥系数之和即可得到 {[Li,Ri]} 集合对应的容斥系数。
集合 {[li,ri]} 的容斥系数显然仅与其大小有关——并且,因为容斥系数是 −1 的次幂,所以我们强制将所有容斥系数取个反(即变成了 −1 的“上述大小”次幂),然后其容斥系数可以被拆作其子集的系数的积。
考虑对于每个 [Li,Ri],其有多种被分拆为 [li,ri] 的做法,而不同的 [L,R] 间分拆是独立的,故由乘法原理,总拆分数即为各个 [Li,Ri] 间拆分数之积;而我们上面阐述了取反后的容斥系数亦可被乘在一起,故事实上 {[Li,Ri]} 对应的所有分拆方案的容斥系数之和即为每个 [Li,Ri] 对应的所有分拆方案的容斥系数和之积。
显然 [Li,Ri] 唯一有效信息是其长度。故我们可设 fi,j 表示长度为 i 的区间在被拆作长度为 j 的区间之重叠时所有方案的容斥系数之和。则得到此时对答案的贡献为 −n!∏fRi−Li+1,k(Ri−L1+1)!。
那么我们于是可设 gi 表示序列长度为 i 时的答案取反后除去 i! 后的结果。考虑转移。
要么是 i 不划在任意区间中(此时就是 gi−1),要么是 i 与之前一些元素划在一段。
然后便可以列出 gi=gi−1+∑j=kigi−j×fj,kj!。答案为 n!(1−gn)(因为取过反)。
是多项式技巧还是奇怪东西来求 g 我们暂且不论,毕竟求 g 的前提是求 f。
考虑求出 fi,k。
考虑元素 i。唯一可能的长度为 k 且包含其的区间即为 [i−k+1,i],则其必然被选中。这样之后前一个区间的右端点便不能右于 i−k+1,也即两个相邻端点距离不超过 k−1。于是便有:
fi,k={0(i<k)−1(i=k)∑j=1k−1−fi−j,k(i>k)
一个前缀和优化即可 O(n) 求出单个 k 的 f。
打出表后发现这个 f 似乎很有趣。具体而言,k=1 时是除 i=1 时其它位置全为 0 的,故此时需要特判;其余情况存在循环节,长度为 k 且每个循环节仅有前两位置非零(分别为 −1 和 1)。那么非零位置个数便仅有 n/k 个,暴力转移 g 即可做到调和级数的 O(n2logn)。
代码:
include<bits/stdc++.h>
using namespace std;
int n,mod,res[1010],fac[1010],inv[1010],g[1010];
//int f[1010],s[1010];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d",&n,&mod);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
res[1]=fac[n];
/for(int i=2;i<=n;i++){
for(int j=1;j<=n;j++)f[j]=s[j]=0;
f[i]=s[i]=mod-1;
for(int j=i+1;j<=n;j++)f[j]=(s[j-i]-s[j-1]+mod)%mod,s[j]=(s[j-1]+f[j])%mod;
for(int j=1;j<=n;j++)printf("%d ",f[j]);puts("");
}/
for(int i=2;i<=n;i++){
g[0]=1;
for(int j=1;j<=n;j++){
g[j]=g[j-1];
for(int k=i;k<=j;k+=i){
(g[j]+=mod-1llg[j-k]inv[k]%mod)%=mod;
if(k<j)(g[j]+=1llg[j-k-1]inv[k+1]%mod)%=mod;
}
}
res[i]=1llfac[n](mod+1-g[n])%mod;
}
for(int i=n;i;i--)printf("%d\n",res[i]);
return 0;
}
XXI.CF708E Student's Camp
显然可以设一个 DP:fi,l,r 表示前 i 行联通,且第 i 行剩下的区间是 [l,r] 时的概率。转移枚举下一行剩余的区间,可以做到 O(nm4)。
观察转移的形式,发现能转移到 fi,l,r 的 l,r 均在某范围内。于是二维前缀和优化一下可以做到 O(nm2)。
然后考虑反着处理一下:fi,l,r=pl,r(∑fi−1,p,q−∑p<q<lfi−1,p,q−∑r<p<qfi−1,p,q),其中 pl,r 表示恰好留下区间 [l,r] 中元素的概率。
注意到上式中有很优美的求和,也即二维均大于或小于某值的 DP 值之和。
显然这个 DP 具有对称性,也即 [l,r] 的 DP 值与 [m−r+1,m−l+1] 的 DP 值应该是相同的。所以,前后缀和的 DP 值也是相同的。我们设 tp=∑l<r<pfi−1,l,r,则上式转为 fi,l,r=pl,r(tm−tl−1−tm−r)。
我们设 sp=∑l<r<pfi,l,r。然后我们尝试从 t 转移到 s。
首先我们可以拆开 pl,r:显然这个概率关于 l 和 r 是独立的。于是我们重新设 px 表示风吹掉 x 块的概率,则有新的概率为 pl−1pm−r。
于是有转移式
sx=∑l<r<xpl−1pm−r(tm−tl−1−tm−r)
把括号拆开,并令 Tx=pxtx,得到
sx=∑l<r<xpl−1pm−rtm−∑l<r<xTl−1pm−r−∑l<r<xplTm−r
共有三个式子。第一个式子中 tm 前的系数可以预处理出来。第二个、第三个式子都可以随着 x 增加用一个值来维护。故转移做到 O(m)。总复杂度 O(m2+nm)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,p,P[1510],K,fac[100100],inv[100100],f[1510],g[1510],r[1510];
int s[1510],t[1510];
int C(int x,int y){if(x<y)return 0;return 1llfac[x]inv[y]%modinv[x-y]%mod;}
int main(){
scanf("%d%d%d%d",&n,&m,&p,&K),p=1llpksm(K)%mod,scanf("%d",&K);
fac[0]=1;for(int i=1;i<=K;i++)fac[i]=1llfac[i-1]i%mod;
inv[K]=ksm(fac[K]);for(int i=K;i;i--)inv[i-1]=1llinv[i]i%mod;
// printf("%d\n",p);
for(int i=0;i<m;i++)if(i<=K)P[i]=1llC(K,i)ksm(p,i)%modksm(mod+1-p,K-i)%mod;
for(int l=1;l<=m;l++)for(int r=l;r<=m;r++)f[r]=(1llP[l-1]P[m-r]+f[r])%mod;
for(int i=1;i<=m;i++)(f[i]+=f[i-1])%=mod;
for(int i=0;i<=m;i++)r[i]=f[i];
for(int i=2;i<=n;i++){
for(int j=0;j<=m;j++)swap(f[j],g[j]);
for(int j=1;j<=m;j++)s[j]=(1llg[j]P[j]+s[j-1])%mod;
t[0]=P[0];for(int j=1;j<=m;j++)t[j]=(t[j-1]+P[j])%mod;
// for(int j=0;j<=m;j++)printf("%d ",g[j]);puts("");
// for(int j=0;j<=m;j++)printf("%d ",s[j]);puts("");
// for(int j=0;j<=m;j++)printf("%d ",t[j]);puts("");
int S=0,T=0;
for(int j=0;j<=m;j++){
f[j]=(1llr[j]g[m]+mod-S+mod-T)%mod;
S=(1lls[j]P[m-j-1]+S)%mod;
T=(1llt[j]P[m-j-1]%mod*g[m-j-1]+T)%mod;
}
}
// for(int j=0;j<=m;j++)printf("%d ",f[j]);puts("");
printf("%d\n",f[m]);
return 0;
}
XXII.[ARC089D] ColoringBalls
考虑用颜色序列去对应操作。
颜色序列中有若干连续白段。这些段显然不能被任何操作上色。而对于其它彩段,其至少需要先被染红一次。进而我们肯定贪心地用一遍红色染掉一段极长彩段。
故我们可以首先枚举彩段的数量,设为 R。需要给每段分配一个 r。肯定是贪心地分配序列中出现的前 R 个 r。
然后我们发现,对于被分配了至少一个 b 的彩段,这之后无论再分配给其什么操作,效果都是相同的:为 rb 交错次数增加一。换句话说,对于一段分配了 K 个操作(其中前两个操作分别是 r 和 b)的彩段,其允许所有连续 b 段数量不超过 K−1 的染色方案。但是为了不重复统计,我们强制其必须出现 K−1 段连续的 b。
我们需要再枚举被分配了 b 的彩段数量,设为 B。肯定是贪心地为前 B 个彩段分配,且分配的永远是 r 后第一个 b。这样,我们可以确定第 i 条彩段的 b 的位置 pi,则后 i 条彩段总共使用的额外操作数必须不大于 n−pi。
彩段在序列中是有顺序的,且当且仅当两个彩段的操作数相同,它们之间没有顺序之分。其余情形下,彩段间都有顺序。为了对顺序计数,我们强制排在前的彩段被分配的字符数更多。
进而可以 DP:设 fi,j,k 表示后 i 条彩段分配 j 个字符且第 i 条分配 k 个字符的方案数。转移枚举之前若干段分配字符数都为某值,复杂度带调和级数,为 O(n4logn)。写出式子后发现可以简单前缀和优化,复杂度变为 n3logn。因为在外层还要枚举 R,B,复杂度为 O(n5logn)。
这之后考虑统计答案。子问题是一段字符扩张成一个串,一些位置要扩张成非空串,一些位置要扩张成任意串。两类位置的数量都可以简单计算,然后剩下就是隔板法简单算了。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int N=200;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,f[100][100][100],fac[300],inv[300],p[100],P[100],q[100],cR,res;
char s[100];
int C(int x,int y){if(y<0||x<y)return 0;return 1llfac[x]inv[y]%modinv[x-y]%mod;}
bool occ[100];
int main(){
scanf("%d%d%s",&n,&m,s+1);
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1llfac[i-1]i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=1,j=1;i<=m;i++)if(s[i]'r'){
cR++,j=max(j,i);
while(j<=m&&s[j]'r')j++;
p[cR]=j,P[cR]=i;if(j<=m)j++;
}
for(int R=0;R<=cR;R++)for(int B=0;B<=R;B++){
if(p[B]>m)break;
memset(occ,false,sizeof(occ)),memset(q,0,sizeof(q));
for(int i=1;i<=R;i++)occ[P[i]]=true;
for(int i=1;i<=B;i++)for(int j=p[i];j<=m;j++)q[i]+=!occ[j];
memset(f,0,sizeof(f));
f[B+1][0][0]=1;
for(int i=B+1;i>1;i--)for(int j=0;j<=q[i];j++){
int sum=0;
for(int L=1;L<=m;L++){
(sum+=f[i][j][L-1])%=mod;
for(int K=1;K<i&&j+LK<=q[i-K];K++)
f[i-K][j+LK][L]=(1llC(R+1-i+K,K)sum+f[i-K][j+LK][L])%mod;
}
}
int any=(B+1)<<1;
// printf("RB:%d,%d\n",R,B);
for(int j=0;j<=q[1];j++)for(int k=0;k<=m;k++){
if(!f[1][j][k])continue;
// printf("%d,%d:%d,%d\n",j,k,f[1][j][k],2(j+R)+1);
res=(1llf[1][j][k]C(n-1+any,2(j+R))+res)%mod;
}
}
printf("%d\n",res);
return 0;
}
XXIII.[AGC035E] Develop
考虑最终得到的状态。其在 [1,n] 中有一些缺失的元素。
我们必然可以让删掉的元素是且仅是这些缺失的元素——因为如果一个元素删掉后又被写上了,则干脆一开始就别把它删了。
进而我们可以得到一些先后关系,即位置 i 的数必然先于位置 i−2 和位置 i+K 删掉。
考虑把这种关系建成图,则问题等价于在图中删去若干点使得无环。
发现这里面所有的环都是 lcm(2,K) 的环。也就是说每个这样的环中至少要干掉一个点,计算这样的方案数。
当 K 是偶数时,问题关于奇偶独立。分开考虑后,问题变为不存在 K/2+1 个连续数的方案数。那么维护当前已经保留了连续的多少个数,随便 DP 一下即可,复杂度平方。
当 K 是奇数时,手玩发现假如存在经过超过两条 K 边的环,则这个环不是简单环,换句话说就是只要排除一切两条 K 边的环就 OK 了。
怎么办呢?我们考虑将所有点按照奇偶性分类,则 K 边必然是在类间,2 边必然是在类内。
考虑设某条 K 边的终点是某个奇数 a。则其构成的环必然是 a→a−2→a−4→⋯→b−K→b→b−2→b−4→⋯→a−K→a。
这条环在 b−K→b 的位置被统计。因此我们可以按照 b−K 递增的顺序,每次把 b−K 与 b(前者是奇数,后者是偶数)加入状态。
设 fi,j,k 表示当前 b−K=i,以 i 作为某条 a′→b′−k 路径上一点时的路径最长为 j,且从 b−K 开始连续选中 k 个元素时的方案数。转移枚举 (b−K,b) 两个数中选哪些,复杂度 O(n3)。
特别地,当 K=1 时,其与 K 是偶数的情形类似。
代码:
include<bits/stdc++.h>
using namespace std;
int mod;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
namespace SIMP{
int f[200][200];
int calc(int n,int m){//n numbers,can appear no more than m times in a row.
memset(f,0,sizeof(f));
f[0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<=m;j++){
ADD(f[i+1][0],f[i][j]);
if(j<m)ADD(f[i+1][j+1],f[i][j]);
}
int ret=0;for(int j=0;j<=m;j++)ADD(ret,f[n][j]);
return ret;
}
}
namespace COMP{
int f[200][200][200],bin[200];
int calc(int n,int m){
bin[0]=1;for(int i=1;i<=n;i++)bin[i]=(bin[i-1]<<1)%mod;
if(n<=m)return bin[n];
for(int i=0;i<=(m>>1);i++)f[0][0][i]=(i(m>>1)?1:bin[(m>>1)-i-1]);
for(int i=1;(i<<1)-1<=n;i++)for(int j=0;j<m+2;j++)for(int k=0;k<=n;k++){
if(!f[i-1][j][k])continue;
// printf("%d,%d,%d:%d\n",i-1,j,k,f[i-1][j][k]);
ADD(f[i][0][0],f[i-1][j][k]);//choose neither.
if((i<<1)-1+m<=n)ADD(f[i][0][k+1],f[i-1][j][k]);//choose i+K.
if(j+1m+2)continue;
ADD(f[i][j?j+1:j][0],f[i-1][j][k]);//choose i.
if(k>=m)continue;
if((i<<1)-1+m<=n)ADD(f[i][max(j,k+1)+1][k+1],f[i-1][j][k]);//choose both.
}
int sum=0;
for(int j=0;j<m+2;j++)for(int k=0;k<=n;k++)ADD(sum,f[(n+1)>>1][j][k]);
return sum;
}
}
int n,m;
int main(){
scanf("%d%d%d",&n,&m,&mod);
if(m==1){printf("%d\n",SIMP::calc(n,2));return 0;}
if(!(m&1)){printf("%d\n",1llSIMP::calc(n>>1,m>>1)SIMP::calc(n-(n>>1),m>>1)%mod);return 0;}
printf("%d\n",COMP::calc(n,m));
return 0;
}
XXIV.[BZOJ#1471]不相交路径
考虑用全体方案减去有公共点的方案数。
公共点可能有多个,其构成一序列 {x1,x2,…,xk}。我们仅在 (x1,xk) 对的位置统计一次。
令 di,j 表示 i→j 的路径数。令 fi 表示 A→i,C→i 两条路径仅在 i 处交的方案数,同理令 gj 表示 j→B,j→D 仅在 j 处交的方案数。则答案为 dA,BdC,D−∑i,jfidi,j2gj。
令 hi,j 表示 i→j 且无其它公共点的路径对数。可以枚举第一个公共点容斥得出。
考虑 fi。有 fi=dA,idC,i−∑i′fi′hi′,i。gj 同理。
复杂度三方。
代码:
include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,deg[200],A,B,C,D,q[200],l,r,res;
ll h[200][200],d[200][200],f[200],g[200];
bool s[200][200];
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),s[x][y]=true,deg[y]++;
scanf("%d%d%d%d",&A,&B,&C,&D);
l=r=1;
for(int i=1;i<=n;i++)if(!deg[i])q[r++]=i;
while(l!=r){
int x=q[l++];
for(int y=1;y<=n;y++)if(s[x][y]&&!--deg[y])q[r++]=y;
}
for(int i=1;i<=n;i++){
d[q[i]][q[i]]=1;
for(int j=1;j<=n;j++)for(int k=j+1;k<=n;k++)if(s[q[j]][q[k]])d[q[i]][q[k]]+=d[q[i]][q[j]];
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
h[i][j]=d[i][j]d[i][j];
for(int k=1;k<=n;k++)if(k!=i&&k!=j)h[i][j]-=h[i][k]d[k][j]d[k][j];
}
for(int i=1;i<=n;i++){
f[q[i]]=d[A][q[i]]d[C][q[i]];
for(int j=1;j<i;j++)f[q[i]]-=f[q[j]]h[q[j]][q[i]];
}
for(int i=n;i>=1;i--){
g[q[i]]=d[q[i]][B]d[q[i]][D];
for(int j=n;j>i;j--)g[q[i]]-=g[q[j]]h[q[i]][q[j]];
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)res+=f[i]d[i][j]d[i][j]g[j];
printf("%lld\n",d[A][B]*d[C][D]-res);
return 0;
}
XXV.[AGC036F] Square Constraints
对于位置 i,其上填入的元素有区间 [Li,Ri] 的限制。并且,从 0→2n−1,L,R 都是递减的。
假如只有上限,计数是简单的:把上限递增排序得到序列 a,然后答案即为 ∏i=02n−1(ai−i)。
现在同时有上下界,我们考虑容斥。
注意到对于 i∈[n,2n),有 Li=0。这就让容斥变得不很困难了。具体而言,我们考虑将 [0,n) 中元素按照 L、[n,2n) 中元素按照 R 合并在一起排序。然后设 fi,j 表示前 i 个位置中有 j 个位置按照 L 算时的答案。
考虑转移。分为以下转移:
[n,2n) 中元素按照 R 算。此时一共有 sum+j 个位置被占,其中 sum 为前 i 个位置中 [n,2n) 中元素的数量,故系数是 R−(sum+j)。
[0,n) 中元素按照 L 算。此时仍有 sum+j 个位置被占,故系数是 L−1−(sum+j)。
[0,n) 中元素按照 R 算。此时考虑共有多少位置的上界小于之:
全体 [n,2n) 中元素。可以简单统计。
前 i 个位置中所有 [0,n) 中元素。可以简单统计。
前 i 个位置外且按照 L 算的 [0,n) 中元素(注意到事实上有 R0≥R1≥⋯≥Rn−1≥L0≥⋯≥Ln−1)。这意味着我们需要知道按照 L 统计的元素总数,进而需要在外层再枚举一次。
单次 DP 复杂度平方,外层还要再枚举一次,故总复杂度三方。
include<bits/stdc++.h>
using namespace std;
int n,mod,L[510],R[510],p[510],q[510],f[510][510],res;
int calc(int m){
memset(f,0,sizeof(f)),f[0][0]=1;
int sum=0;
for(int i=0;i<(n<<1);i++){
if(p[i]>=n)for(int j=0;j<=i-sum;j++)f[i+1][j]=(1llf[i][j](R[p[i]]-(sum+j))+f[i+1][j])%mod;
else for(int j=0;j<=i-sum;j++){
f[i+1][j+1]=(1llf[i][j](L[p[i]]-(sum+j))+f[i+1][j+1])%mod;
f[i+1][j]=(1llf[i][j](R[p[i]]-(n+i-sum+m-j))+f[i+1][j])%mod;
}
sum+=(p[i]>=n);
}
// printf("%d:%d\n",m,f[n<<1][m]);
return f[n<<1][m];
}
int main(){
scanf("%d%d",&n,&mod);
for(int i=0;i<(n<<1);i++){
R[i]=sqrt((nn<<2)-ii)+2,q[i]=i;
while(R[i]R[i]+ii>(nn<<2))R[i]--;
R[i]++;
R[i]=min(R[i],n<<1);
}
for(int i=0;i<n;i++){
L[i]=sqrt(nn-ii)+2;
while(L[i]L[i]+ii>=nn)L[i]--;
L[i]++;
}
// for(int i=0;i<(n<<1);i++)printf("%d,%d\n",L[i],R[i]);
reverse(q,q+n),reverse(q+n,q+(n<<1));
merge(q,q+n,q+n,q+(n<<1),p,[](int x,int y){return x<n?L[x]<R[y]:R[x]<L[y];});
// for(int i=0;i<(n<<1);i++)printf("%d ",p[i]);puts("");
for(int i=0;i<=n;i++){int tmp=calc(i);if(i&1)(res+=mod-tmp)%=mod;else(res+=tmp)%=mod;}
printf("%d\n",res);
return 0;
}
XXVI.[AGC039F] Min Product Sum
考虑全局最小值。其所在的行和列中所有位置的 f 值都是该值。于是可以删去这些位置后进入子态。
但是全局最小值可能不止一个。于是我们可以枚举若干行和列的最小值都为该值。
设 fi,j,k 表示已经删去了 i 行 j 列且当前考虑到 k 的方案数。枚举 k+1 占了几行几列,进而即可转移。
转移的系数可以平方容斥得出,进而有一个七方的做法。
先贴出七方的代码:
include<bits/stdc++.h>
using namespace std;
int n,m,K,mod,f[110][110][110],C[110][110];
int ksm(int x,int y){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d%d%d",&n,&m,&K,&mod);
if(n<m)swap(n,m);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
f[0][0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)for(int k=0;k<K;k++)
for(int I=i;I<=n;I++)for(int J=j;J<=m;J++){
int num=0;
for(int p=i;p<=I;p++)for(int q=j;q<=J;q++){
int tmp=1llf[i][j][k]C[n-i][n-I]%modC[I-i][p-i]%modC[m-j][m-J]%modC[J-j][q-j]%mod
ksm(K-k,pq-ij)%modksm(K-k-1,IJ-pq)%mod;
if((I-p+J-q)&1)(num+=mod-tmp)%=mod;else(num+=tmp)%=mod;
}
(f[I][J][k+1]+=1llnumksm(k+1,(n-i)(m-j)-(n-I)*(m-J))%mod)%=mod;
}
printf("%d\n",f[n][m][K]);
return 0;
}
我们列出 (i,j,k)→(I,J,k+1) 的系数式:
(k+1)(n−i)(m−n)−(n−I)(m−J)(n−in−I)(m−jm−J)∑p=iI∑q=jJ(I−ip−i)(J−jq−j)(−1)I−p+J−q(K−k)pq−ij(K−k−1)IJ−pq
注意到上式事实上可以被写作 (i,j)→(p,q)→(I,J) 的三步式,故可以设一个到 (p,q) 的辅助 DP g,进而做到五方。
代码:
include<bits/stdc++.h>
using namespace std;
int n,m,K,mod,f[110][110][110],g[110][110][110],fac[110],inv[110];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d%d%d",&n,&m,&K,&mod);
if(n<m)swap(n,m);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
// for(int i=0;i<=n;i++){for(int j=0;j<=i;j++)printf("%d ",C[i][j]);puts("");}
f[0][0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)for(int k=0;k<K;k++)
for(int I=i;I<=n;I++)for(int J=j;J<=m;J++){
(g[I][J][k]+=
1llf[i][j][k]inv[I-i]%modinv[J-j]%mod
ksm(K-k,IJ-ij)%modksm(k+1,(n-i)(m-j)-(n-I)(m-J))%mod)%=mod;
int tmp=
1llg[i][j][k]inv[I-i]%modinv[J-j]%mod*
ksm(K-k-1,IJ-ij)%modksm(k+1,(n-i)(m-j)-(n-I)(m-J))%mod;
if((I-i+J-j)&1)(f[I][J][k+1]+=mod-tmp)%=mod;else(f[I][J][k+1]+=tmp)%=mod;
}
printf("%d\n",1llf[n][m][K]fac[n]%modfac[m]%mod);
return 0;
}
但是五方还远远不够。事实上,通过二维 FFT 之类有一个三方对数的做法,不采用二维 FFT 也可以分离 i,j 的值进而做到四方。
四方代码:
include<bits/stdc++.h>
using namespace std;
int n,m,K,mod,f[110][110][110],g[110][110][110],G[110],F[110],fac[110],inv[110],phi;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d%d%d",&n,&m,&K,&mod),phi=mod-1;
if(K==1){puts("1");return 0;}
if(n<m)swap(n,m);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
// for(int i=0;i<=n;i++){for(int j=0;j<=i;j++)printf("%d ",C[i][j]);puts("");}
f[0][0][0]=1;
for(int k=0;k+1<K;k++)for(int i=0;i<=n;i++)for(int j=0;j<=m;j++){
f[i][j][k]=1llf[i][j][k]ksm(k+1,(n-i)(m-j))%mod;
G[j]=0;for(int I=0;I<=i;I++)(G[j]+=1llf[I][j][k]inv[i-I]%mod)%=mod;
for(int J=0;J<=j;J++)(g[i][j][k]+=1llG[J]inv[j-J]%mod)%=mod;
g[i][j][k]=1llg[i][j][k]ksm(K-k,ij)%mod;
g[i][j][k]=1llg[i][j][k]ksm(K-k-1,phi-ij)%mod;
if((i+j)&1)g[i][j][k]=mod-g[i][j][k];
F[j]=0;for(int I=0;I<=i;I++)(F[j]+=1llg[I][j][k]inv[i-I]%mod)%=mod;
for(int J=0;J<=j;J++)(f[i][j][k+1]+=1llF[J]inv[j-J]%mod)%=mod;
if((i+j)&1)f[i][j][k+1]=mod-f[i][j][k+1];
f[i][j][k+1]=1llf[i][j][k+1]ksm(k+1,phi-(n-i)(m-j))%mod;
}
int res=0;
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)
(res+=1llf[i][j][K-1]inv[n-i]%modinv[m-j]%modksm(K,(n-i)(m-j))%mod)%=mod;
printf("%d\n",1llresfac[n]%modfac[m]%mod);
return 0;
}
XXVII.[ARC093D] Dark Horse
对于与 1 无关的对决,必然是标号小的获胜;从结果出发,因为 1 是冠军,所以 1 对上其它人必然获胜,进而结果等价于编号小的必然胜利。
于是我们考虑从小往大把每个人加入,则其获胜的场即为到根的路径。
Definition 1.定义“天敌”为能战胜 1 的人,“猎物”为无法战胜 1 的人。
考虑 1 的所有对决,其为从 1 出发到根的路径,一共存在 n 场对决。这 n 场对决的任意一场的对手都不能是天敌。换句话说,这里面每一场都必须靠一个猎物撑着。
于是我们考虑设一个 DP 态 fS 表示集合 S 中所有场次的对手都被猎物占据时的方案数,也即一共有 S 个位置(此处 S 亦可表示其对应的 bitmask)已经被“解锁”时的方案数。则如果下一个人是猎物,可以有 2k×fS→fS+2k,也可以有 (S−i)×fS→fS。如果下一个人是天敌,则第一种转移则不可使用。
但是这个算法复杂度是 n22n 的,且不方便优化。
怎么办呢?注意到上述算法没有利用 m≤16 的性质。怎么利用呢?考虑容斥,即钦定某子集中的天敌与 1 展开了对决。
若天敌 ai 在场次 2k 上与 1 展开了对决,则 ai 是其所在子树中的最小值。进而可以 DP,按照 ai 递减顺序处理,每次如果 ai 配给了某棵子树就乘以方案数,复杂度 mn2n。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,N,M,a[20],f[1<<20],lim,fac[1<<20],inv[1<<20],res;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d",&n,&m),N=1<<n,M=1<<m;
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1llfac[i-1]i%mod;
inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=0;i<m;i++)scanf("%d",&a[i]),a[i]--;
f[0]=1;
for(int i=m-1;i>=0;i--)for(int j=N-1;j>=0;j--)
for(int k=0;k<n;k++)if(!(j&(1<<k))){
if(N-a[i]<(j|(1<<k)))continue;
// printf("%d,%d,%d:%d\n",i,j,k,1llfac[N-a[i]-j-1]%mod(1<<k)%mod*
// inv[N-a[i]-(j|(1<<k))]%mod);
(f[j|(1<<k)]+=1llf[j]fac[N-a[i]-j-1]%mod(1<<k)%mod
inv[N-a[i]-(j|(1<<k))]%mod)%=mod;
}
for(int i=0;i<N;i++)f[i]=1llf[i]fac[N-i-1]%mod*N%mod;
// for(int i=0;i<N;i++)printf("%d ",f[i]);puts("");
for(int i=0;i<N;i++)if(__builtin_popcount(i)&1)(res+=mod-f[i])%=mod;else(res+=f[i])%=mod;
printf("%d\n",res);
return 0;
}
XXIX.[ARC096C] Everything on It
考虑用总方案数(22n)减去不合法方案数。
不合法方案数中包含了若干两种或两种以上的元素,若干一种的元素和若干不出现的元素。不出现的元素可以直接忽略,出现一次的元素可以随便挑一个集合扔进去。即,假如我们要往一个包含 i 个子集的子集族中添加 j 个出现一次的元素,方案数为 ij。
设 fi,j 表示 i 个元素的全体合法子集族大小的 j 次幂之和。答案为 fn,0。
我们可以列出式子
fi,j=∑k=02i(2ik)kj−∑p=0i−1∑q=0i−pfp,j+q
上式最大的问题是如何计算 ∑k=02i(2ik)kj。我们将其改写成 ∑i=0m(mi)in,并考虑用斯特林数展开。
∑i=0m(mi)in=∑i=0m(mi)∑j=0n{nj}ij―=∑i=0m∑j=0n{nj}(mi)ij―=∑i=0m∑j=0n{nj}(m−jm−i)mj―=∑j=0n{nj}mj―∑i=0m(m−jm−i)=∑j=0n{nj}mj―2m−j
在 fi,j 的计算中,n=j,m=2i。考虑对于每个 i 分开计算,则此时可以令 si,j=(2i)j―22i−j,然后 fi,j=∑k=0j{jk}si,k。
似乎这个问题完全不弱于矩阵乘法?所以就没法进一步处理下去了。
正确的解法是设一个辅助 DP,即 gi,j 表示 i 个不合法元素分到 j 个集合且每个集合都至少有一个不合法元素的方案数,则 gi,j=gi−1,j−1+(j+1)gi−1,j:前一半是单独分到一个集合,后一半是分到之前所在的某元素的集合或压根不放到集合中。则存在 i 个不合法元素的方案数即为 fi=∑jgi,j(2n−i)j,其中每个集合都可能包含其它元素。答案为 ∑i=0n(−1)ifi(ni)22n−i。
代码:
include<bits/stdc++.h>
using namespace std;
int f[3010],g[3010][3010],n,mod,phi,bit[3010],C[3010][3010],res;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int main(){
scanf("%d%d",&n,&mod),phi=mod-1;
bit[0]=1;for(int i=1;i<=n;i++)bit[i]=(bit[i-1]<<1)%phi;
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
for(int i=0;i<=n;i++)g[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)g[i][j]=(1ll(j+1)g[i-1][j]+g[i-1][j-1])%mod;
for(int i=0;i<=n;i++)for(int j=0;j<=i;j++)f[i]=(1llg[i][j]ksm(2,(n-i)j)+f[i])%mod;
for(int i=0;i<=n;i++){
int tmp=1llC[n][i]f[i]%modksm(2,bit[n-i])%mod;
if(i&1)(res+=mod-tmp)%=mod;else(res+=tmp)%=mod;
}
printf("%d\n",res);
return 0;
}
XXX.CF1608F MEX counting
根据奇怪限制,盲猜一手计数 DP。我们考虑要储存什么东西来描述状态:
i 表示 DP 了 i 位;
j 表示当前的 mex;
k 储存比 mex 大的数的出现信息。
然后我们简单猜到 k 事实上只要储存出现了多少个即可。i 是 O(n) 的,j 是 O(K) 的,k 是 O(n) 的。状态数 O(n2K),是可以接受的。但是我们看一下 fi,j,k 的转移:
(j+k)fi,j,k→fi+1,j,k,刷出了一个 [0,j−1] 或是 k 中的数。
fi,j,k→fi+1,j,k+1,刷出了一个 [j+1,n] 且不是 k 中的数。需要注意的是,这里我们并不关心刷出来的是什么:这要留到 mex 变动的时候再确定。
fi,j,k→fi+1,j′,k−(j′−j−1),即恰好刷出了当前的 mex,即 j;然后,新 mex 为 j′。
考虑这种转移的系数:其为 k!(k−(j′−j−1))!,也即 kj′−j−1―。
考虑转化定义:令一个辅助 DP gi,j,j+k=fi,j,k,则 (k−j)!(k+1−j′)!gi,j,k→gi+1,j′,k+1。上方只与 k,j 有关,下方只与 k+1,j′ 有关,则用一个前缀和就可以转移了。
复杂度 O(n2K)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,L[2010],R[2010],f[2][2010][4010],fac[4010],inv[4010],res;
int main(){
scanf("%d%d",&n,&m);
fac[0]=1;for(int i=1;i<=(n<<1|1);i++)fac[i]=1llfac[i-1]i%mod;
inv[n<<1|1]=ksm(fac[n<<1|1]);for(int i=n<<1|1;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=1,x;i<=n;i++)scanf("%d",&x),L[i]=max(x-m,0),R[i]=min(x+m,n+1);
L[0]=R[0]=0;
f[0][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=L[i];j<=R[i];j++)for(int k=0;k<=n;k++)f[i&1][j][j+k]=0;
for(int j=L[i-1];j<=R[i-1];j++)for(int k=j;k<=j+n;k++)if(f[!(i&1)][j][k])
(f[i&1][max(j+1,L[i])][k+1]+=1llf[!(i&1)][j][k]fac[k-j]%mod)%=mod,
(f[i&1][max(k+2,L[i])][k+1]+=mod-1llf[!(i&1)][j][k]fac[k-j]%mod)%=mod;
for(int j=L[i]+1;j<=R[i];j++)for(int k=j;k<=j+n;k++)
(f[i&1][j][k]+=f[i&1][j-1][k])%=mod;
for(int j=L[i];j<=R[i];j++)for(int k=j;k<=j+n;k++)
f[i&1][j][k]=1llf[i&1][j][k]inv[k-j]%mod;
for(int j=L[i-1];j<=R[i-1];j++)for(int k=j;k<=j+n;k++)if(f[!(i&1)][j][k])
(f[i&1][j][k]+=1llf[!(i&1)][j][k]k%mod)%=mod,
(f[i&1][j][k+1]+=f[!(i&1)][j][k])%=mod;
// for(int j=L[i];j<=R[i];j++)for(int k=0;k<=n;k++)
// printf("[%d,%d,%d]:%d\n",i,j,k,f[i&1][j][j+k]);
}
for(int j=L[n];j<=R[n];j++)for(int k=0;k<=n;k++)
if(k<=max(n-j,0))(res+=1llf[n&1][j][j+k]fac[max(n-j,0)]%mod*inv[max(n-j,0)-k]%mod)%=mod;
printf("%d\n",res);
return 0;
}
XXXI.CF1152F2 Neko Rules the Catniverse (Large Version)
一个显然的想法是,我们考虑对于无序数列 a,找到其合法的顺序数。
考虑递减遍历 a。当遍历到 ai 时,我们考虑 ai 相对之前的元素可能有什么位置关系:
插在之前所有元素后面。这显然是必然可行的。
插在两个元素中间。则只需保证这个数能处于其下一个数之前即可。
故我们即可令一个状态 fi,j,k 表示当前考虑到 i,当前序列已有 j 个数,下 m 位的出现状态是 k 即可。状态数是 O(k2m),套个矩阵快速幂问题就解决了。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,K,N;
struct Matrix{
int a[210][210];
Matrix(){memset(a,0,sizeof(a));}
intoperator[](const int&x){return a[x];}
friend Matrix operator(Matrix&u,Matrix&v){
Matrix w;
for(int i=0;i<N;i++)for(int j=0;j<N;j++)for(int k=0;k<N;k++)w[i][j]=(1llu[i][k]v[k][j]+w[i][j])%mod;
return w;
}
}M;
int KSM(){
Matrix R;
for(int i=0;i<N;i++)R[i][i]=1;
for(;n;n>>=1,M=MM)if(n&1)R=RM;
int res=0;
for(int j=0;j<(1<<m);j++)(res+=R[0][(K<<m)|j])%=mod;
return res;
}
int main(){
scanf("%d%d%d",&n,&K,&m),N=(K+1)<<m;
for(int i=0;i<=K;i++)for(int j=0;j<(1<<m);j++){
M[i<<m|j][(i<<m)|((j<<1)&((1<<m)-1))]=1;
if(i==K)continue;
M[i<<m|j][((i+1)<<m)|((j<<1)&((1<<m)-1))|1]=__builtin_popcount(j)+1;
}
printf("%d\n",KSM());
return 0;
}
XXXII.[AGC022F] Checkers
一个想法是把每次操作建成二叉树,左儿子是对称中心,右儿子是被对称点。然后从树根出发,每向左儿子走一步权值就翻倍,每向右儿子走一步权值就取反,求所有不同的叶节点权值可重集对应的多项式系数之和。
通过分析每种权值的正点和负点个数,讨论应该在何时把一个权为 i 的叶子分裂成一个 2i 的叶子和一个 −i 的叶子,我们可以有一个 n6 的方程,并可以通过前缀和简单优化到 n5。
到这里已经可以通过本题了,但是我们还可以做得更好。
二叉树的结构不是很好,我们把它重新写成一个常规树:被对称点向对称中心连边。
把树画在平面上,树根是最终剩下的那个点,然后每个点的儿子从左往右是它按照时间顺序的对称中心。
考虑分析一个点的权值:若其深度是 d,则权值是 2d;同时,每有一个儿子,权值就取反;其到根的路径中,每有一个在其右侧的兄弟,权值也取反。
这个模型的优势在于,所有权值相同的点都是同层的,分析起来就比前述的二叉树模型要简单……吗?
因为转移是有后效性的,所以这个分析甚至连一个合理的 DP 都设计不出来。我们必须设计一个 把未来的影响也规划入状态 的 DP。
设 fi,j 表示当前一共有若干层,且最后一层中有 j 个点 当前的符号与最终的符号不同。这个东西很好地考虑了后效性,但是唯一的问题就在于,我们关心的是 这层的点 中 符号为正 和 符号为负 的点 各自的数量。就算两个态要翻转的东西不同,它们也可能对应同构的信息。
注意到这层的点 仅会被下层的点影响。这意味着有限制的点实际上是对下层的叶子数的限制。
考虑一种最终方案,其中有两个点的符号不同,且都有奇数个儿子(也即都属于 j)。这时把它们都变成偶数个儿子(也即都不属于 j)会让 j 减少二,且两个点的符号都反转,也就是没有变动。
不断让一对同时被翻转的异号点同归于尽,我们最终得到的结论是 j 中对应的 所有节点都是同号的。
我们发现,这样一种翻转方案就唯一对应了一种正负个数方案。
考虑转移。我们枚举下层最终有 x 个正号和 y 个负号。
不妨令 j 中的点目标都是负号。则考虑 j 个奇儿子的点。不妨令它们都只有一个儿子(显然,因为它们当前是正的所以儿子应该也是正的),然后剩下的偶数随便乱放(会产生相等个数的正负号)。于是下层当前状态有 x+y−j2 个负号和 x+y+j2 个正号,还要修改 |x−x+y+j2|=|x−y−j2| 个符号才能与目标相同。
每次从 fi,j 转移到 fi+x+y,|x−y−j2|,其中 x,y 的限制是:
x+y+j 是偶数。
x+y≥max(j,1)。
复杂度四方。
到这里我们看向一开头给出的二叉树做法,就会发现差别所在:二叉树做法不可避免要在外层讨论权值 i,因为它的状态是成环的,必须有 i 维加以区分(或者,在观察式子后,确实有可以不讨论权值做到四方的做法,但是细节太多导致接下来优化到三方的式子会变得稀碎进而不是一般的难啃)。而树的状态很自然就摆脱了环的桎梏,进而可以忽略权值简单做到四方。
下面考虑怎么优化到三方。x,y 两维是相对独立的(对答案的系数贡献分别是 1x! 与 1y!),我们如果能把它们拆开就会很好办。
x 有关的是 (i+x,x−j) 两个东西。可以有 fi,j 先转移到 gi+x,x−j,然后由 gi,j 转移到 fi+y,|j−y2|。
注意到我们有三条限制:是偶数,大于 j,大于 1。
是偶数是简单的;大于 j 因为我们记录的是 x−j 所以也是简单的;比较麻烦的是大于 1 的限制。
这里可以对 x=0 时暴力转移,然后转移 g 数组的时候强制 x 非零即可。
复杂度就被优化到三方了。
代码:
include<bits/stdc++.h>
using namespace std;
int n,mod,f[510][510],g[510][1010],fac[510],inv[510],res;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
void ADD(int&x,const long long&y){x=(x+y)%mod;}
int main(){
freopen("rabbit.in","r",stdin);
freopen("rabbit.out","w",stdout);
scanf("%d%d",&n,&mod);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
f[1][1]=f[1][0]=1;
for(int i=1;i<=n;i++){
for(int j=-n;j<=n;j++)if(g[i][j+n]){
int y=max(0,-j);
if((y+j)&1)y++;
for(;i+y<=n;y+=2)ADD(f[i+y][abs(j-y)>>1],1llg[i][j+n]inv[y]);
}
for(int j=0;j<=n;j++)if(f[i][j]){
for(int x=1;i+x<=n;x++)ADD(g[i+x][x-j+n],1llf[i][j]inv[x]);
int y=max(1,j);
if((y+j)&1)y++;
for(;i+y<=n;y+=2)ADD(f[i+y][(y+j)>>1],1llf[i][j]inv[y]);
}
}
printf("%d\n",1llf[n][0]fac[n]%mod);
return 0;
}
XXXIII.送分题/[AGC055F]Creative Splitting
前一题似乎是后一题的加强版?
题意:定义一个长度为 n2 的序列是合法的,当且仅当其可被拆作 n 个长度为 n 的序列,满足每个序列中第 i 个数小于等于 i。
对序列中的每个 [1,n2] 中的位置以及其 [1,n] 中的取值,求出满足该位置恰好等于该值的合法序列数,对给定质数 p 取模。
数据范围:n≤30,108≤p≤109。
我们以下按照 AGC 那题的思路去讲,也即 分成 n 个长度为 k 的序列。
首先考虑如何判定一个序列合法。我们考虑贪心,从后往前扫过整个序列,维护当前所有 n 个序列的长度,初始全为 k。当扫到一个值时,显然把它扔到长度不小于它且最短的序列中是最好的。于是我们得到一个判定,也即维护一个数集 {a},每扫到一个数就把大于等于其的最小数减一即可。
现在我们要计数,钦定第 pos 个数是 val。则其会在第 nk−pos+1 轮被考虑到。
显然在考虑其时,我们只需知道当前维护的集合的信息即可。
我们考虑钦定一个集合,并求出:
产生该集合的方案数;
该集合考虑 val 得到的新集合,到全 k 态的方案数。
考虑把这个集合中的数依次画出来。比如 1,3,3,4,5 就对应了如下的东西:
. . . . #
. . . # #
. # # # #
. # # # #
# # #
1 3 3 4 5
其中,# 对应了存在的位置,而 . 对应了已经消掉的位置。
考虑某一步的影响。则其影响是让紧贴某个 . 右侧的 # 变成 .。
注意到在例子中的集合中,考虑 2 和考虑 3 的效果 都是令第二列最上方的 # 变成 .。
这对应了 恰拥有一个 . 的行共有两行。
故我们发现,令一个 p 序列表示 每行的 . 数,初始全为 0,然后 每次选择任一个 p 加一,然后生成最终 p 的方案数 即等于 按照原始问题得到当前枚举数集 的方案数。
换句话说,这建立了 p 的变化方案全集 与 能生成数集的序列全集 间的 双射。
另一种思路是,考虑原始问题的全体双射(例如不相交路径计数,但这并非好的双射),找到能够扩展的双射,然后发现其实际意义就是原始矩阵的转置计数。
在考虑 pos 之前,我们能找到的全体 p 态 唯一需要满足 的条件就是 ∑p=nk−pos,而并不需要满足单调性之类。
钦定一个 p 态,那么我们只需做从全 0 态到当前的 p 态的计数即可。
1 2 8 9 #
4 5 7 # #
6 # # # #
3 # # # #
# # #
1 3 3 4 5
如上,我们为每个 . 在其被删去的时刻标号。则其事实上是一个多项式系数的形式,为 (nk−pos)!∏pi!。
考虑 val 的影响。令 prex 表示小于 x 的 p 的个数,则 val 的影响即为令一个满足 prex<val≤prex+1 的 x,其对应的某个 pi=x 增加 1。
分析得后 n2−pos 位的方案数是 (pos−1)!(n−x)∏(n−pi)!。事实上,其等于 (pos−1)!∏(n−pi−oi)!,其中 oi 仅在 x 对应的 pi 处为 一,其它位上都为零,也即其为 val 的影响;把它乘出来,就是 (n−x)。
那么考虑 DP。枚举 val,然后钦定 p 有序,然后计数;记 cnti 为等于 i 的 p 数,则 prei=∑j<icntj;把 prei 和 pos=nk−∑p=nk−∑jcntj 压入状态,复杂度六方。
代码:
include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
typedef __uint128_t LLL;
struct FastMod {
ULL b, m;
void init(ULL b){this->b = b; m = ULL((LLL(1) << 64) / b);}
ULL operator()(ULL a){
ULL q = (ULL)((LLL(m) * a) >> 64);
ULL r = a - q * b;
return r >= b ? r - b : r;
}
} M;
int mul(int x, int y){return M((ULL)x * y);}
int n,mod,fac[1010],inv[1010],f[40][40][1010],g[1010][40],pov[40][40];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int main(){
freopen("antiak.in","r",stdin);
freopen("antiak.out","w",stdout);
scanf("%d%d",&n,&mod),M.init(mod);
fac[0]=1;for(int i=1;i<=nn;i++)fac[i]=mul(fac[i-1],i);
inv[nn]=ksm(fac[nn]);for(int i=nn;i;i--)inv[i-1]=mul(inv[i],i);
for(int p=0;p<=n;p++){
pov[p][0]=1;
for(int cnt=1;cnt<=n;cnt++)pov[p][cnt]=mul(pov[p][cnt-1],mul(inv[p],inv[n-p]));
for(int cnt=0;cnt<=n;cnt++)pov[p][cnt]=mul(pov[p][cnt],inv[cnt]);
}
for(int val=1;val<=n;val++){
f[0][0][0]=1;
for(int p=0;p<=n;p++)for(int pre=0;pre<=n;pre++)for(int sum=0;sum<=nn;sum++){
if(!f[p][pre][sum])continue;
// printf("%d,%d,%d:%d\n",p,pre,sum,qwq);
for(int cnt=0;pre+cnt<=n&&sum+pcnt<=nn;cnt++)
if(pre<val&&val<=pre+cnt)
ADD(f[p+1][pre+cnt][sum+pcnt],mul(mul(f[p][pre][sum],pov[p][cnt]),(n-p)));
else
ADD(f[p+1][pre+cnt][sum+pcnt],mul(f[p][pre][sum],pov[p][cnt]));
}
for(int sum=0;sum<nn;sum++)
// printf("%d,%d,%d:%d\n",n+1,n,sum,f[n+1][n][sum]),
g[nn-sum][val]=mul(mul(mul(f[n+1][n][sum],fac[sum]),fac[nn-sum-1]),fac[n]);
memset(f,0,sizeof(f));
}
for(int i=1;i<=n*n;i++,puts(""))for(int j=1;j<=n;j++)printf("%d ",g[i][j]);
return 0;
}
XXXIV.Permutation
题意:对长为 n 的排列计数,满足值 i 所在的 极长公差为一的等差数列(可能是递增或递减) 的长度不超过 ai+1,对 998244353。
数据范围:n≤5×103,1≤ai≤n。
首先一个观察是,我们可以把所有的极长等差数列所在的段写出来。比如说,如果钦定 [l,r] 中的值在同一段中,则 [l,r] 中所有 ai 都不超过 r−l。
可以按照值去 DP。DP 划分成若干段,则段间的顺序是阶乘,段内部的顺序有正放反放两种(除了段长为 1 的情形)。但需要保证,相邻的两段不能合并。
于是我们考虑容斥。对于 一组合法划分,计算 钦定其中有 k 个划分被连在一块 的方案数,有 (−1)k 的系数。
考虑 DP。设 fi,j,0/1 表示前 i 个数分成 j 段,且当前所在划分是否是其所在段中最后一段的方案数。预处理出从每个位置开始的最长划分,用前缀和即可优化到平方。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int n,a[5010],rp[5010],f[5010][5010][2],res,fac[5010];
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int main(){
freopen("permutation.in","r",stdin);
freopen("permutation.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1,j=1;i<=n;i++){
j=max(j,i);
while(j<=n){
bool ok=true;
for(int k=i;k<=j;k++)if(a[k]<=j-i){ok=false;break;}
if(!ok)break;
j++;
}
rp[i]=j-1;
}
// for(int i=1;i<=n;i++)printf("%d ",rp[i]);puts("");
f[0][0][1]=1,f[1][0][1]=mod-1;
for(int i=0;i<n;i++)for(int j=0;j<=i;j++)for(int t=0;t<2;t++)if(f[i][j][t]){
// printf("%d,%d,%d:%d\n",i,j,t,f[i][j][t]);
ADD(f[i+1][j][t],f[i][j][t]);
ADD(f[i+1][j+t][0],mod-f[i][j][t]);
ADD(f[rp[i+1]+1][j+t][0],f[i][j][t]);
ADD(f[i+2][j+t][1],(f[i][j][t]<<1)%mod);
ADD(f[rp[i+1]+1][j+t][1],mod-(f[i][j][t]<<1)%mod);
ADD(f[i+1][j+t][1],(f[i][j][t]<<!t)%mod);
ADD(f[i+2][j+t][1],mod-(f[i][j][t]<<!t)%mod);
}
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
for(int j=0;j<=n;j++)res=(1llfac[j]f[n][j][1]+res)%mod;
printf("%d\n",res);
return 0;
}
XXXV.CF1383E Strange Operation
考虑操作的影响:00 变成 0,10 变成 1,01 变成 1,11 变成 1。
这说明什么?说明 0 总是可以被删掉的,但是一段连续的 1 必然会剩下至少一个。
那么我们考虑用目标串在原串中匹配。原串可能由若干段 0 和 1 交替构成,且目标串中一段连续的 0 必然对应了原串中一段连续的 0。
于是我们考虑一个 DP:fi 表示前 i 段的结果。
转移考虑下一段的长度。如果下一段是 1(也即第 i 段是 0 段),则其可以向其后所有的 j,其中满足第 j 段是 1 段,转移 fi×lenj,其中 lenj 表示第 j 段的长度。其实际意义是,当下一段的 1 共有 (∑k∈(i,j)lenk,∑lenk+lenj] 个时,恰好会转移到 fi。我们发现 fj 的系数恰为 lenj。通过前缀和啥的随便搞搞,可以线性实现转移。
如果下一段是 0(也即第 i 段是 1 段),则枚举下一段的长度,我们会转移到长度大于等于该长度的第一段。用单调栈啥的乱搞搞,复杂度也可以做到线性。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,len[1001000],f[1001000],sum[2][1001000],stk[1001000],tp;
char s[1001000];
int main(){
scanf("%s",s+1),n=strlen(s+1);
if(s[1]!='0')len[++m]=0;
for(int l=1,r=1;l<=n;l=r){
while(r<=n&&s[r]s[l])r++;
len[++m]=r-l;
}
if(m1){printf("%d\n",n);return 0;}
if(s[n]=='0')m--;
for(int i=1;i<=m;i++){
sum[0][i]=sum[0][i-1],sum[1][i]=sum[1][i-1];
if(!(i&1)){
f[i]=1ll(sum[0][i-1]+len[1]+1)len[i]%mod;
(sum[1][i]+=f[i])%=mod;
}else{
f[i]=1lllen[i]f[i-1]%mod;
while(tp&&len[stk[tp]]<len[i])
f[i]=(1ll(len[i]-len[stk[tp]])(sum[1][stk[tp]]+mod-sum[1][stk[tp-1]])+f[i])%mod,tp--;
stk[++tp]=i;
(sum[0][i]+=f[i])%=mod;
}
// printf("%d,%d:%d\n",i,len[i],f[i]);
}
printf("%d\n",1llsum[1][m](len[m+1]+1)%mod);
return 0;
}
XXXVI.CF722E Research Rover
一眼看上去,这不是经典老题VI.CF559C Gerald and Giant Chess嘛。S 除 O(logS) 次就会衰变到 1,于是我们就只需对于 i∈[1,logS] 求出 恰经过 i 个点的方案数,然后计算即可。
令 fi,j 表示 以特殊点 i 结尾且经过恰 j 个点的方案数。
考虑容斥,用总方案数减去经过超过 j 个点的方案数,然后再减去恰为 1∼j−1 的方案数。总方案数可以简单知;超过 j 个点的方案数可以在路径中的第 j 个点处统计。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,K,S,f[2010][30],fac[200100],inv[200100],res;
pair<int,int>p[2010];
int C(int x,int y){return 1llfac[x+y]inv[x]%modinv[y]%mod;}
int main(){
scanf("%d%d%d%d",&n,&m,&K,&S);
fac[0]=1;for(int i=1;i<=n+m;i++)fac[i]=1llfac[i-1]i%mod;
inv[n+m]=ksm(fac[n+m]);for(int i=n+m;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=1;i<=K;i++)scanf("%d%d",&p[i].first,&p[i].second);
sort(p+1,p+K+1);
if(p[K]!=make_pair(n,m))p[++K]=make_pair(n,m),S<<=1;
if(p[1]!=make_pair(1,1))p[++K]=make_pair(1,1),S<<=1;
sort(p+1,p+K+1);
f[1][1]=1;
for(int i=2;i<=K;i++){
for(int k=2;k<30;k++){
f[i][k]=C(p[i].first-1,p[i].second-1);
for(int j=1;j<i;j++)if(p[j].first<=p[i].first&&p[j].second<=p[i].second)
(f[i][k]+=mod-1llf[j][k]C(p[i].first-p[j].first,p[i].second-p[j].second)%mod)%=mod;
}
for(int k=29;k>=2;k--)(f[i][k]+=mod-f[i][k-1])%=mod;
}
int all=C(n-1,m-1);
for(int k=1;k<30;k++){
S=(S+1)>>1;
res=(1llSf[K][k]+res)%mod;
(all+=mod-f[K][k])%=mod;
}
(res+=all)%=mod;
printf("%d\n",1llres*ksm(C(n-1,m-1))%mod);
return 0;
}
XXXVII.CF1626F A Random Code Problem
一个常规的想法是,既然 K 这么小,那我们状压吧!
但是状压的复杂度是 n2K 这种要人命的东西。
因为与取模相关,所以我们条件反射用一发 lcm。
但是 1∼17 中所有数的 lcm 是 12252240,看上去不太妙。
但是注意到我们 并不关心其被 17 取模的结果。于是用 1∼16 的 lcm,720720 即可。
考虑每个数,计算其在多少次流程中出现,简单 DP 即可。
复杂度 O(lcm(1∼16)×17+n)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
const int lcm=720720;
int n,a,X,Y,K,M,res,sum;
int f[lcm],g[lcm],F[lcm],G[lcm];
int main(){
scanf("%d%d%d%d%d%d",&n,&a,&X,&Y,&K,&M);
for(int =0;<n;_++)f[a%lcm]++,(sum+=a/lcm)%=mod,a=(1llaX+Y)%M;
sum=1llsumlcm%mod;
res=1llKsum%modksm(n,K-1)%mod;
// printf("%d\n",res);
for(int i=1;i<=K;i++){
for(int j=0;j<lcm;j++){
// if(f[j]||g[j])printf("[%d:%d,%d]",j,f[j],g[j]);
F[j]=(1llf[j](n-1)+F[j])%mod;
G[j]=(1llg[j](n-1)+G[j])%mod;
(F[j/ii]+=f[j])%=mod;
G[j/ii]=(1llf[j]j+g[j]+G[j/ii])%mod;
}//puts("");
for(int j=0;j<lcm;j++)f[j]=F[j],g[j]=G[j],F[j]=G[j]=0;
}
for(int j=0;j<lcm;j++)(res+=g[j])%=mod;
printf("%d\n",res);
return 0;
}
XXXVIII.CF830D Singer House
考虑路径中的最高点。定义 fi 表示最高点距叶子的距离是 i 的方案。
考虑如何构建路径。显然,如果路径同时有在两个儿子中的点,则从一棵子树到另一棵子树必须经过当前点。于是我们可以定义 gi 表示一条非空路径完全在 i 中的方案数——显然 g 可以简单由 f 推知,然后转移则有 2(gi−1+1)2−1→fi。其中,gi−1+1 是在子树中选择一条路径(可能为空)的方案数,平方表示把两个儿子的路径并起来,乘二是表示两个儿子都可以作为起点,减一是因为当路径仅有一个点时,方向是无意义的。
否则,路径完全来自某个儿子。这等价于在那个儿子中寻找 两条无公共点的有序路径 的方案数。设 hi 表示在 i 中寻找两条非空无公共点的路径数,则有 2hi−1→fi。乘二是表示从哪个儿子转移。h 因为是有序的,所以就直接把路径表示为“第一条路径 →i→ 第二条路径”即可。
现在考虑 h 应该如何求。设 d 表示 有一条路径经过根节点 的 h,则由 d 可简单推知 h。
若有一条路径严格横跨两个子树,则包含两条路径的子树的方案数是 hi−1,包含一条路径的子树的方案数是 gi−1(往路径后面直接接东西),合并两边要乘上 4 的系数(枚举哪个儿子作为贡献 d 的子树;枚举跨越 i 的方式是从 h 往 g 还是从 g 往 h),得到式子为 4hi−1gi−1→di。
若两条路径 不同时在一棵子树,则必然有一侧子树是作为 g 贡献,另一侧子树是作为 h 贡献。列式子发现是 2gi−1(hi−1+2gi−1+1)→di,其中 2 是枚举哪棵子树,g 是那棵子树的贡献,hi−1 是 i 作为另一棵子树的中继点的方案数,2gi−1 是 i 接在另一棵子树中路径的开头或结尾的方案数,1 是 i 自己单独作为路径的方案数)。
否则,两条路径同时在一棵子树。若 i 作为一条路径的开头或结尾,则会有一个 8h 的系数,分别来自枚举儿子、枚举哪条路径、枚举路径开头结尾。
最后,i 必然是作为一条路径的中继点,从一个儿子来,又回到这个儿子。这表明这个儿子中需要三条不交路径。
这不对吧?
我们思考一下,我们的 i 究竟有什么含义:在最终方案中,有多少条路径从 i 的儿子上来,又回到这个儿子。
显然,每进行一次该操作,都有一个父亲会被占用,故有意义的路径数是 O(n) 级别的。
我们发现,直接按照这个定义去搞,或许就可以了。
设 fi,j 表示最终路径中,“从某个父亲向 i 子树中有一条路径”这个事件总共发生 j 次的方案数。
发现更好的等价定义是,gi,j 表示 i 子树中划 j 条无交路径的方案数。然后令 f 表示恰一条路径经过 i 的方案数。则之前的 f,g,h,d 数组都是其中 j=1/2 的特例。转移要枚举两边子树各自划多少条路径,是一个类卷积的形式,可以三方暴力搞。
这道题给我们的启示是,像这种二叉树问题,因为子树节点很多所以不能从父亲出发找儿子,而应该从儿子出发找父亲,因为父亲的数量是 O(n) 的。有了这个观察,状态的设计将会更加容易。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[410][410],g[410][410];
int main(){
scanf("%d",&n);
g[0][0]=f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++)for(int k=0;k<=n;k++){
if(j+k+1<=n)f[i][j+k+1]=(1llg[i-1][j]g[i-1][k]+f[i][j+k+1])%mod;//take i as individual path.
if(j+k<=n)f[i][j+k]=(2llg[i-1][j]g[i-1][k]%mod(j+k)+f[i][j+k])%mod;
//choose a path,and insert i in the front or at the back of it.
if(j+k>=2&&j+k-1<=n)
f[i][j+k-1]=(1ll(j+k)(j+k-1)%modg[i-1][j]%modg[i-1][k]+f[i][j+k-1])%mod;
//use i to fuse two paths.
}
for(int j=0;j<=n;j++)for(int k=0;k<=n;k++)
if(j+k<=n)g[i][j+k]=(1llg[i-1][j]*g[i-1][k]+g[i][j+k])%mod;
for(int j=0;j<=n;j++)(g[i][j]+=f[i][j])%=mod;
// for(int j=0;j<=n;j++)printf("[%d,%d]",f[i][j],g[i][j]);puts("");
}
printf("%d\n",g[n][1]);
return 0;
}
XXXIX.CF1528E Mashtali and Hagh Trees
首先先来分析一下“朋友”关系的性质。
考虑两个标志性的结构:a→c,b→c 和 d→e,d→f。
假如不满足 c→d,则 a,b 两点必然各自通过一条路径与 d 相连,或者被 d 连到。也就是说,a→d,b→d 与 a→c,b→c 成环。
也就是说,我们可以把结构描述为:一棵内向树以一条链连到一棵外向树。
因为出入度小于等于三,所以这是一个类二叉树结构。于是设 fi 表示深度为 i 的二叉树数,然后简单用前缀和什么的乱搞搞即可。
具体而言,我们要计数:
根节点度数不超过三的内向树和外向树数。
剔除同时是外向树和内向树的树数(即为链数,等于 1)。
内外向树拼接的方案数。钦定其中一个节点度数为二以避免重复计数。还要减去其退化成内向树或外向树的方案。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int inv2=499122177;
const int inv6=166374059;
int n,f[1001000],g[1001000];
int main(){
scanf("%d",&n);
f[0]=g[0]=1;
for(int i=1;i<=n;i++){
if(i>=2)f[i]=1llf[i-1]g[i-2]%mod;//a son of i-1 and a son less than i-1
(f[i]+=1llf[i-1](f[i-1]-1)%modinv2%mod)%=mod;//two different sons of i-1
(f[i]+=f[i-1])%=mod;//two same sons of i-1
(f[i]+=f[i-1])%=mod;//one son of i-1.
g[i]=(g[i-1]+f[i])%mod;
}
int res1=0;if(n>=2)res1=1llf[n-1]g[n-2]%mod(g[n-2]-1)%modinv2%mod;//one son of n-1 and two different sons less than n-1
int res2=0;if(n>=2)res2=1llf[n-1]g[n-2]%mod;//one son of n-1 and two same sons less than n-1
int res3=0;if(n>=2)res3=1llf[n-1]g[n-2]%mod;//one son of n-1 and one son less than n-1
int res4=0;if(n>=2)res4=1llf[n-1](f[n-1]-1)%modinv2%modg[n-2]%mod;//two different sons of n-1 and one son less than n-1
int res5=0;if(n>=2)res5=1llf[n-1]*g[n-2]%mod;//two same sons of n-1 and one son less than n-1
int res6=1ll*f[n-1]*(f[n-1]-1)%mod*(f[n-1]-2)%mod*inv6%mod;//three different sons of n-1
int res7=1ll*f[n-1]*(f[n-1]-1)%mod;//two same sons of n-1 and one different son of n-1
int res8=f[n-1];//three same sons of n-1
int res9=1ll*f[n-1]*(f[n-1]-1)%mod*inv2%mod;//two different sons of n-1
int res10=f[n-1];//two same sons of n-1;
int res11=f[n-1];//one son of n-1;
// printf("%d %d %d %d %d %d %d %d %d %d %d\n",res1,res2,res3,res4,res5,res6,res7,res8,res9,res10,res11);
int ser1=(0ll+res1+res2+res3+res4+res5+res6+res7+res8+res9+res10+res11)%mod;
int ser2=(2ll*ser1+mod-1)%mod;
int ser3=0;
for(int i=1;i+1<n;i++){
int ans1=0;if(i>=2)ans1=1ll*f[i-1]*g[i-2]%mod;
int ans2=1ll*f[i-1]*(f[i-1]-1)%mod*inv2%mod;
int ans3=f[i-1];
int sna1=(0ll+ans1+ans2+ans3)%mod;
int sna2=(f[n-i-1]+mod-1)%mod;//substract the situation of solely a chain
// printf("%d:%d,%d\n",i,sna1,sna2);
ser3=(1ll*sna1*sna2+ser3)%mod;
}
int qwq=(ser2+ser3)%mod;
printf("%d\n",qwq);
return 0;
}
XL.[TopCoder12909]Seatfriends
首先先重定义一下变量名:令环长为 n,时刻数为 m,连通块上限为 K。
我们考虑最终剩下 p 个连通块。那我们接下来分配空格的过程是简单的。
于是我们便仅需对连通块数量 DP。记 fi,j 表示放入 i 个人,成 j 个连通块的方案数。
考虑转移。新加入的人可以:
插入某段。
用其把两段合并。
用其新建一段。
但需要保证任意时刻 i+j≤n,不然态就不合法。
考虑对于最终的 fn,j 计算其对应的态数。
我们只需钦定第一个人在环的第一个位置,然后为 j−1 个间隔分配方案即可。
复杂度平方。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int f[2010][2010],C[2010][2010];
class Seatfriends{
private:
public:
int countseatnumb(int n,int m,int K){
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
f[1][1]=1;
for(int i=1;i<m;i++)for(int j=1;j<=i&&j<=K&&i+j<=n;j++){
(f[i+1][j+1]+=1lljf[i][j]%mod)%=mod;//new a cluster
(f[i+1][j]+=2lljf[i][j]%mod)%=mod;//add into a cluster
(f[i+1][j-1]+=1lljf[i][j]%mod)%=mod;//fuse two clusters
}
if(n==m)return 1llf[m][0]n%mod;
int res=0;
for(int i=1;i<=K&&m+i<=n;i++)res=(1llf[m][i]C[n-m-1][i-1]+res)%mod;
return 1llresn%mod;
}
}my;
XLI.[GYM102056E]Immortal … Universe
首先考虑对于一个态,判定其是否合法。
考虑我们的决策:当资金数仅为一的时候,
如果接下来两个态都是盈利态,则显然我们无论选哪个都是可以的(因为选择一个态不会使得另一个盈利态变得不合法,换句话说就是盈利态无论何时选,只要选了都是优的)。
只有一个态是盈利态,则我们必然选择之。
都是亏空态,那就输了。
考虑任何一个态,其可以用二元组 (a,b) 来描述,分别表示第一支股票的前 a 个态和第二支股票的前 b 个态都经历过了。当前持有的资金数可以简单算出。
不合法当且仅当存在一个态 (a,b),满足其可达、资金数为一、不可一步走到盈利态。
后两个条件的判定是简单的。第一个条件需要我们分析一下。
不可达会在什么时候发生呢?
在之前的某个时刻就已经破产了。但是不管这个条件,也能得到其已经破产的信息,故放置不管即可。
被资金数为一时的强制操作变得不可达了。
考虑该强制操作。其仅可能在资金数为一、只有一个态是盈利态时是强制的。
但是我们考虑分析一下:因为我们在非强制时的操作可以看作是任意的,那么我们可以钦定一种操作序列达到目标态。
发现,我们钦定的操作序列,不主动往赔钱态走 是其合法的充分条件(但不一定必要)。
考虑 (a,b) 态。不妨假定 a≤b。我们首先考虑沿着第一支股票一路狂奔。
假如奔到了一个资金数为一且第一支股票下一个态是亏空态的态,则下一步可以动第二支股票。
假如第二支股票的下一步不是盈利态,则这事实表明,我们可达一个破产态,在判定该破产态时就已经表明当前盈亏序列不合法了。
假如下一步是盈利态,那走一步必然是合法的。
不管怎么说,我们这样搞,总能在第一支股票上执行完前 a 个态。此时,第二支股票已执行的态不超过 a,因为只有在第一支股票上亏到资金为一时我们才会动第二支股票,故第二支股票执行的态数不超过 a。
那么因为第一支股票的 a+1 态是亏损态或者压根不存在,所以接下来我们不断动第二支股票必然是合法的。则我们必然可以到达 (a,b) 态(不管之前会不会到达一个破产态)。
上述证明表明了,对于一个 (a,b),假如不考虑 中道崩殂 中途破产,就总是可达的。
于是,我们证明了,盈亏序列不合法当且仅当存在一个态 (a,b),满足其资金数为一、不可一步走到盈利态。
那么我们考虑计数。
我们考虑在所有满足不可一步走到盈利态的 (a,b) 中,找到满足资金数最少的态。假如该资金数小于等于一(可能是负数,但是为了避免重复计算我们将其延迟到此处计算)则其显然不合法,否则合法。
不可一步走到盈利态,这个东西关于两支股票是相对独立的。我们只需对两支股票分别找到所有满足下一步是亏损态的态中资金数最小的态,然后判定它们的和是否小于等于零即可。
考虑这需要我们记录的信息:下标、前缀和、前缀最小和,这好像是三方的。
咋办呢?考虑容斥。
我们考虑对于每个值,计算前缀最小和小于等于其的方案数。
那么我们钦定一个位置,让此处的前缀最小和等于该值,然后还要钦定其下一个位置必须填入 −1。令 fi 表示前缀 i 的最小值等于该值的方案数,gi 表示其是首次等于该值的方案数。则最终方案数是 ∑i=1n−1gi2?,其中 ? 是一个对于钦定的每个值均相同的常数。
考虑从 f 容斥得出 g。有 gi=fi−∑j=0i−1gj?,其中 ? 仍是一个对于钦定的每个值均相同的常数。
虽然我们不能对钦定的每个值都进行一遍容斥(那是三方的),但是我们可以做的是 平方预处理出每个 fi 对答案的贡献系数,然后对于每个钦定的值代入其对应的 f,解出答案。
需要特别注意的是,前缀最小和有可能为整个串的和。此时整个串的和不能贡献给另一个串的整串和,可以特别再 DP 一下。
另外还需要特别注意的是,前缀最小和要分小于等于零和大于零两种情形讨论:前者中,前缀最小和等于 K 时,必然曾经经过 K+1 的态;而后者却不是这样。
但对于后者,假如前缀最小和是 K,我们仍可以计算钦定前 K 个位置全部为 1 后,压到直线上以及压到直线下的方案数之差。
复杂度平方。
但是上述算法细节多到爆炸,并且我还写挂了。
于是我们考虑另一种解法,即倒序 DP。
倒序 DP 的思想非常 trivial:fi,j 表示 DP 到第 i 位,当前的前缀 min 为 j。
考虑转移。假如填入 −1,则有 fi,j→fi−1,min(j−1,0)。
假如填入 1,则有 fi,j→fi−1,j+1。
我们另外还要再记一维,表示当前的前缀最小值是不是来自于全局和。
这比起上面的容斥方法不知道要好到哪里去了。
合并两段中前缀最值信息往往需要维护两段中各自的前缀最值信息,以及段内所有元素之和。
但是,假如我们把合并写成树的形式,则 树上最右的叶子及其祖先 的元素和是永远不会被使用的。
这意味着假如能够始终保证合并的段是右侧段,则事实上段内元素和就不用使用。
反映到 DP 中就是倒序 DP。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int n,m,f[2][10010][2],g[2][10010][2],res;
char s[5010];
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
void DP(){
memset(f,0,sizeof(f));
f[n&1][n][0]=1;
for(int i=n;i;i--)for(int j=-n;j<=n;j++)for(int k=0;k<2;k++)if(f[i&1][j+n][k]){
if(s[i]!='P')ADD(f[!(i&1)][j+n+1][k],f[i&1][j+n][k]);
if(s[i]!='V')ADD(f[!(i&1)][min(0,j-1)+n][k|(0<=j-1)],f[i&1][j+n][k]);
f[i&1][j+n][k]=0;
}
}
int main(){
scanf("%s",s+1),n=strlen(s+1),DP(),memcpy(g[0],f[0],sizeof(f[0]));
scanf("%s",s+1),m=n,n=strlen(s+1),DP(),memcpy(g[1],f[0],sizeof(f[0]));
swap(n,m);
for(int i=-n;i<=n;i++)for(int I=0;I<2;I++)if(g[0][i+n][I])
for(int j=-m;j<=m;j++)for(int J=0;J<2;J++)if(g[1][j+m][J])
if(i+j>=(I||J))res=(1llg[0][i+n][I]g[1][j+m][J]+res)%mod;
printf("%d\n",res);
return 0;
}
XLII.[清华集训2017]某位歌姬的故事
按照套路,我们把限制拆成两条:区间 [li,ri] 中所有数都不大于 ai;区间 [li,ri] 中存在某个数等于 ai。
那么第一条限制是比较好处理的:我们将 ai 递增排序,对于 ai 相同的限制一起考虑。已经被考虑的限制(即 aj<ai 的限制)显然满足第一条限制且不会贡献给第二条限制,故可以 直接把它们从序列中删除。然后问题就转换为,给定一个空序列上的若干 [li,ri],要求区间中所有数都不大于 ai 且存在某个数等于 ai。假如在删除已被考虑的限制后,有某个 [li,ri] 变空了,则显然不存在解。
下一步处理也是比较套路的:对于 [li,ri]⊆[lj,rj],显然只要 [li,ri] 的第二条限制被满足,[lj,rj] 的第二条限制亦被满足,只需考虑其第一条限制即可。于是我们先把 [lj,rj] 删去,得到若干两两不具有包含关系的 [li,ri],计算其方案数后,再考虑所有不包含在所有 [li,ri] 且包含在某个 [lj,rj] 中的位置的影响(显然,即为 ai 的这些位置数量次幂)即可。
现在考虑 [li,ri] 的部分。其可以被表述为:给定一个空序列上的若干两两不包含的 [li,ri],要求区间中所有数都不大于 ai 且存在某个数等于 ai。
按照套路,两两不互相包含等价于 将左端点递增排序后,右端点也会递增排序。换句话说,DP 中很重要的一点,即 DP 顺序,已经有端倪了:我们只需取左端点递增序(也即右端点递增序),按照这个顺序排序即可。
限制是区间中存在某个数等于 ai。按照套路,我们考虑容斥:钦定若干个区间中不存在 ai,然后求出方案数乘上容斥系数并求和即可。
那么我们就可以尝试构建 DP 了:定义 fi 表示钦定第 i 个区间中不存在 ai 的方案数。则对于 j>i 可以转移到 fj,要分第 j 个区间和第 i 个区间有交还是无交考虑:假如有交,贡献就是 (ai−1) 的 [lj,rj] 剔除交集的大小 K 次幂,也即 (ai−1)rj−ri;假如无交,则贡献是 (ai−1)rj−lj+1ailj−ri−1。需要注意的是,这个式子中 ailj−ri−1 一项是夹在两个区间之间的位置填数的方案数;但是问题在于,被夹在区间中的部分不一定全能填(其中可能有一些空白位置)。因此我们可以在离散化的时候预先处理好边界问题,把不需要的位置扔掉或者额外记录区间中有多少存在的位置。
第一类转移关于 i,j 是独立的,可以差分简单转移;第二类转移可以被拆成 pjpi 的形式,其中 p 是某种前缀积,可以前缀和简单转移。(可能需要对 ai=1 的情形特别处理一下——但是 ai=1 时所有位置都必然填 1,于是可以直接忽略即可)
总复杂度做到 QlogA 之类总是简单的。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int N,A,q,l[510],r[510],a[510];
vector
set
int L[510],R[510],n,m;
int P[1010],len[1010],sum[1010];
int f[510],s1[510],s2[510];
int amin(int ){
// printf("%d(%d):\n",,v[]);
U.clear(),V.clear(),n=m=0;
for(auto x:w[]){
auto it=s.lower_bound(l[x]);
if(its.end())return 0;
l[x]=it;
it=s.lower_bound(r[x]);
if(it!=s.end())r[x]=it;else r[x]=N;
}
sort(w[].begin(),w[].end(),[](int x,int y){return l[x]l[y]?r[x]>r[y]:l[x]<l[y];});
for(auto x:w[]){
while(!U.empty()&&r[U.back()]>=r[x])V.push_back(U.back()),U.pop_back();
U.push_back(x);
}
for(auto x:U){
auto it=s.lower_bound(l[x]);
while(it!=s.end()&&it<r[x])P[m++]=it,it=s.erase(it);
}
int num=0;
for(auto x:V){
auto it=s.lower_bound(l[x]);
while(it!=s.end()&&it<r[x])num+=u[it+1]-u[*it],it=s.erase(it);
}
if(v[]==1)return 1;
// printf("EXT:%d\n",num);
num=ksm(v[],num);
sort(P,P+m);
for(int i=0;i<m;i++)len[i]=u[P[i]+1]-u[P[i]],sum[i+1]=sum[i]+len[i];
for(auto x:U)L[n]=lower_bound(P,P+m,l[x])-P,R[n]=lower_bound(P,P+m,r[x])-P,n++;
// for(int i=0;i<n;i++)printf("<%d,%d>",L[i],R[i]);puts("");
// for(int i=0;i<m;i++)printf("[%d,%d]",P[i],u[P[i]+1]-u[P[i]]);puts("");
// for(int i=0;i<=m;i++)printf("%d ",sum[i]);puts("");
for(int i=0;i<n;i++)f[i]=s1[i]=s2[i]=0;
int res=0;
for(int i=0,j=0;i<n;i++){
f[i]=mod-(1lls1[i]ksm(v[]-1,sum[R[i]])+1ll(s2[i]+1)ksm(v[],sum[L[i]])%mod*ksm(v[]-1,sum[R[i]]-sum[L[i]]))%mod;
// printf("<%d,%d>:%d\n",s1[i],s2[i],f[i]);
res=(1llf[i]ksm(v[],sum[m]-sum[R[i]])+res)%mod;
while(j<n&&R[i]>L[j])j++;
// printf("|%d,%d|\n",i,j);
s1[i+1]=(1llf[i]ksm(v[]-1,mod-1-sum[R[i]])+s1[i+1])%mod;
(s1[j]+=mod-1llf[i]ksm(v[]-1,mod-1-sum[R[i]])%mod)%=mod;
s2[j]=(1llf[i]ksm(v[],mod-1-sum[R[i]])+s2[j])%mod;
(s1[i+1]+=s1[i])%=mod;
(s2[i+1]+=s2[i])%=mod;
}
// printf("%d\n",res);
res=1ll(ksm(v[_],sum[m])+res)num%mod;
// printf("%d\n",res);
return res;
}
void mina(){
s.clear(),u.clear(),v.clear();
scanf("%d%d%d",&N,&q,&A);
for(int i=1;i<=q;i++)
scanf("%d%d%d",&l[i],&r[i],&a[i]),r[i]++,u.push_back(l[i]),u.push_back(r[i]),v.push_back(a[i]);
u.push_back(1),u.push_back(N+1);
sort(u.begin(),u.end()),u.resize(unique(u.begin(),u.end())-u.begin());
sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
for(int i=0;i<v.size();i++)w[i].clear();
for(int i=0;i+1<u.size();i++)s.insert(i);
for(int i=1;i<=q;i++)
l[i]=lower_bound(u.begin(),u.end(),l[i])-u.begin(),
r[i]=lower_bound(u.begin(),u.end(),r[i])-u.begin(),
a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin(),
w[a[i]].push_back(i);
int res=1,num=0;
for(int i=0;i<v.size();i++)res=1llresamin(i)%mod;
for(auto x:s)num+=u[x+1]-u[x];
// printf("<%d,%d>\n",res,num);
res=1llresksm(A,num)%mod;
printf("%d\n",res);
}
int T;
int main(){/freopen("1.in","r",stdin);/scanf("%d",&T);while(T--)mina();return 0;}
XLIII.[JSOI2019]神经网络
考虑哈密顿回路,其中包含若干某棵树中的边,还有若干连接树之间的边。
考虑所有连续、极长、来自某棵树的边。其显然构成这棵树中的一条路径。
则整条哈密顿路径由若干树中路径,再加上链接树中路径的树间路径拼接而成。
考虑一棵树:其被裁成若干路径。我们记 fi(x) 表述第 i 棵树裁成 x 条路径(路径是有向的,即 x→y≠y→x;但是,对于仅由单点组成的路径,其并没有方向之分)的方案数。考虑钦定一组 x1,x2,…,把 f1(x1),f2(x2),… 拼成一条哈密顿路径,其要求仅仅是 来自相同树的路径不能在哈密顿路径中相邻。
fi(x) 可以简单树形 DP 求出。则现在的问题可以被转换成,有若干种颜色的珠子,第 i 种颜色有 ai 个且两两有区别,将它们串成环,求同色珠子不相邻的方案数。
首先先把仅有一种颜色的方案扔掉。然后考虑容斥:用任意排的方案数,减去钦定某一对同色珠子相邻的方案数,加上两对……
枚举 {b} 表示第 i 种颜色最终缩成了仅剩 bi 段。则系数是 (−1)∑a−∑b(∑b−1)!;ai 个珠子拼出 bi 段的方案数,是 ai!bibi,其中 ∑i=1xi 是一段的 EGF,ai! 把 EGF 转成 OGF,1bi! 是因为段间顺序并没有影响。其可以被化成 ai!bi
bi=(ai)![xai−bi]1(1−x)bi=ai!bi!(ai−bi+bi−1bi−1)=ai!bi!(ai−1bi−1)。这个式子也是很符合直觉的,我们考虑其组合意义:考虑 bi 段,为它们定下顺序并排成一列就得到了 ai 的排列的一组划分,而划分数就是 (ai−1bi−1)。
对于一棵树,其有效信息仅有 b;于是我们预处理出 gi(bi) 表示每个 bi 的贡献,然后不同的树间的 g 就是卷积合并的过程。
暴力卷积合并的复杂度就是平方。
代码:
include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=998244353;
void ADD(int&x,ll y){x=(x+y)%mod;}
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int m,n,A,sz[5010];
vector
int f[5010][5010][3],g[5010][3];//isolation;source or sink;relay
int h[5010],d[5010],e[5010],inv[5010],fac[5010],res;
void dfs(int x,int fa){
sz[x]=1,memset(f[x][1],0,sizeof(f[x][1])),f[x][1][0]=1;
for(auto y:v[x])if(y!=fa){
dfs(y,x);
for(int i=1;i<=sz[x];i++)for(int I=0;I<3;I++)for(int j=1;j<=sz[y];j++)for(int J=0;J<3;J++){
ADD(g[i+j][I],(J1?2ll:1ll)f[x][i][I]f[y][j][J]);
if(I2||J2)continue;
if(I0)ADD(g[i+j-1][1],1llf[x][i][I]f[y][j][J]);
if(I1)ADD(g[i+j-1][2],2llf[x][i][I]f[y][j][J]);
}
sz[x]+=sz[y];
for(int i=1;i<=sz[x];i++)for(int I=0;I<3;I++)f[x][i][I]=g[i][I],g[i][I]=0;
}
}
void mina(){
scanf("%d",&n);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
for(int i=1;i<=n;i++)d[i]=(f[1][i][0]+2llf[1][i][1]+f[1][i][2])%mod;
// for(int i=1;i<=n;i++)printf("%d ",d[i]);puts("");
for(int b=1;b<=n;b++){
e[b]=0;
for(int a=b;a<=n;a++){
int lam=1llfac[a-1]inv[b-1]%modinv[a-b]%modfac[a]%modinv[b]%modd[a]%mod;
// printf("(%d,%d):%d\n",a,b,lam);
if((a-b)&1)lam=mod-lam;
(e[b]+=lam)%=mod;
}
}
// for(int i=1;i<=n;i++)printf("%d ",e[i]);puts("");
for(int i=0;i<=A+n;i++)d[i]=0;
for(int i=0;i<=A;i++)for(int j=1;j<=n;j++)ADD(d[i+j],1llh[i]*e[j]);
A+=n;for(int i=0;i<=A;i++)h[i]=d[i];
for(int i=1;i<=n;i++)v[i].clear();
}
int main(){
scanf("%d",&m);
if(m1){puts("0");return 0;}
h[0]=1;
for(int i=1;i<=m;i++)mina();
fac[0]=1;for(int i=1;i<=A;i++)fac[i]=1llfac[i-1]i%mod;
for(int i=1;i<=A;i++)ADD(res,1llh[i]fac[i-1]);
printf("%d\n",res);
return 0;
}
XLIV.[JSOI2018]防御网络
萌萌小可爱题。
首先图是点仙人掌。我们可以将图划分成若干部分,每部分要么是环,要么是环间的边。
对于环间的边,其出现在斯坦纳树上,当且仅当其两侧都有至少一个点。直接统计两侧点数即可得到这一概率。
对于环,我们首先可以对于每个点求出其侧存在至少一个点的概率,然后我们的目标是把所有出现至少一个点的环上点连接。我们把所有这样的点列出来,则其中仅有一对相邻点间的边可以被贪掉。显然我们选择距离最大的边即可。
断环成链,然后令 fl,r,c 表示区间 [l,r] 中相隔最远的两个点距离不超过 c 的概率。转移可以用前缀和简单做到 O(1)。然后用 Ex=∑P(x≥i) 即可计算期望。复杂度可以简单对每个环做到
环
长
O(环长3),而
环
长
O(∑环长3)=O(n3)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
const int inv2=ksm(2);
int n,m,dfn[210],low[210],stk[210],tp,tot,bin[210],res;
vector
int col[210],c;
void Tarjan(int x,int fa){
dfn[x]=low[x]=++tot,stk[++tp]=x;
for(auto y:v[x])if(y!=fa){
if(!dfn[y])Tarjan(y,x),low[x]=min(low[x],low[y]);
else low[x]=min(low[x],dfn[y]);
}
if(dfn[x]!=low[x])return;
c++;int y;do col[y=stk[tp--]]=c;while(y!=x);
}
int sz[210];
bool vis[210],siv[210];
void countsz(int x){vis[x]=true,sz[x]=1;for(auto y:v[x])if(!vis[y])countsz(y),sz[x]+=sz[y];}
int countsz(int x,int fa){vis[fa]=true,countsz(x);return sz[x];}
int prob[210],K;
void dfs(int x){
siv[x]=vis[x]=true;
for(auto y:v[x])if(col[y]col[x]&&!siv[y])dfs(y);
prob[++K]=(1+mod-bin[countsz(x,0)]);
}
int f[210];
int pro[210],orp[210],sum[210];
void calc(){
if(K1)return;
// for(int i=1;i<=K;i++)printf("%d ",prob[i]);puts("");
pro[0]=1;for(int i=1;i<=K;i++)pro[i]=1llpro[i-1](1+mod-prob[i])%mod;
orp[K]=ksm(pro[K]);for(int i=K;i;i--)orp[i-1]=1llorp[i](1+mod-prob[i])%mod;
for(int k=1;k<=K;k++)for(int i=1;i<=K;i++){
f[i]=prob[i];
sum[i]=1llf[i]orp[i]%mod;
for(int j=i+1;j<=K;j++){
f[j]=sum[j-1];
if(j-k>i)(f[j]+=mod-sum[j-k-1])%=mod;
f[j]=1llf[j]pro[j-1]%modprob[j]%mod;
sum[j]=(1llf[j]orp[j]+sum[j-1])%mod;
}
for(int j=i;j<=K;j++)if(i+(K-j)<=k)
// printf("%d:<%d,%d>:%d\n",k,i,j,f[j]),
(res+=1llf[j]pro[i-1]%modpro[K]%modorp[j]%mod)%=mod;
}
(res+=pro[K]-1)%=mod;
// (res+=mod-1llKpro[K]%mod)%=mod;
}
int main(){
scanf("%d%d",&n,&m);
bin[0]=1;for(int i=1;i<=n;i++)bin[i]=1llbin[i-1]inv2%mod;
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
Tarjan(1,0);
for(int i=1;i<=n;i++){
for(auto j:v[i])if(col[i]!=col[j]&&i>j)
res=(1ll(1+mod-bin[countsz(i,j)])*(1+mod-bin[countsz(j,i)])+res)%mod,
memset(vis,false,sizeof(vis));
if(siv[i])continue;
dfs(i),memset(vis,false,sizeof(vis)),calc(),K=0;
}
printf("%d\n",res);
return 0;
}
XLV.CF1707D Partial Virtual Trees
首先我们先忽略每一时刻集合大小必然减小的限制,计算此时的方案数然后容斥一下即可得到强制减小时的方案数。
令 fx,i 表示在 i 时刻 x 节点仍然健在的方案数。由 f1,i 可以简单推知 fi′ 表示 i 时刻仅存 1 节点的方案数,然后容斥即得到答案。
于是我们现在只需求出 f。
考虑转移。转移需要求出 gx,i 表示 i 时刻 x 对应的子树中有节点健在的方案数。则合并 x 与其儿子 y 时,就把 fx 与 gy 的前缀和做点乘即可,
考虑 gx,i。其又有两种可能的转移:x 一直续到了 i 时刻的方案数,以及 x 中道崩殂,但是某个子树 y 中的节点撑到最后的方案数。
前者就直接是 fx,i。考虑后者。
枚举这样一个 y,则方案数可以直接写成 gx,i=fx,i+∑y∈sonxgy,i∑j=0i−1∏z∈sonx,z≠y∑k=0jgz,k,其中 j 枚举 x 撑到哪个时刻。
然后可以直接写成 gx,i=fx,i+∑y∈sonxgy,i∑j=0i−1fx,j∑k=0jgy,k。
fx,j∑k=0jgy,k 可以直接对于每个 (y,j) 二元组预处理出来,然后求前缀和再与 f 点加即可。
时间复杂度可以轻松做到 O(n2logn)。通过处理前后缀并合并可以规避求逆,进而让复杂度变成 O(n2)。
代码:
include<bits/stdc++.h>
using namespace std;
int f[2010][2010],g[2010][2010],G[2010][2010],C[2010][2010],n,mod,d[2010];
//f:the number of ways of having a single node at time i.
//g:the number of ways of having (some none-empty things) at time i.
//h:the ... of having some empty things at time i.
vector
int pre[2010][2010],suf[2010][2010];
void dfs(int x){
for(int i=0;i<n;i++)f[x][i]=1;
for(int =0;<v[x].size();++){
int y=v[x][];
v[y].erase(find(v[y].begin(),v[y].end(),x));
dfs(y);
for(int i=0;i<n;i++)f[x][i]=1llf[x][i]G[y][i]%mod;
}
for(int i=0;i<n;i++)pre[0][i]=suf[v[x].size()][i]=1;
for(int =0;<v[x].size();++){
int y=v[x][];
for(int i=0;i<n;i++)pre[+1][i]=1ll*pre[][i]G[y][i]%mod;
}
for(int =(int)v[x].size()-1;>=0;--){
int y=v[x][];
for(int i=0;i<n;i++)suf[_][i]=1llsuf[+1][i]*G[y][i]%mod;
}
for(int =0;<v[x].size();++){
int y=v[x][];
static int h[2010];
for(int i=0;i<n;i++)h[i]=1ll*pre[][i]suf[_+1][i]%mod;
for(int i=1;i<n;i++)(h[i]+=h[i-1])%=mod;
for(int i=1;i<n;i++)(g[x][i]+=1llh[i-1]g[y][i]%mod)%=mod;
}
for(int i=0;i<n;i++)(g[x][i]+=f[x][i])%=mod;
G[x][0]=g[x][0];for(int i=1;i<n;i++)G[x][i]=(G[x][i-1]+g[x][i])%mod;
// printf("%d:\n",x);
// for(int i=0;i<n;i++)printf("%d ",f[x][i]);puts("");
// for(int i=0;i<n;i++)printf("%d ",g[x][i]);puts("");
// for(int i=0;i<n;i++)printf("%d ",G[x][i]);puts("");
}
int main(){
scanf("%d%d",&n,&mod);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(int i=0;i<=n;i++)C[i][0]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
dfs(1);
for(int i=1;i<n;i++){
d[i]=f[1][i-1];
for(int j=1;j<i;j++)(d[i]+=mod-1lld[j]*C[i][j]%mod)%=mod;
}
for(int i=1;i<n;i++)printf("%d ",d[i]);puts("");
return 0;
}
XLVI.[AGC058B]Adjacent Chmax
直接由排列去构建序列是可行的,但是处理其的算法至少也要平方。
不如考虑如何验证序列是否能被排列生成吧!
一个排列中元素在序列中对应的部分必然是一段区间 [li,ri]。当区间为空时,我们认为 ri=li−1。
考虑如何判定一组序列是否能被生成。事实上,只要保证:
所有 [li,ri] 依次首位相接拼成全串。
[li,ri] 属于 ai 作为区间最值的区间(仅当 [li,ri] 非空)。
这两个条件只要均满足,该序列即合法。这是显然的:一段 [li,ri] 若包含 i 则一定可行,否则 i 一定是被更大东西给占领了,亦显然可行。
直接 DP 转移,复杂度平方。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
int n,L[5010],R[5010],a[5010];
int f[5010],g[5010];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
for(L[i]=i;L[i]>1&&a[L[i]-1]<a[i];L[i]--);
for(R[i]=i;R[i]<n&&a[R[i]+1]<a[i];R[i]++);
}
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++)g[j]=f[j];
for(int j=1;j<=n;j++)ADD(g[j],g[j-1]);
for(int j=L[i];j<=R[i];j++)
ADD(f[j],(g[j-1]+mod-(L[i]==1?0:g[L[i]-2]))%mod);
}
printf("%d\n",f[n]);
return 0;
}
XLVII.[AGC002F] Leftmost Ball
首先我们要尝试判定一个序列是否合法。
考虑该序列中所有白球的位置。则其合法当且仅当:
第 i 个白球左侧出现的所有不同颜色数不超过 i−1。
每种颜色出现次数恰为 K−1。
可以发现,满足该条件的序列必然可以被构造出。
下一步就是计数了。首先假定我们已经确定白球的分布,则分析发现显然从右往左处理会更优。
我们记 fi,j 为:
已经填入了 i 个白球。
有 j 种颜色完全在第 i 个白球之右。
转移可以加一个白球或是确定一种颜色的位置。颜色确定的位置直接用组合数处理一下即可。为了避免重复计数,我们钦定该颜色的第一个球 必然紧贴着第 i 个白球。
复杂度平方。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,fac[4010000],inv[4010000],f[4010][4010];
int C(int x,int y){return y<0||x<y?0:1llfac[x]inv[y]%modinv[x-y]%mod;}
int main(){
scanf("%d%d",&n,&m);
if(m==1){printf("%d\n",1);return 0;}
fac[0]=1;for(int i=1;i<=nm;i++)fac[i]=1llfac[i-1]i%mod;
inv[nm]=ksm(fac[nm]);for(int i=nm;i;i--)inv[i-1]=1llinv[i]i%mod;
f[0][0]=1;
for(int i=0;i<=n;i++)for(int j=0;j<=n;j++){
// printf("<%d,%d>:%d\n",i,j,f[i][j]);
if(i<=j)(f[i+1][j]+=f[i][j])%=mod;
(f[i][j+1]+=1llf[i][j]%modC((j+1)(m-1)+i-2,m-2)%mod)%=mod;
}
printf("%d\n",1llf[n][n]fac[n]%mod);
return 0;
}
XLVIII.[AGC009E] Eternal Average
考虑最终结构时,每个元素的贡献:显然,其有一个 K−x 的系数。
那么换句话说,所有可被组出的东西都可以被 K 进制小数表出。我们只需在意小数上每一位的贡献即可。
那么我们从 x 更大(也即,更靠后的小数位)出发,向高位 DP,每次确定一位的值,然后判定某种方案是否可行。
把合并的状态写成一棵树,然后一个暴力的想法是,fi,j,k,t,p 表示:
第 i 层。
总计消耗了 j 个 0 叶节点和 k 个 1 叶节点。
第 i 层现存 t 个节点。
i+1 层往后共进位的信息是 p。
转移枚举第 i 层用多少个 0 叶子和多少个 1 叶子。答案为 ∑if0,n,m,1,i。
首先我们可以发现,层的信息事实上是不需要的。
然后紧接着我们发现,进位的信息事实上也是不必要的:因为 0 叶子和 1 叶子数量都是确定的,因此就算 K 个深度为 x 的 1 叶子等效于 1 个深度为 x−1 的 1 叶子,但是二者间差了 K−1 个叶子名额,因此事实上是不同的……也不是。
例如,从上往下每层的 1 叶子数分别是 0,4,0 和 1,0,3 的两棵树,其虽然树的形态不同但是结果是一致的。
我们不妨考虑一种贪心:在每层中,新增若干 1,然后添加 最少量的 0 使得这层能够被成功上推。这样,答案会是全体 1 用完且最上层恰有一个节点的态。多余的若干 0 可以被拿来凑叶子,因为 (K−1)|(n+m−1) 所以必然可行。
但是到最后我们发现上述做法似乎没有前途。
事实上,因为所有叶子的 K−xi 之和恰为 1,而最终结果的 t 可以被 m 个 1 叶子的 ∑K−xi 表出,则 n 个 0 叶子的 ∑K−xi 必然恰表出 1−t。故事实上我们只需对:
能被 n 个 0 叶子表出的 1−t 和 m 个 1 叶子表出的 t。
计数其即可。
因为某位上的一个 1 可以被拆成下位上的 K 个 1,换句话说就是一切 n−i(K−1) 和 m−j(K−1) 都是可行的。
于是我们仍然是按位 DP,每新增一位就枚举该位上的取值,然后给 0 和 1 分别贡献即可。
直接暴力 DP 是三方的,通过简单前缀和优化可以做到平方。但是直接三方搞,剪剪枝就飞快过去了。
代码:
include<bits/stdc++.h>
const int mod=1e9+7;
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
using namespace std;
int n,m,K,res;
int f[2010][2010];
int main(){
scanf("%d%d%d",&n,&m,&K);
for(int i=1;i<K;i++)f[K-i][i]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(f[i][j]){
if(!((n-i)%(K-1))&&!((m-j)%(K-1)))
ADD(res,f[i][j]);
for(int k=max(0,j+K-1-m);k<K&&k<=n-i;k++)
ADD(f[i+k][j+K-k-1],f[i][j]);
}
printf("%d\n",res);
return 0;
}
IL.[AGC012F] Prefix Median
这种计数题显然只能判定一个序列能否被生成。
于是我们考虑判定。
从前往后搞的话,流程是加数,初态不确定,好像不太好;不如从后往前搞,初态是确定的,则流程是删数。
然后发现,初始的中位数是全局中位数;然后每往前一步,我们可以执行的操作是:
删掉两个大于当前中位数的东西,让中位数变成其前驱。
删掉两个小于当前中位数的东西,让中位数变成其后继。
删掉一个比它大的,一个比它小的,保持中位数不变。
然后我们就发现,通过上述流程,我们可以生成的 b 必然满足 bi 在 a 中的排名是在 [i,2n−i] 之中。(因为,每次你干掉两个在之后没有再出现的元素即可)
是否所有满足该条件的 b 均合法呢?并不是,因为 b 每次的变动是变成其前驱或后继之一,而不一定所有的 b 都能够成功变动。具体而言,如果在某一时刻的中位数是 i,下一时刻的中位数是 j;在之后的某一时刻(这里的前后是倒序的,也即在序列中越靠后的在时刻上就越靠前)中位数有 i′→j′,且有 (i,j)⊊(i′,j′),则这个方案就是不合法的(因其不可能构造合法的删数的前后继关系)。
一个显然的想法是正序思考,即当前有若干 i→j,换句话说就是有若干 (i,j) 被禁掉了跨越其的行为。
但是你会发现这不行,因为你没法简单描述之前的状态。
正确的做法还是倒序处理。每个 i→j 禁掉了所有被其 严格包含 的转移。
然后发现,被禁掉的转移这之后也不会再出现。那么就很明朗了:设 fi,j,k 表示当前 DP 到第 i 位,其左侧有 j 个转移尚未被禁掉,右侧有 k 个转移尚未被禁掉(注意,这些“未被禁掉”的转移 并不意味着它们一定存在于前 i 位中,也即其是当前的 b 的后缀可能可以往前面添加的元素)。每 DP 一步时,首先先往最左最右各自添加一个转移(也即 ai 和 a2n−i),然后转移枚举下一步转移填什么,并把中间的转移禁掉。需要注意的是,当 a 并非全都不同时,相邻相同的东西只需考虑一个即可。
复杂度可以简单做到四方。可以通过前缀和简单优化到三方,但是我太懒不写。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,a[110],f[110][110][110],res;
int main(){
scanf("%d",&n);
for(int i=1;i<(n<<1);i++)scanf("%d",&a[i]);
sort(a+1,a+(n<<1));
f[n][0][0]=1;
for(int i=n;i>1;i--)for(int j=0;j<=(n<<1);j++)for(int k=0;k<=(n<<1);k++)if(f[i][j][k]){
bool x=(a[i]!=a[i-1]),y=(a[(n<<1)-i]!=a[(n<<1)-i+1]);
for(int =0;<j+x;++)(f[i-1][][k+y+1]+=f[i][j][k])%=mod;
for(int =0;<k+y;++)(f[i-1][j+x+1][]+=f[i][j][k])%=mod;
(f[i-1][j+x][k+y]+=f[i][j][k])%=mod;
}
for(int i=0;i<=(n<<1);i++)for(int j=0;j<=(n<<1);j++)(res+=f[1][i][j])%=mod;
printf("%d\n",res);
return 0;
}
L.[AGC019E] Shuffle and Swap
首先一个观察是,我们只需在意两个串中如下三个东西的数量即可:
在 A 中是 1 而 B 中是 0 的位置(记作 10 位)。设其有 a 个。
A 是 1 且 B 是 1 的位置(记作 11 位)。设其有 b 个。
A 是 0 而 B 是 1 的位置(记作 01 位)。因为两个串中 1 数量相同所以其数量亦为 a。
然后考虑一次交换。
10 换到 01:恰能产生产生 00 和 11 各一个,且这两个位置以后都不会再被交换。换句话说,可以将它们从流程中扔掉。一共有 a2 种交换方式,且交换完后 a 减小一。
10 换到 11:交换并不能产生新态,且交换完的 10 无法再交换,将一直保持 10。也就是说,这一步交换是不合法的。
11 和 11 自产自销。
这里使得我们要将 11 分类:未进行任何交换的 11(共 c 个,记作 X-11),以及进行了 A 中交换的 11 和 B 中交换的 11(各 d 个,前者记作 A-11,后者记作 B-11)。
则一次交换要么发生在两个 X-11 间,此时 c 减二 d 加一;要么发生在一个 A-11 和一个 X-11 间,此时 c 减一 d 不变;要么发生在一个 B-11 和一个 X-11 间,仍是 c 减一 d 不变;要么发生在 A-11 和 B-11 间,此时 d 减一;要么一个 X-11 和自己交换,c 减一 d 不变。
11 换到 01:相当于凭空少了一个 11。注意到只有 X-11 做这样的交换才是可行的——A-11 和 B-11 都会产生不合法的态。
于是我们考虑 (a,c,d),其有如下几种转移:
→(a−1,c,d),方案数为 a2。
→(a,c−2,d+1),方案数为 c(c−1)。
→(a,c−1,d),方案数为 2cd+c+ca。
→(a,c,d−1),方案数为 d2。
写一个暴力出来发现过了样例。于是我们便得到了一个三方做法。
优化到平方的做法是上述 DP 做法无法实现的。事实上,我们只考虑 10 和 01 的交换以及 11 和 01 的交换,然后 11 的自交被我们忽略。这样之后,我们得到 (a,b′) 表示 a 对 01-10 全都消完,共消耗了 b′ 个 11 的方案数,然后剩下的 b−b′ 个 11,其不管怎么换均是合法的,故方案数即为 ((b−b′)!)2,然后再在总操作序列中挑一些位置插进去即可。复杂度就做到了平方。
还可以进一步优化。注意到最终交换必然构成若干链和若干环(我们上面的 DP 就是 DP 链的流程)。而链的状态可以用生成函数描述,然后我们只需求一个多项式快速幂即可。时间复杂度可以做到线性对数。
但是这不妨碍我们直接平方莽过去。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
char s[10100],t[10100];
int n,a,b,f[2][10100],fac[10100],inv[10100],res;
int main(){
scanf("%s%s",s,t),n=strlen(s);
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1llfac[i-1]i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=0;i<n;i++)if(s[i]'1')(t[i]'1'?b:a)++;
f[a&1][b]=1;
for(int i=a;i>=0;i--)for(int j=b;j>=0;j--)if(f[i&1][j]){
if(i>=1)(f[!(i&1)][j]+=1llii%modf[i&1][j]%mod)%=mod;
if(j>=1)(f[i&1][j-1]+=1llji%modf[i&1][j]%mod)%=mod;
if(!i)(res+=1llf[i&1][j]fac[a+b]%modinv[a+b-j]%modfac[j]%mod)%=mod;
f[i&1][j]=0;
}
printf("%d\n",res);
return 0;
}
LI.[ZJOI2016]线段树
显然可以建出笛卡尔树。因为是随机序列,所以笛卡尔树的期望深度是对数的。
然后考虑一个位置的值:其仅可能被改成其某个祖先的值。于是我们枚举其每个祖先,计算其值小于该祖先值的方案数,然后差分一下即可得到每个祖先值的方案数。
考虑某个祖先值。我们发现我们只关注左右两侧首个大于等于其的值的位置,记作 l,r。
若一次操作的左端点在 l 左侧,则 l 会与其右端点取 max。
同理,右端点在 r 右侧则 r 会与左端点取 min。
我们的目标是 l 小于当前位置且 r 大于当前位置。
记 fi,l,r 为 i 次操作后 l,r 的值分别确定为其后的方案数,转移枚举 l,r 分别转移到哪里然后可以简单前缀和优化。复杂度三方。然后再枚举 i 及其对应区间,复杂度 O(n4logn)……吗?
并不是。一个点的复杂度是其所有祖先对应区间长度的平方乘以 q。而,事实上所有这样的区间长度平方和的期望是 n2 而不是 n3logn,因此复杂度直接就是 n2q……
但是我们还可以做到更好。因为多次 DP 仅有初值不同所以直接把所有初值作为系数合在一起考虑即可。复杂度严格 O(n2q)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,a[410],b[410],f[410][410],g[410][410],h[410][410],res[410];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),b[i]=a[i];
sort(b+1,b+n+1);
for(int i=1;i<=n;i++)for(int l=1,r=1;l<=n;l=r){
while(l<=n&&a[l]>=b[i])l++;
if(l>n)break;
for(r=l;r<=n&&a[r]<b[i];r++);
// printf("%d[%d,%d]\n",i,l,r-1);
(f[l][r-1]+=b[i]-b[i-1])%=mod;
}
for(int =1;<=m;_++){
for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)
g[l][r]=(g[l-1][r]+1llf[l][r](l-1))%mod;
for(int l=n;l;l--)for(int r=n;r>=l;r--)
h[l][r]=(h[l][r+1]+1llf[l][r](n-r))%mod;
for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)
f[l][r]=(((1ll(l-1)l+1ll(r-l+1)(r-l+2)+1ll(n-r)(n-r+1))>>1)%modf[l][r]+g[l-1][r]+h[l][r+1])%mod;
for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)
g[l][r]=h[l][r]=0;
}
int lam=(1lln(n+1)>>1)%mod,mal=1;
for(int i=1;i<=m;i++)mal=1llmallam%mod;
for(int i=1;i<=n;i++){
for(int l=1;l<=i;l++)for(int r=i;r<=n;r++)(res[i]+=f[l][r])%=mod;
// printf("<%d>\n",res[i]);
res[i]=(1llb[n]*mal+mod-res[i])%mod;
}
for(int i=1;i<=n;i++)printf("%d ",res[i]);puts("");
return 0;
}
LII.折半状压
对于所有边权都在 [1,m] 中的整数随机的 n 个点的完全图,求其最小生成树的期望边权和。
数据范围:n≤100,m≤109+6。答案对 109+7 取模。
m 那么大显然是给插值的。
考虑如何统计。显然要对每个权值,统计其对答案的贡献。但是怎么表示其对答案的贡献呢?
记 f(i) 表示仅保留 ≤i 的边时,整张图期望的连通块数。则有权值 i 的边期望在 MST 中出现 f(i)−f(i−1) 条,故总贡献即为 ∑i=1mi(f(i−1)−f(i))=∑i=0m−1f(i)−mf(m)=∑i=1mi(f(i−1)−f(i))=∑i=0m−1f(i)−m。
考虑如何求出 f(i)。记 g(i) 表示 i 个点的图连通的概率,则 f=∑i=1n(ni)g(i)(1−p)i(n−i)。
g 如何求呢?这是经典的问题。记 h(i) 表示不连通的概率,则 g(i)=1−h(i)。h 就大力枚举 1 所在连通块包含几个点即可。列式得到 h(n)=∑i=1n−1(n−1i−1)g(i)(1−p)i(n−i),其中 p 是两个点间有边的概率。
发现求出不同的 f(i) 时,有影响的仅仅是 p。而 f(i) 时的 p 又等于 im,其是关于 i 的一次多项式,那么由上式 g 是 O(n2) 次多项式,f 是 O(n2) 次多项式,于是处理 O(n2) 个 f 然后求其前缀和在 m−1 处的点值即可。
复杂度 O(n4)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,K,f[5100],fac[5100],inv[5100];
int calc(int P){
static int g[110],h[110],pov[5100];
pov[0]=1;for(int i=1;i<=(n(n-1)>>1);i++)pov[i]=1llpov[i-1](1+mod-P)%mod;
for(int i=1;i<=n;i++){
h[i]=0;
for(int j=1;j<i;j++)
h[i]=(1llfac[i-1]inv[j-1]%modinv[i-j]%modg[j]%modpov[j(i-j)]+h[i])%mod;
g[i]=(1+mod-h[i])%mod;
}
int res=0;
for(int i=1;i<=n;i++)
res=(1llfac[n]inv[i]%modinv[n-i]%modg[i]%modpov[i(n-i)]+res)%mod;
// printf("calc:%d=%d\n",P,res);
return res;
}
int Lagrange(int y){
int res=0;
for(int i=0;i<=K;i++){
int ser=1llf[i]inv[i]%modinv[K-i]%mod;
if((K-i)&1)ser=mod-ser;
for(int j=0;j<=K;j++)if(j!=i)ser=1llser(y+mod-j)%mod;
(res+=ser)%=mod;
}
return res;
}
int main(){
freopen("seat.in","r",stdin);
freopen("seat.out","w",stdout);
scanf("%d%d",&n,&m),K=min((n(n-1)>>1)+10,m);
fac[0]=1;for(int i=1;i<=max(n,K);i++)fac[i]=1llfac[i-1]i%mod;
inv[max(n,K)]=ksm(fac[max(n,K)]);for(int i=max(n,K);i;i--)inv[i-1]=1llinv[i]i%mod;
for(int i=0;i<=K;i++)f[i]=calc(1lli*ksm(m)%mod);
for(int i=1;i<=K;i++)(f[i]+=f[i-1])%=mod;
printf("%d\n",(Lagrange(m-1)+mod-m)%mod);
return 0;
}
LIII.绘画
给定一个 n 行 m 列方格,其中所有格子初始为白色。你可以选择若干列和若干 (1,1)→(n,m) 方向的对角线染黑,求所有本质不同的结果局面数。
数据范围:n,m≤40。
首先考虑将对角线转成行。
abcd a
efgh eb
ijkl---->ifc
mnop mjgd
qrst qnkh
rol
sp
t
注意到这里实际上对角线方向出现了偏差(由左上-右下变成了右下-左上),但是二者事实上是无区别的。
我们为行列标号,左上角为 (1,1)。则行的标号范围是 [1,n+m),列的范围则是 [1,m]。
考虑某个局面下,我们选择行上集合 S 染黑、列上集合 T 染黑。
但是并非所有 (S,T) 对应的局面都是两两不同的。具体而言,我们钦定仅有那些 所有被全部染黑的行都属于 S,且所有被全部染黑的列都属于 T 的局面 是合法的。
考虑从左往右决策每一列,统计当前局面下合法的行上方案数。
我们首先需要使得所有被全部染黑的行属于 S。在决策完第 i 列后,会有若干以 i 作结尾的行。我们需要知道其中有哪些是被完全染黑的。
每一行对应的列是连续的:是以 i 结尾的一段区间。故,我们只需额外维护一个信息 j 表示区间 (j,i] 中的列,是被染黑的 极长 连续段。
于是我们可以暂设一个状态:fi,j 表示上述状态对应的合法行上方案数。初始是 f0,0=2n+m−1(此时我们认为一切行上方案均合法)。考虑转移:
fi−1,j→fi,i。第 i 列直接不选。
12kfi−1,j→fi,j。第 i 列选择,且此时会使得 k 条以 i 结尾的行被完全染黑,此时它们的状态就被固定,所以要乘以 12k。显然,所有行的状态只会在 i 被判定(指被尝试固定)一次,所以可以直接乘,不会出现重复固定的场合。
这样,我们成功让所有被全部染黑的行都属于 S。但是我们还要让所有被全部染黑的列都属于 T。
我们发现,第 i 列与 [i,i+n) 行相关。我们这时需要套一个容斥:在第 i 列未被染黑时,要用总方案数减去 [i,i+n) 全部被染黑的方案数。
[i,i+n) 的区间是随着 i 增加不断右移的。于是我们就只需维护最后一个被钦定染黑的行(i+n−1)即可。设为 k,于是有状态被扩充为 fi,j,k。初始仍是 f0,0,0=2n+m−1。
这时我们不禁会有一个疑问:k 维钦定的若干行被染黑(用来容斥),与 j 维钦定的若干行被染黑(因为这些行被列给染黑),这两个会不会被重复钦定?
答案是不会。首先在每个 i 处,二者至多被钦定一种(k 维仅在 i 列不选,j 维仅在 i 列选),不会冲突;k 被钦定时,新增的若干行都不会覆盖原有的 j 的行(因为 i−1 以前的 j 行至多到 i−1,但是 i 的 k 行是从 i 开始);j 被钦定时,其所有新增位置都可以被得知,进而可以手动 ban 掉那些已经被 k 钦定的行(即从 k+1 开始寻找新增的行)。
暴力处理,复杂度 O(n4),且可以用一些简单手法变成 O(n3)。
代码:
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1llxx%mod)if(y&1)z=1llzx%mod;return z;}
int n,m,bin[210],nib[210],f[80][210][210],res;
int main(){
freopen("paint.in","r",stdin);
freopen("paint.out","w",stdout);
scanf("%d%d",&n,&m);
bin[0]=1;for(int i=1;i<n+m;i++)bin[i]=(bin[i-1]<<1)%mod;
nib[n+m-1]=ksm(bin[n+m-1]);for(int i=n+m-1;i;i--)nib[i-1]=(nib[i]<<1)%mod;
// for(int i=0;i<n+m;i++)printf("%d ",bin[i]);puts("");
// for(int i=0;i<n+m;i++)printf("%d ",nib[i]);puts("");
f[0][0][0]=bin[n+m-1];
for(int i=1;i<=m;i++)for(int j=0;j<i;j++)for(int k=0;k<n+m;k++)
if(f[i-1][j][k]){
// printf("%d,%d,%d=%d\n",i-1,j,k,f[i-1][j][k]);
(f[i][i][k]+=f[i-1][j][k])%=mod;
(f[i][i][i+n-1]+=mod-1llf[i-1][j][k]nib[i+n-1-max(k,i-1)]%mod)%=mod;
int num=0;
for(int t=k+1;t<n+m;t++)
if(min(t,m)==i&&j<=max(t-n,0))num++;
// printf("<%d,%d>\n",i+n-1-max(k,i-1),num);
(f[i][j][k]+=1llf[i-1][j][k]nib[num]%mod)%=mod;
}
for(int j=0;j<=m;j++)for(int k=0;k<n+m;k++)
if(f[m][j][k])
// printf("%d,%d,%d=%d\n",m,j,k,f[m][j][k]),
(res+=f[m][j][k])%=mod;
printf("%d\n",res);
return 0;
}
LIX.CF1696H Maximum Product?
考虑无脑选绝对值最大的。
这样可能出现符号错误。我们尝试:
将绝对值最小的正数换成绝对值最大且未选择的负数。
将绝对值最小的负数换成绝对值最大且未选择的正数。
这需要我们知道绝对值最小的正数与负数分别为何。因为本题显然三方可以通过,而常规操作仅需维护下标和选择数数目两维,所以我们可以枚举一些东西。于是我们同时枚举最小正数和最小负数。在两者之间的部分,我们仅能填正数或仅能填负数;大于二者较大者的部分,两者均可填。我们可以再枚举一轮两侧分别填多少数,则现在已经要三方了。
这需要我们维护两个信息:区间中仅选正/负,选若干个的积;以及后缀选若干个的积。均可简单统计。
需要注意的是可能我们全选正数或全选负数。前者显然是容易的,后者要视 m 的奇偶性选最小或最大的,不在话下。
然后就是有正有负的场合。假如选恰 m 个无法修改,但是亦是简单的。否则可以修改:我们上述流程会诞生一个状态 (i,j) 表示正数、负数分别为 i,j,且积为负的状态的积之和。这样的状态一共有平方个;然后我们可以枚举下一个出现的数,判定其究竟是贡献给答案还是不贡献给答案,进而得到次大数的选择与否方案。
总复杂度三方。细节不是一般地多。
include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,a[610],bin[610],preneg[610],prepos[610],sufneg[610],sufpos[610],res;
void allposi(){
static int f[610];
f[0]=1;
for(int i=n;i;i--)if(a[i]>=0){
res=(1llf[m-1]a[i]%modbin[i-1]+res)%mod;
for(int j=m;j;j--)
f[j]=(1llf[j-1]a[i]+f[j])%mod;
}
}
void allnega(){
static int f[610];
f[0]=1;
if(!(m&1)){
for(int i=n;i;i--)if(a[i]<0){
res=(1llf[m-1](a[i]+mod)%modbin[i-1]+res)%mod;
for(int j=m;j;j--)
f[j]=(1llf[j-1](a[i]+mod)+f[j])%mod;
}
}else{
for(int i=1;i<=n;i++)if(a[i]<0){
res=(1llf[m-1](a[i]+mod)%modbin[sufneg[i+1]]+res)%mod;
for(int j=m;j;j--)
f[j]=(1llf[j-1](a[i]+mod)+f[j])%mod;
}
memset(f,0,sizeof(f));
f[0]=1;
for(int i=n;i;i--)if(a[i]<0){
for(int j=1;j<i;j++)
if(a[j]>=0)
// printf("<%d,%d,%d>\n",i,j,f[m-1]),
res=(1llf[m-1]a[j]%modbin[preneg[i-1]]%modbin[prepos[j-1]]+res)%mod;
for(int j=m;j;j--)
f[j]=(1llf[j-1](a[i]+mod)+f[j])%mod;
}
}
}
int f[610][610][2];
int main(){
// freopen("I.in","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
sort(a+1,a+n+1,[](const int&x,const int&y){return abs(x)<abs(y);});
// for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("");
for(int i=1;i<=n;i++)
preneg[i]=preneg[i-1]+(a[i]<0),
prepos[i]=prepos[i-1]+(a[i]>=0);
for(int i=n;i;i--)
sufneg[i]=sufneg[i+1]+(a[i]<0),
sufpos[i]=sufpos[i+1]+(a[i]>=0);
bin[0]=1;for(int i=1;i<=n;i++)bin[i]=(bin[i-1]<<1)%mod;
allposi(),allnega();
if(m==1){printf("%d\n",res);return 0;}
// printf("%d\n",res);
f[n+1][0][0]=1;
for(int i=n;i;i--){
for(int j=0;j<=m;j++)for(int k=0;k<2;k++)f[i][j][k]=f[i+1][j][k];
for(int j=1;j<=m;j++)for(int k=0;k<2;k++)
f[i][j][k]=(1llf[i+1][j-1][k^(a[i]<0)](a[i]+mod)+f[i][j][k])%mod;
}
for(int i=1;i<=n;i++){
static int g[610][2];
memset(g,0,sizeof(g)),g[0][0]=1;
for(int j=i+1;j<=n;j++)
if((a[j]>=0)==(a[i]>=0)){
for(int k=m;k;k--)for(int t=0;t<2;t++)
g[k][t]=(1llg[k-1][t^(a[j]<0)](a[j]+mod)+g[k][t])%mod;
}else{
int mun[2]={0,0};
for(int k=0;k<=m-2;k++)
for(int I=0;I<2;I++)for(int J=0;J<2;J++)
mun[IJ1]=(1llg[k][I]f[j+1][m-2-k][J]+mun[IJ1])%mod;
int num=mun[1];
res=((1llbin[i-1]mun[0]+mun[1])%mod(a[i]+mod)%mod(a[j]+mod)+res)%mod;
// printf("<%d,%d>:-%d,%d\n",i,j,mod-mun[0],mun[1]);
// printf("<%d>\n",res);
for(int k=i-1,p=i-1;k;k--)
if((a[k]>=0)!=(a[i]>=0)){
while(p&&(llabs(1lla[k]a[j])<llabs(1lla[i]a[p])||(a[p]>=0)!=(a[i]>=0)))p--;
int val=1llnum(a[k]+mod)%mod(a[j]+mod)%mod;
val=1llvalbin[a[k]>=0?prepos[k-1]:preneg[k-1]]%mod;
val=1llvalbin[a[p]>=0?prepos[p]:preneg[p]]%mod;
// printf("A<%d|%d,%d,%d>:%d\n",k,p,i,j,val);
(res+=val)%=mod;
}
for(int k=i-1,p=i-1;k;k--)
if((a[k]>=0)!=(a[j]>=0)){
while(p&&(llabs(1lla[k]a[i])<=llabs(1lla[j]a[p])||(a[p]>=0)!=(a[j]>=0)))p--;
int val=1llnum(a[k]+mod)%mod(a[i]+mod)%mod;
val=1llvalbin[a[k]>=0?prepos[k-1]:preneg[k-1]]%mod;
val=1llval*bin[a[p]>=0?prepos[p]:preneg[p]]%mod;
// printf("B<%d|%d,%d,%d>:%d\n",k,p,i,j,val);
(res+=val)%=mod;
}
// printf("(%d)\n",res);
}
}
printf("%d\n",res);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通