前言
五道我不会做的DP黑题。 链接里都有题意,我就不用再在文章里写了
我们考察一下每一棵子树是怎么构建起来的。
由于操作等价于长叶子和把一些树边伸长,所以已经分出的枝杈是无法改变的,那么对于一棵子树的树根,在两个及以上儿子子树中出现节点之前,它自己就必须出现。
除了有这个限制之外,这题就是一个普通的节点顺次出现的方案数的问题。所以我们可以树形DP,那么每个节点要么在子树内第一个出现,要么在某一个儿子子树出现一些节点后出现,枚举一下然后用组合数计算即可。
#include<bits/stdc++.h>
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=200005;
const ll INF=1e18;
ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
if(x<0)putchar('-'),x=-x;
ptf[lpt=1]=x%10;
while(x>9)x/=10,ptf[++lpt]=x%10;
while(lpt)putchar(ptf[lpt--]^48);
if(c>0)putchar(c);
}
const ll MOD=998244353;
ll ksm(ll a,ll b,ll mo){
ll res=1;
for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
return res;
}
ll fac[MAXN],inv[MAXN];
int init(int n){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
inv[n]=ksm(fac[n],MOD-2,MOD);
for(int i=n-1;i>1;i--)inv[i]=inv[i+1]*(i+1)%MOD;
return 114514;
}
int cbddl=init(MAXN-4);
ll C(int n,int m){
if(m>n||m<0)return 0;
return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
ll IC(int n,int m){
if(m>n||m<0)return 0;
return inv[n]*fac[m]%MOD*fac[n-m]%MOD;
}
struct edge{
int v,to;edge(){}
edge(int V,int T){v=V,to=T;}
}e[MAXN<<1];
int EN,G[MAXN];
void addedge(int u,int v){
e[++EN]=edge(v,G[u]),G[u]=EN;
e[++EN]=edge(u,G[v]),G[v]=EN;
}
int n,siz[MAXN];
ll f[MAXN];
void dfs(int x,int fa){
siz[x]=0;ll ans=1;
for(int i=G[x],v;i;i=e[i].to){
if((v=e[i].v)==fa)continue;
dfs(v,x),siz[x]+=siz[v],(ans*=C(siz[x],siz[v])*f[v]%MOD)%=MOD;
}f[x]=ans;
if(x^1){
for(int i=G[x],v;i;i=e[i].to){
if((v=e[i].v)==fa)continue;
(f[x]+=ans*IC(siz[x],siz[v])%MOD*C(siz[x],siz[v]-1))%=MOD;
}
}siz[x]++;
}
int main()
{
n=read();
for(int i=1;i<n;i++)addedge(read(),read());
dfs(1,0);
print(f[1]);
return 0;
}
第一条结论:把有向图缩点后可以得到一条有向的链。
这个你可以考虑调整法,因为原图是个完全图,所以一定可以用恰好
n
?
1
n-1
n?1 条强连通分量外的边把分量连成一条有向链。
那么此时的强连通分量个数就等于链上的边数+1。此时一条链上的边,它的意义其实等价于某种把所有点分成
S
S
S 集合和
T
T
T 集合(不为空)的方法,使得跨集合的边中只有
S
→
T
S\rightarrow T
S→T 方向的边。
于是我们可以枚举集合
S
,
T
S,T
S,T,算出这种分割合法的概率,然后就可以求和算出缩点后链上边数的期望。
显然直接
O
(
2
n
)
O(2^n)
O(2n) 枚举是行不通的,但是这题保证了两个方向概率不等的边的数量不超过19,也就是说这些边连成的连通块大小最大为20。我们可以枚举每一个这样的连通块,然后枚举它的子集做状压DP,最后用类似背包的方法把连通块的答案合并起来即可。由于其它边两个方向概率相等,所以可以直接算。
总复杂度
O
(
2
m
+
1
m
+
n
2
)
O(2^{m+1}m+n^2)
O(2m+1m+n2)。
#include<bits/stdc++.h>
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=2333;
const ll INF=1e18;
ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
if(x<0)putchar('-'),x=-x;
ptf[lpt=1]=x%10;
while(x>9)x/=10,ptf[++lpt]=x%10;
while(lpt)putchar(ptf[lpt--]^48);
if(c>0)putchar(c);
}
const ll MOD=998244353,iv2=(MOD+1)>>1;
ll ksm(ll a,ll b,ll mo){
ll res=1;
for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
return res;
}
int n,m,w[233][233],cnt[1<<20];
ll mi[2333],iv,dp[233],f[233],g[1<<20],ans=1;
bool vis[233];
void dfs(int x,vector<int>&V){
V.push_back(x),vis[x]=1;
for(int v=1;v<=n;v++)if(!vis[v]&&w[x][v]!=iv2)dfs(v,V);
}
int main()
{
n=read(),m=read(),iv=ksm(10000,MOD-2,MOD);
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)w[i][j]=iv2;
mi[0]=1;
for(int i=1;i<=2333-5;i++)mi[i]=mi[i-1]*iv2%MOD;
for(int i=1;i<=m;i++){
int u=read(),v=read();
w[u][v]=read()*iv%MOD,w[v][u]=(MOD+1-w[u][v])%MOD;
}
for(int i=1;i<(1<<(m+1));i++)cnt[i]=cnt[i>>1]+(i&1);
int sum=0;
dp[0]=1;
for(int x=1;x<=n;x++)if(!vis[x]){
vector<int>a;
dfs(x,a);
int k=a.size();
for(int i=0;i<=k;i++)f[i]=0;
for(int s=0;s<(1<<k);s++)g[s]=1;
for(int i=0;i<k;i++)
for(int j=0;j<k;j++)if((i^j)&&w[a[i]][a[j]]!=iv2)
for(int s=1;s<(1<<k);s++)if(((s>>i)&1)&&(~(s>>j)&1))
(g[s]*=(w[a[i]][a[j]]<<1))%=MOD;
for(int s=0;s<(1<<k);s++)
(f[cnt[s]]+=g[s]*mi[cnt[s]*(k-cnt[s])])%=MOD;
for(int i=sum+k;i>=0;i--){
(dp[i]*=f[0]*mi[i*k]%MOD)%=MOD;
for(int j=min(i,k);j>0;j--)if(i-j<=sum)
(dp[i]+=dp[i-j]*f[j]%MOD*mi[j*(sum-i+j)]%MOD*mi[(i-j)*(k-j)])%=MOD;
}sum+=k;
}
for(int i=1;i<n;i++)(ans+=dp[i])%=MOD;
print(ans*ksm(10000,n*(n-1),MOD)%MOD);
return 0;
}
看到部分分的提示我们可以发现,最优解一定是一条链,每个节点滑稽值不大于它的父亲。
然后我们贪心地想,如果一个节点的滑稽值等于它的父亲,那么把它从链中抽出去放到链尾一定不劣。所以节点的滑稽值相当于要先不断变小,直到变为所有
a
i
a_i
ai? 的按位与和,然后剩下的滑稽值全部等于这个值。
在变小的那部分节点中,儿子节点的滑稽值一定是父亲节点的滑稽值的(位运算)子集。由于滑稽值与上某个
a
i
a_i
ai? 过后再与一个相同的值一定不优,所以我们假定
a
i
a_i
ai? 能够重复用,那么就可以用FWT预处理一下然后
O
(
1
)
O(1)
O(1) 判断某个滑稽值能否一步变为另一个值。
然后就可以用枚举子集的方法DP了。原本DP的形式为
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示链上第
i
i
i 个点滑稽值为
j
j
j (
i
i
i 后面的点的滑稽值为所有
a
i
a_i
ai? 的按位与和)时的最小滑稽值总和,由于滑稽值单减所以可以用倒着枚举
j
j
j 的方式把第一维去掉。
总复杂度就是每个值状态枚举一遍子集的复杂度,为
O
(
3
log
?
n
)
=
O
(
n
log
?
2
3
)
≈
O
(
n
n
)
O(3^{\log n})=O(n^{\log_23})≈O(n\sqrt{n})
O(3logn)=O(nlog2?3)≈O(nn
?)。
#include<bits/stdc++.h>
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=1<<18;
const ll INF=1e18;
ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
if(x<0)putchar('-'),x=-x;
ptf[lpt=1]=x%10;
while(x>9)x/=10,ptf[++lpt]=x%10;
while(lpt)putchar(ptf[lpt--]^48);
if(c>0)putchar(c);
}
int n,lim=(1<<18)-1,sum=lim;
ll f[MAXN+5];
bool g[MAXN+5];
ll MIN(ll x,ll y){return x<y?x:y;}
#define min MIN
int main()
{
n=read();
for(int i=0;i<=lim;i++)f[i]=INF;
for(int i=1,x;i<=n;i++)x=read(),f[x]=x,sum&=x,g[x]=1;
for(int i=0;i<=lim;i++)if(f[i]<INF)f[i]+=sum*(n-1ll);
for(int k=1;k<=lim;k<<=1)
for(int i=0;i<=lim;i++)if(i&k)g[i]|=g[i^k];
for(int s=lim;s>0;s--)if(f[s]<INF)
for(int t=s;t>0;){
t=(t-1)&s;
if(g[lim^s^t])f[t]=min(f[t],f[s]-sum+t);
}
print(f[sum]);
return 0;
}
我们设
f
(
n
k
)
f{n\choose k}
f(kn?) 表示
(
n
k
)
{n\choose k}
(kn?) 质因数分解后
P
P
P 处的次数,那么有
f
(
n
k
)
=
∑
i
=
1
+
∞
?
n
P
i
?
?
?
k
P
i
?
?
?
n
?
k
P
i
?
f{n\choose k}=\sum_{i=1}^{+\infty}\lfloor\frac{n}{P^i}\rfloor-\lfloor\frac{k}{P^i}\rfloor-\lfloor\frac{n-k}{P^i}\rfloor
f(kn?)=i=1∑+∞??Pin????Pik????Pin?k?? 我们把
n
n
n 和
k
k
k 都转化成
P
P
P 进制后可以发现,这个式子其实就等于
k
k
k 和
n
?
k
n-k
n?k 做加法时产生进位的次数。
所以我们把
A
A
A 转化成
P
P
P 进制,然后从低位到高位做一个简单的数位DP即可。想怎么做都行,只要复杂度不超过长度的平方。
#include<bits/stdc++.h>
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=100005;
const ll INF=1e18;
ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
if(x<0)putchar('-'),x=-x;
ptf[lpt=1]=x%10;
while(x>9)x/=10,ptf[++lpt]=x%10;
while(lpt)putchar(ptf[lpt--]^48);
if(c>0)putchar(c);
}
const ll MOD=1e9+7,iv2=(MOD+1)>>1;
ll ksm(ll a,ll b,ll mo){
ll res=1;
for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
return res;
}
ll P,k,a[23333],f[2][23333][2][2],ans;
int n;
char in[23333];
ll pr(ll x){
if(x<0)return 0;
if(x<P)return ((x+2)*(x+1)>>1)%MOD;
x=min(x,(P<<1)-2);
return (((P+1)*P+(P+(P<<1)-2-x)*(x-P+1))>>1)%MOD;
}
ll CG(ll l,ll r){
if(l>r||r<0)return 0;
l=max(l,0ll),r=max(r,-1ll);
return pr(r)-pr(l-1)+MOD;
}
int main()
{
P=read(),k=read(),n=1;
scanf("%s",in);
for(int id=0,lim=strlen(in);id<lim;id++){
int c=in[id]^48;
for(int i=0;i<n;i++)a[i]*=10;
a[0]+=c;
for(int i=0;i<n;i++)
if(a[i]>=P)a[i+1]+=a[i]/P,a[i]%=P,n=max(n,i+2);
}
f[1][0][0][0]=1;
for(int id=0;id<=n;id++){
bool e=id&1,t=e^1;
for(int i=0;i<=id+1;i++)
f[e][i][0][0]=f[e][i][0][1]=f[e][i][1][0]=f[e][i][1][1]=0;
for(int i=0;i<=id;i++)
for(int u=0;u<2;u++)
for(int v=0;v<2;v++){
ll d=f[t][i][u][v];
if(!d)continue;
(f[e][i][0][0]+=d*CG(0,a[id]-v-1))%=MOD;
(f[e][i][u][0]+=d*CG(a[id]-v,a[id]-v))%=MOD;
(f[e][i][1][0]+=d*CG(a[id]-v+1,P-v-1))%=MOD;
(f[e][i+1][0][1]+=d*CG(P-v,P+a[id]-v-1))%=MOD;
(f[e][i+1][u][1]+=d*CG(P+a[id]-v,P+a[id]-v))%=MOD;
(f[e][i+1][1][1]+=d*CG(P+a[id]-v+1,(P<<1)-2))%=MOD;
}
}
for(int i=k;i<=n;i++)(ans+=f[n&1][i][0][0])%=MOD;
print(ans);
return 0;
}
我们可以放心地直接破环成链,因为这样不会改变连通性。然后注意到连通块只会包含不会交叉,于是我们可以很容易地往区间DP上面想。
朴素的区间DP可以解决无限制的连边,但是加上限制不仅复杂度变大而且还会假。原始的DP是一个简单的线性变换,所有东西都是用DP求的,所以可以猜到正解肯定要把某些东西直接求。
如果是
n
n
n 个点之间随便连边的话,可以推出当
n
n
n 为奇数时方案数为0,为偶数时方案数为
1
?
3
?
5
?
.
.
.
?
(
n
?
1
)
1\cdot3\cdot5\cdot...\cdot(n-1)
1?3?5?...?(n?1)。这个时候我们可以很容易推广到有限制的情况,因为除了已经连边的点以外,其他点之间显然是随便连的,所以算出任何一个点集内点的连边方案数。
我们考虑计算每个连通块的贡献。假设这个连通块恰好包含在区间
[
l
,
r
]
[l,r]
[l,r] 内,那么我们只需要满足
l
l
l 和
r
r
r 连通即可。显然如果不连通的话,由于
l
l
l 处于区间边界,它所在连通块不会被区间内的其它连通块包含,所以我们如果容斥来算贡献的话会方便很多。
设
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示
i
,
j
i,j
i,j 连通且区间
[
i
,
j
]
[i,j]
[i,j] 内外不连通的方案数,那么有
f
[
i
]
[
j
]
=
g
(
i
,
j
)
?
∑
k
=
i
j
?
1
f
[
i
]
[
k
]
?
g
(
k
+
1
,
j
)
f[i][j]=g(i,j)-\sum_{k=i}^{j-1}f[i][k]\cdot g(k+1,j)
f[i][j]=g(i,j)?k=i∑j?1?f[i][k]?g(k+1,j) 其中
g
(
i
,
j
)
g(i,j)
g(i,j) 表示区间
[
i
,
j
]
[i,j]
[i,j] 内的点随意连边(去掉已经连了的点)的方案数。
这个DP总时间
O
(
n
3
)
O(n^3)
O(n3),最后再统计一下贡献即可。
#include<bits/stdc++.h>
#define ll long long
#define lll __int128
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=2333;
const ll INF=1e18;
ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
if(x<0)putchar('-'),x=-x;
ptf[lpt=1]=x%10;
while(x>9)x/=10,ptf[++lpt]=x%10;
while(lpt)putchar(ptf[lpt--]^48);
if(c>0)putchar(c);
}
const ll MOD=1e9+7,iv2=(MOD+1)>>1;
ll ksm(ll a,ll b,ll mo){
ll res=1;
for(;b;b>>=1,a=a*a%mo)if(b&1)res=res*a%mo;
return res;
}
int n,k,mat[MAXN],a[MAXN][MAXN],b[MAXN][MAXN];
ll f[MAXN][MAXN],g[MAXN],ans;
int main()
{
n=read()<<1,k=read();
for(int i=1,u,v;i<=k;i++)u=read(),v=read(),mat[u]=v,mat[v]=u;
g[0]=1;
for(int i=1;i<=n;i++)g[i<<1]=g[(i-1)<<1]*((i<<1)-1)%MOD;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++){
a[i][j]=a[i][j-1],b[i][j]=b[i][j-1];
if(mat[j]>0){
b[i][j]++;
if(mat[j]<j&&mat[j]>=i)a[i][j]+=2;
}
}
for(int i=0;i<=n;i++)f[i+1][i]=1;
for(int i=n;i>0;i--)
for(int j=i+1;j<=n;j+=2)if(a[i][j]==b[i][j]){
f[i][j]=g[j-i+1-b[i][j]];
for(int k=i+1;k<j;k+=2)
(f[i][j]+=MOD-f[i][k]*g[j-k-b[k+1][j]]%MOD)%=MOD;
(ans+=f[i][j]*g[n-(k<<1)-j+i-1+b[i][j]])%=MOD;
}
print(ans);
return 0;
}
|