【冲刺NOI】DP专训

本文探讨了动态规划在解决复杂问题中的应用,包括树形DP、完全图的强连通分量分析、有向图链的构建策略。此外,还详细解析了滑稽树上的滑稽果问题,通过贪心思想找到最优解。同时,文章涵盖了数位DP、质因数分解在组合数学中的应用,以及区间DP解决弦连通性问题的方法。通过对各种算法的深入剖析,展示了动态规划与图论在解决实际问题中的强大能力。
摘要由CSDN通过智能技术生成

前言

五道我不会做的DP黑题。
链接里都有题意,我就不用再在文章里写了

UOJ607-跳蚤电话

我们考察一下每一棵子树是怎么构建起来的。

由于操作等价于长叶子和把一些树边伸长,所以已经分出的枝杈是无法改变的,那么对于一棵子树的树根,在两个及以上儿子子树中出现节点之前,它自己就必须出现。

除了有这个限制之外,这题就是一个普通的节点顺次出现的方案数的问题。所以我们可以树形DP,那么每个节点要么在子树内第一个出现,要么在某一个儿子子树出现一些节点后出现,枚举一下然后用组合数计算即可。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#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;
}

UOJ181-密码锁

第一条结论:把有向图缩点后可以得到一条有向的链。

这个你可以考虑调整法,因为原图是个完全图,所以一定可以用恰好 n − 1 n-1 n1 条强连通分量外的边把分量连成一条有向链。

那么此时的强连通分量个数就等于链上的边数+1。此时一条链上的边,它的意义其实等价于某种把所有点分成 S S S 集合和 T T T 集合(不为空)的方法,使得跨集合的边中只有 S → T S\rightarrow T ST 方向的边。

于是我们可以枚举集合 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>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#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;
}

UOJ370-滑稽树上滑稽果

看到部分分的提示我们可以发现,最优解一定是一条链,每个节点滑稽值不大于它的父亲。

然后我们贪心地想,如果一个节点的滑稽值等于它的父亲,那么把它从链中抽出去放到链尾一定不劣。所以节点的滑稽值相当于要先不断变小,直到变为所有 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(nlog23)O(nn )

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#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]);//只有sum处的值是合法且最优的
	return 0;
}

CF582D-Number of Binominal Coefficients

我们设 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+PinPikPink
我们把 n n n k k k 都转化成 P P P 进制后可以发现,这个式子其实就等于 k k k n − k n-k nk 做加法时产生进位的次数。

所以我们把 A A A 转化成 P P P 进制,然后从低位到高位做一个简单的数位DP即可。想怎么做都行,只要复杂度不超过长度的平方。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#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;
}

AGC028D-Chords

我们可以放心地直接破环成链,因为这样不会改变连通性。然后注意到连通块只会包含不会交叉,于是我们可以很容易地往区间DP上面想。

朴素的区间DP可以解决无限制的连边,但是加上限制不仅复杂度变大而且还会假。原始的DP是一个简单的线性变换,所有东西都是用DP求的,所以可以猜到正解肯定要把某些东西直接求。

如果是 n n n 个点之间随便连边的话,可以推出当 n n n 为奇数时方案数为0,为偶数时方案数为 1 ⋅ 3 ⋅ 5 ⋅ . . . ⋅ ( n − 1 ) 1\cdot3\cdot5\cdot...\cdot(n-1) 135...(n1)。这个时候我们可以很容易推广到有限制的情况,因为除了已经连边的点以外,其他点之间显然是随便连的,所以可以算出任何一个点集内点的连边方案数。

我们考虑计算每个连通块的贡献。假设这个连通块恰好包含在区间 [ 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=ij1f[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>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#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;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值