AtCoder AGC028 D
2
n
2n
2n 个点围成一圈,顺时针标号
1
…
2
n
1\dots 2n
1…2n。要把这些点份分成
n
n
n 对,每对之间用线段相连,如果两条线段相交,那么四个端点联通。初始给定
k
k
k 对已经配对的点,求所有配对方式的联通块总数。
n
≤
300
n\le 300
n≤300
思路是计算每个(种)联通块出现的方案数。
设一个联通块最小的点是 i i i,最大的点是 j j j,发现每一个联通块都能被这么表示一遍。我们尝试这样计算每一个联通块出现的方案数。令 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示只考虑配对 i i i 到 j j j 中的点, i i i 所在的联通块的最大编号是 j j j 的方案数。如果 i i i 到 j j j 中间有初始配好的点连向了外面,那么方案数为 0。否则用随便配对的方案数减去不合法的。设区间中未配对点数为 s u m sum sum,总方案数为 ( s u m − 1 ) × ( s u m − 3 ) × ⋯ × 1 (sum-1)\times(sum-3)\times\cdots \times1 (sum−1)×(sum−3)×⋯×1。对于不合法的方案,枚举 i i i 所在联通块的最大值为 k k k,要减去 d p [ i ] [ k ] dp[i][k] dp[i][k] 乘上 k + 1 , ⋯   , j k+1,\cdots ,j k+1,⋯,j 中随便配的方案数。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7;
ll f[1010],dp[1010][1010];
int s[1010],vis[1010],lk[1010];
inline int read()
{
char c=getchar();int x=0,flag=1;
while(!isdigit(c)){if(c=='-') flag=-1;c=getchar();}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*flag;
}
int dfs(int l,int r)
{
//cout<<l<<r;
if(l>=r||(s[r]-s[l-1])&1) return 0;
if(~dp[l][r]) return dp[l][r];
for(int i=l;i<=r;i++) if(vis[i]&&(lk[i]>r||lk[i]<l)) return dp[l][r]=0;
ll ans=f[s[r]-s[l-1]];
for(int k=l;k<r;k++)
{
ans=(ans-dfs(l,k)*f[s[r]-s[k]]%mod+mod)%mod;
}
return dp[l][r]=ans;
}
int main()
{
int n=read()<<1,k=read();
for(int i=1;i<=k;i++)
{
int a=read(),b=read();
lk[a]=b;
lk[b]=a;
vis[a]=vis[b]=1;
}
f[0]=1;
for(int i=2;i<=1000;i+=2) f[i]=f[i-2]*(i-1)%mod;
for(int i=1;i<=n;i++) s[i]=s[i-1]+1-vis[i];
ll ans=0;
memset(dp,-1,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++) ans=(ans+dfs(i,j)*f[s[n]-s[j]+s[i-1]])%mod;
cout<<ans;
return 0;
}
Codeforces 1063F
给定一个字符串,在其中选择若干个不相交子串,满足后一个子串的长度严格小于前一个,并且是前一个子串的子串。最大化选出子串的数量。
观察到答案是 n \sqrt n n 级别的,并且一定存在一个最优解的长度是从1开始的连续整数。
sol1:
设
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示第一个串从
i
i
i 开始,往后面选
j
j
j 个串可不可行。然后在外层枚举
j
j
j,hash 一下就可以从后往前转移。
sol2:
设
f
[
i
]
f[i]
f[i] 表示第一个串从
i
i
i 开始,往后最多选多少个串。还是从后往前转移,显然可以二分一个答案
m
i
d
mid
mid,查询
x
>
i
+
m
i
d
−
1
x>i+mid-1
x>i+mid−1 并且
l
c
p
(
i
,
x
)
≥
m
i
d
lcp(i,x)\ge mid
lcp(i,x)≥mid 中
f
f
f 值最大的
x
x
x,判断
f
[
x
]
f[x]
f[x] 是否大于等于
m
i
d
−
1
mid-1
mid−1。这样就有了一个
O
(
n
log
2
n
)
O(n\log^2n)
O(nlog2n) 的做法。
然后类似后缀数组求 h e i g h t height height,可以发现 f [ i + 1 ] ≥ f [ i ] − 1 f[i+1]\ge f[i]-1 f[i+1]≥f[i]−1。因此 f [ i ] ≤ f [ i + 1 ] + 1 f[i]\le f[i+1]+1 f[i]≤f[i+1]+1,可以在均摊 O ( n ) O(n) O(n) 的时间内完成计算。
具体的,我们如何 check 一个答案 x x x?实际上我们是要求原序列位置在某个后缀,并且 rank 序列位置在某个区间中的 f f f 的最大值。这是一个二维问题,并且有一维是一个后缀形式,这样就可以用主席树来做。主席树外面的维度是原序列的位置。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int f[2000010][20],Max[20000010],root[500010],ls[20000010],rs[20000010],sa[500010],wb[500010],x[500010],y[500010],rk[500010],gg[500010],n,height[500010],tot;
char s[500010];
inline int read()
{
char c=getchar();int x=0,flag=1;
while(!isdigit(c)){if(c=='-') flag=-1;c=getchar();}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*flag;
}
void SA()
{
int m=1000,p=0;
for(int i=1;i<=n;i++) wb[x[i]=s[i]]++;
for(int i=1;i<=m;i++) wb[i]+=wb[i-1];
for(int i=n;i>=1;i--) sa[wb[x[i]]--]=i;
for(int j=1;p<n;j<<=1,m=p)
{
p=0;
for(int i=n-j+1;i<=n;i++) y[++p]=i;
for(int i=1;i<=n;i++) if(sa[i]>j) y[++p]=sa[i]-j;
for(int i=1;i<=m;i++) wb[i]=0;
for(int i=1;i<=n;i++) wb[x[y[i]]]++;
for(int i=1;i<=m;i++) wb[i]+=wb[i-1];
for(int i=n;i>=1;i--) sa[wb[x[y[i]]]--]=y[i];
p=1;
for(int i=1;i<=n;i++) swap(x[i],y[i]);
x[sa[1]]=1;
for(int i=2;i<=n;i++)
{
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+j]==y[sa[i-1]+j]) x[sa[i]]=p;
else x[sa[i]]=++p;
}
}
for(int i=1;i<=n;i++) rk[sa[i]]=i;
int k=0;
for(int i=1;i<=n;i++)
{
if(k) k--;
if(rk[i]==1) continue;
int j=sa[rk[i]-1];
while(s[j+k]==s[i+k]) k++;
height[rk[i]]=k;
}
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][0]=height[i];
for(int j=1;j<=19;j++)
for(int i=1;i<=n;i++) f[i][j]=min(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
int get(int l,int r)
{
if(l>r) return 1e9;
int k=gg[r-l+1];
return min(f[l][k],f[r-(1<<k)+1][k]);
}
void modify(int &root,int pre,int l,int r,int x,int k)
{
root=++tot;
Max[root]=max(Max[pre],k);
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) modify(ls[root],ls[pre],l,mid,x,k),rs[root]=rs[pre];
else modify(rs[root],rs[pre],mid+1,r,x,k),ls[root]=ls[pre];
}
int query(int root,int l,int r,int x,int y)
{
if(!root) return 0;
if(x>y) return 0;
if(x<=l&&y>=r) return Max[root];
int mid=l+r>>1,ans=0;
if(x<=mid) ans=query(ls[root],l,mid,x,y);
if(y>mid) ans=max(ans,query(rs[root],mid+1,r,x,y));
return ans;
}
bool check(int xx,int k,int pos)
{
int x=rk[xx];
int l=x,r=n,ansl=0,ansr=n+1;
while(l<=r)
{
int mid=l+r>>1;
if(get(x+1,mid)<k) r=mid-1,ansr=mid;
else l=mid+1;
}
r=x,l=1;
while(l<=r)
{
int mid=l+r>>1;
if(get(mid+1,x)<k) l=mid+1,ansl=mid;
else r=mid-1;
}
int tmp=query(root[pos],1,n,ansl+1,ansr-1);
return tmp>=k;
}
int main()
{
n=read();
scanf("%s",s+1);
for(int i=2;i<=n;i++) gg[i]=gg[i>>1]+1;
SA();
int k=0,ans=0;
Max[0]=0;
for(int i=n;i>=1;i--)
{
if(k<n) k++;
while(!check(i,k-1,i+k)&&!check(i+1,k-1,i+k)&&k) k--;
modify(root[i],root[i+1],1,n,rk[i],k);
ans=max(ans,k);
}
cout<<ans;
return 0;
}
ZROI #399
YJC最近在学习竞赛图,竞赛图指任意两个点之间有且仅有一条有向边的有向图。他现在对竞赛图中强连通分量的大小很感兴趣。
YJC定义了一张竞赛图的权值。设1号点所在的强连通分量大小为k,则这张竞赛图的权值为
d
k
d_k
dk,
d
d
d 是一个给定的数组。
现在YJC想知道,对于所有
1
≤
i
≤
n
1≤i≤n
1≤i≤n,等概率随机生成一张i个点的竞赛图,这张竞赛图的权值的期望是多少?YJC发现自己不会算,所以他来向你求助。
竞赛图的性质:
1.竞赛图一定存在哈密顿路径。
2.如果竞赛图强连通,那么一定存在哈密顿回路。
3.竞赛图缩点以后是一条链(可能有跨越的边,但不会有分叉)。
这个题我们需要利用第三个性质进行计算。
设
g
[
n
]
g[n]
g[n] 表示
n
n
n 个点的强连通竞赛图的数量。那么我们根据第三条性质枚举缩点后链上最后一个点:
g
[
n
]
=
2
n
(
n
−
1
)
2
−
∑
i
=
1
n
−
1
(
n
i
)
g
[
i
]
⋅
2
(
n
−
i
)
(
n
−
i
−
1
)
2
g[n]=2^{\frac{n(n-1)}2}-\sum_{i=1}^{n-1}{n\choose i}g[i]·2^{\frac {(n-i)(n-i-1)}2}
g[n]=22n(n−1)−i=1∑n−1(in)g[i]⋅22(n−i)(n−i−1)
由于每个点等价,我们求出所有图的每个点的点权和,除以
n
n
n 就是答案。设
f
[
n
]
f[n]
f[n] 表示
n
n
n 个点的所有图的每个点的点权和,我们枚举最后一个点的大小:
f
[
n
]
=
∑
i
=
1
n
(
n
i
)
g
[
i
]
(
f
[
n
−
i
]
+
d
[
i
]
⋅
i
⋅
2
(
n
−
i
)
(
n
−
i
−
1
)
2
)
f[n]=\sum_{i=1}^{n}{n\choose i}g[i]\bigg(f[n-i]+d[i]·i·2^{\frac{(n-i)(n-i-1)}2}\bigg)
f[n]=i=1∑n(in)g[i](f[n−i]+d[i]⋅i⋅22(n−i)(n−i−1))