20200708 DP难题训练(神仙题

CF1363F Rotating Substrings

在这里插入图片描述
旋转相当于可以把 s s s 中的一个字符提前。

editorial 1 最长公共子序列,很好理解。
editorial 2 神奇的延迟DP,两种转移相互配合,最后两个要完全匹配,说明中间的匹配过程是合法的, f [ i ] [ j ] f[i][j] f[i][j] 的定义为 s [ 1 , i ] s[1,i] s[1,i] + 若干后面提到前面来的字符 与 t [ 1 , j ] t[1,j] t[1,j] 匹配时已经确定的贡献(提前的贡献放在两个转移中的任意一个都可以)

Code:

#include<bits/stdc++.h>
#define maxn 2005
using namespace std;
int T,n,cnt[2][maxn][26],f[maxn][maxn];
char s[maxn],t[maxn];
int main()
{
	for(scanf("%d",&T);T--;){
		scanf("%d%s%s",&n,s+1,t+1);
		for(int i=1;i<=n;i++){
			memcpy(cnt[0][i],cnt[0][i-1],26*4);
			memcpy(cnt[1][i],cnt[1][i-1],26*4);
			cnt[0][i][s[i]-'a']++,cnt[1][i][t[i]-'a']++;
		}
		bool flg=0;
		for(int i=0;i<26;i++) if(cnt[0][n][i]!=cnt[1][n][i]) {flg=1;break;}
		if(flg) {puts("-1");continue;}
		for(int i=0;i<=n;i++) f[0][i]=0;
		for(int i=1;i<=n;i++)
			for(int j=i;j<=n;j++){
				f[i][j]=f[i-1][j]+1;
				if(s[i]==t[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
				if(cnt[0][i][t[j]-'a']<cnt[1][j][t[j]-'a'])
					f[i][j]=min(f[i][j],f[i][j-1]);
			}
		printf("%d\n",f[n][n]);
	}
}

CF1185G2 Playlist for Polycarp (hard version)

在这里插入图片描述
1 ≤ g i ≤ 3 1\le g_i\le3 1gi3

考虑直接DP,首先,因为编号小于等于3,所以我们可以DP出 p i , j , k p_{i,j,k} pi,j,k 表示编号为1,2,3的分别有 i , j , k i,j,k i,j,k个时排成一排没有相邻相同的方案数(再加一维表示最后一位是什么来转移),然后就只需要考虑从编号为1的里面选 i i i个,编号为2的里面选 j j j个,编号为3的里面选 k k k个组成时长为 T T T 的方案数。

p p p 的复杂度与 T T T 无关, O ( n 3 ∗ 常 数 ) O(n^3*常数) O(n3),忽略不计。

求组成时长为 t t t 的选择方案,直接DP的话每次要枚举 i , j , k , t i,j,k,t i,j,k,t来转移,复杂度 O ( ( n 3 ) 3 T n ) O((\frac n3)^3Tn) O((3n)3Tn)

因为最后只求一个位置的值,所以可以考虑 m e e t   i n   t h e   m i d d l e meet~in~the~middle meet in the middle,如果拆分成3个分别算 f i , t 1 , g j , t 2 , h k , t 3 f_{i,t_1},g_{j,t_2},h_{k,t_3} fi,t1,gj,t2,hk,t3,计算的复杂度是 O ( n 2 T ) O(n^2T) O(n2T),合并的复杂度是 O ( ( n 3 ) 3 T 3 ) O((\frac n3)^3T^3) O((3n)3T3)

拆分越多,计算复杂度越低( n n n幂次降低),合并复杂度越高( n T nT nT幂次升高)。
f f f 单独算, g h gh gh 合到一起,计算的复杂度为 O ( n T + n 2 T ) O(nT+n^2T) O(nT+n2T),合并的复杂度为 O ( ( n 3 ) 3 T ) O((\frac n3)^3T) O((3n)3T)。可以通过。

Code:

#include<bits/stdc++.h>
#define maxn 55
#define maxt 2505
#define rep(i,j,k) for(int i=(j),LIM=(k);i<=LIM;i++)
#define per(i,j,k) for(int i=(j),LIM=(k);i>=LIM;i--)
using namespace std;
const int mod = 1e9+7;
int n,T,cnt[4],sum[4],fac[maxn],f[maxn][maxt],gh[maxn][maxn][maxt],p[maxn][maxn][maxn][4],s[maxn][maxn][maxn];
void add(int &a,int b){(a+=b)>=mod&&(a-=mod);}
int main()
{
	scanf("%d%d",&n,&T);
	f[0][0]=gh[0][0][0]=fac[0]=1;
	for(int o=1,t,id;o<=n;o++){
		scanf("%d%d",&t,&id);
		if(id==1){
			per(i,cnt[1],0) per(j,sum[1],0) if(f[i][j]) add(f[i+1][j+t],f[i][j]);
		}
		else{
			per(i,cnt[2],0) per(j,cnt[3],0) per(k,sum[2]+sum[3],0) if(gh[i][j][k])
				add(gh[i+(id==2)][j+(id==3)][k+t],gh[i][j][k]);
		}
		fac[o]=1ll*fac[o-1]*o%mod;
		cnt[id]++,sum[id]+=t;
	}
	p[1][0][0][1]=p[0][1][0][2]=p[0][0][1][3]=1;
	rep(i,0,cnt[1]) rep(j,0,cnt[2]) rep(k,0,cnt[3]){
		add(p[i+1][j][k][1],(p[i][j][k][2]+p[i][j][k][3])%mod);
		add(p[i][j+1][k][2],(p[i][j][k][1]+p[i][j][k][3])%mod);
		add(p[i][j][k+1][3],(p[i][j][k][1]+p[i][j][k][2])%mod);
		s[i][j][k]=(1ll*p[i][j][k][1]+p[i][j][k][2]+p[i][j][k][3])*fac[i]%mod*fac[j]%mod*fac[k]%mod;
	}
	int ans=0;
	rep(i,0,cnt[1]) rep(t,0,sum[1]) if(f[i][t])
		rep(j,0,cnt[2]) rep(k,0,cnt[3])
			add(ans,1ll*f[i][t]*gh[j][k][T-t]%mod*s[i][j][k]%mod);
	printf("%d\n",ans);
}

AT5149 [AGC036F] Square Constraints

在这里插入图片描述
超详细的社论

可以求得每个数的上下界 l b i ≤ P i ≤ r b i lb_i\le P_i\le rb_i lbiPirbi

如果没有下界,是个经典问题,从小到大排序之后每个位置可取的个数是上界减去前面已经选的数的个数(此题因为下标从0开始,要+1)。

考虑容斥,钦定有 k k k 个不满足下界,相当于将上界改为 l b i − 1 lb_i-1 lbi1
要计算方案,在选择每个数的时候,我们需要知道有多少个上界是小于等于当前的上界的(还有一些信息,总之就是要知道它能选多少个数)

于是观察性质可以得到一些单调性的结论:
max ⁡ i = 0 n − 1 l b i − 1 ≤ max ⁡ i = 0 n − 1 r b i \max_{i=0}^{n-1} lb_i-1 \le \max_{i=0}^{n-1} rb_i maxi=0n1lbi1maxi=0n1rbi
l b i , r b i lb_i,rb_i lbi,rbi i i i 增大单调不升,对于 l b i < l b j lb_i<lb_j lbi<lbj,有 r b i ≥ r b j rb_i\ge rb_j rbirbj,对于 i ∈ [ 0 , N ) , j ∈ [ N , 2 N ) i\in[0,N),j\in[N,2N) i[0,N),j[N,2N),有 r b i ≥ r b j rb_i\ge rb_j rbirbj

然后就可以左半边按 l b − 1 lb-1 lb1 为第一关键字, r b rb rb 为第二关键字,右半边按照 r b rb rb 为第一关键字, 0 0 0 为第二关键字 排序之后DP了,详见上面的editorial,代码里面也有注释。

Code:

#include<bits/stdc++.h>
#define maxn 505
#define fi first
#define se second
using namespace std;
int n,mod,f[maxn][maxn];
pair<int,int>a[maxn];
int solve(int k){
	int cnt1=0,cnt2=0;
	f[0][0]=1;
	for(int i=0;i<2*n;i++)
		if(a[i].se==0){//front rb in [N,2N-1] + front choose.
			for(int j=0;j<=cnt2;j++)
				f[i+1][j]=1ll*f[i][j]*(a[i].fi-(cnt1+j)+1)%mod;
			cnt1++;
		}
		else{
			//lb-1, front rb in [N,2N-1] + front choose.
			for(int j=0;j<=cnt2;j++)
				f[i+1][j+1]=1ll*f[i][j]*(a[i].fi-(cnt1+j)+1)%mod;
			//rb, all rb in [N,2N-1] + front not choose(lb_i<lb_j -> rb_i<rb_j) + k
			f[i+1][0]=0;
			for(int j=0;j<=cnt2;j++)
				f[i+1][j]=(f[i+1][j]+1ll*f[i][j]*(a[i].se-(n+cnt2-j+k)+1))%mod;
			cnt2++;
		}
	return f[2*n][k];
}
int main()
{
	scanf("%d%d",&n,&mod);
	for(int i=0;i<n;i++)
		a[i].fi=ceil(sqrt(n*n-i*i))-1,a[i].se=sqrt(4*n*n-i*i);
	for(int i=n;i<2*n;i++)
		a[i].fi=sqrt(4*n*n-i*i),a[i].se=0;
	a[0].se=2*n-1,sort(a,a+2*n);
	int ans=0;
	for(int i=n;i>=0;i--) ans=(solve(i)-ans+mod)%mod;
	printf("%d\n",ans);
}

CF1225G To Make 1

合并 n n n 个数为 1 个,每次合并 x x x y y y 时令 z = x + y z=x+y z=x+y,将 z z z 除以 k k k 直到不能被整除,删除 x , y x,y x,y, 加入 z z z。问合并完后能否得到 1 1 1,并输出方案。
n ≤ 16 ;   k , ∑ a i ≤ 2000 n\le16;~k,\sum a_i \le2000 n16; k,ai2000,保证 k ∤    a i k\not | ~~a_i k  ai

问题可以转化为寻找一组解 { b i } \{b_i\} {bi} 满足 1 = ∑ a i k − b i 1=\sum a_ik^{-b_i} 1=aikbi

首先合并为1的过程肯定可以推出一组 b i b_i bi.
然后证明任意一组 b i b_i bi 都可以构造出合并为1的方法:
如果 n = 1 n=1 n=1,显然。
如果 n > 1 n>1 n>1,令 B = max ⁡ b i ≥ 1 B=\max b_i\ge 1 B=maxbi1,那么有 k B = ∑ a i k B − b i k^B=\sum a_ik^{B-b_i} kB=aikBbi
对于 b i < B b_i<B bi<B a i k B − b i a_ik^{B-b_i} aikBbi 能被 k k k 整除,并且等式左边能被 k k k 整除,那么 b i = B b_i=B bi=B a i a_i ai 的和要能被 k k k 整除,因为 k ∤    a i k\not | ~~a_i k  ai,所以这样的 i i i 的个数大于 1,任取两个设为 j , k j,k j,k
合并 a j , a k a_j,a_k aj,ak,得到新的 a ′ , b ′ a',b' a,b a ′ a' a 仍然不能被 k k k 整除,并且仍然满足一开始的等式,问题变为 n − 1 n-1 n1 的子问题。

那么现在问题就是找出一组这样的解。
通过不断在等式两边同乘 k min ⁡ b i k^{\min b_i} kminbi 可以发现, b i b_i bi 的生成过程可以看做是每次加入一个集合的数,然后整体除上 k k k 的若干次幂。于是可以令 f [ S ] [ x ] f[S][x] f[S][x] 表示加入了 S S S 集合里的数,能否得到 x = ∑ a i − b i ′ x=\sum a_i^{-b_i'} x=aibi,转移就是添加一个数,然后整体除去 k k k 的若干次幂。可以用 bitset 优化,复杂度 O ( 2 n n ∗ 2000 / w ) O(2^nn*2000/w) O(2nn2000/w)

f f f 数组还原 b i b_i bi 时初始令 x = 1 x=1 x=1,不断乘上 k k k 直到再乘的状态不为1,此时一定是由添加一个数转移过来的。乘的过程中给当前集合里的 b i + + b_i++ bi++

最后由 b i b_i bi 还原合并过程就是如上的证明过程。

Code:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1<<16;
int n,N,k,a[16],sum,d[16];
bitset<2005>f[maxn];
void dfs(int s,int v){
	if(!s) return;
	for(;v*k<=sum&&f[s][v*k];v*=k)
		for(int i=0;i<n;i++) if(s>>i&1) d[i]++;
	for(int i=0;i<n;i++) if(s>>i&1&&v>=a[i]&&f[s^1<<i][v-a[i]]) {dfs(s^1<<i,v-a[i]);return;}
}
priority_queue<pair<int,int> >q;
int main()
{
	scanf("%d%d",&n,&k),N=1<<n;
	for(int i=0;i<n;i++) scanf("%d",&a[i]),sum+=a[i];
	f[0][0]=1;
	for(int s=1;s<N;s++){
		for(int i=0;i<n;i++) if(s>>i&1) f[s]|=f[s^1<<i]<<a[i];
		for(int i=sum/k;i;i--) if(f[s][i*k]) f[s][i]=1;
	}
	if(!f[N-1][1]) puts("NO");
	else{
		puts("YES");
		dfs(N-1,1);
		for(int i=0;i<n;i++) q.push(make_pair(d[i],a[i]));
		while(q.size()>1){
			pair<int,int> a = q.top(); q.pop();
			pair<int,int> b = q.top(); q.pop();
			printf("%d %d\n",a.second,b.second);
			a.second += b.second;
			while(a.second%k==0) a.second/=k,a.first--;
			q.push(a);
		}
	}
}

AT4120 [ARC096D] Sweet Alchemy

在这里插入图片描述
首先差分,问题变为每次选择一棵子树,权值为 s z sz sz,花费为 ∑ m i \sum m_i mi,且除了1号点的子树最多选 d d d 次。

直接背包的话,因为权值和花费都很大,无论哪种状态都没法做。

考虑到 n n n 很小,我们有一种复杂度与权值和花费都无关的选物品方法:贪心,尽可能选择性价比大的物品。
直接贪心不对,我们考虑贪心能在什么情况下适用:
i , j i,j i,j 两个物品的权值分别为 v i , v j v_i,v_j vi,vj,且权值比 v i w i < v j w j \frac {v_i}{w_i}<\frac {v_j}{w_j} wivi<wjvj,那么如果 i i i 选了 v j v_j vj 个物品,就可以替换为 j j j v i v_i vi 个物品,权值不变,花费变小,此时按贪心的顺序选是对的。

所以可以把 min ⁡ ( d , max ⁡ v i ) \min(d,\max v_i) min(d,maxvi) 个物品拿来做背包,剩下的部分贪心选择。因为权值最大是 s z = n sz=n sz=n,可以设 f [ i ] f[i] f[i] 表示权值和为 i i i 时的最小花费,然后枚举一个权值,剩下的花费贪心选择。
复杂度 O ( n 3 ∗ n log ⁡ n + n 4 ) O(n^3*n\log n+n^4) O(n3nlogn+n4),权值和的范围是 n 3 n^3 n3

贪心的正确性可以这么理解:
在最优解中,如果最终 j j j 的个数剩下 ≥ n \ge n n 个, i i i 选的个数一定 ≤ n \le n n,当我们背包确定了 i i i n n n 以内选多少个时,剩下的花费就按顺序先给 j j j 贪心选,能够取到最优解。
如果最终剩下 j j j 的个数 x < n x<n x<n 个,那么背包确定选 n − x n-x nx 个,剩下的 d − n d-n dn 个贪心会全部选满(因为贪心的最大个数是 d − n d-n dn,不会再去取那 x x x 里面的),此时恰好能够取到最优解,而不会出现 j j j 的贪心错误地占据了后面的花费的情况。

Code:

#include<bits/stdc++.h>
#define maxn 55
#define LL long long
using namespace std;
int n,m,d,V,lim,ans;
LL f[maxn*maxn*maxn];
struct node{
	int v,id; LL w;
	bool operator < (const node &p)const{return v*p.w>w*p.v;}
}a[maxn];
vector<int>G[maxn];
void dfs(int u){for(int v:G[u]) dfs(v),a[u].v+=a[v].v,a[u].w+=a[v].w;}
void ins(LL v,LL w){for(int i=V;i>=v;i--) f[i]=min(f[i],f[i-v]+w);}
int main()
{
	scanf("%d%d%d%d",&n,&m,&d,&a[1].w),a[1].v=a[1].id=1;
	for(int i=2,x;i<=n;i++) scanf("%d%d",&a[i].w,&x),a[i].v=1,G[x].push_back(i);
	dfs(1);
	memset(f,0x3f,sizeof f),f[0]=0;
	V = n*n*n, lim = min(n,d);
	for(int i=1;i<=n;i++){
		int x=lim;
		for(int j=1;j<=x;x-=j,j<<=1) ins(1ll*j*a[i].v,j*a[i].w);
		if(x) ins(1ll*x*a[i].v,x*a[i].w);
	}
	sort(a+1,a+1+n);
	while(a[n].id!=1) n--;
	for(int i=0;i<=V&&f[i]<=m;i++){
		int v=i,w=f[i];
		for(int j=1;j<n;j++){
			int k=min(1ll*d-lim,(m-w)/a[j].w);
			v+=k*a[j].v,w+=k*a[j].w;
		}
		int k=(m-w)/a[n].w;
		ans=max(ans,v+k*a[n].v);
	}
	printf("%d\n",ans);
}

CF1188D Make Equal

在这里插入图片描述
editorial

Code:

#include<bits/stdc++.h>
#define maxn 100005
#define LL long long
using namespace std;
int n,k,f[65][maxn],cnt[2][maxn];
LL a[maxn];
bool cmp(LL x,LL y){return (x&(1ll<<k)-1)<(y&(1ll<<k)-1);}
void chkmin(int &a,int b){a>b&&(a=b);}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++) a[i]=a[n]-a[i];
	memset(f,0x3f,sizeof f);
	f[0][0]=0;
	for(k=0;k<=58;k++){
		sort(a+1,a+1+n,cmp);
		for(int i=1;i<=n;i++){
			cnt[0][i]=cnt[0][i-1],cnt[1][i]=cnt[1][i-1];
			cnt[a[i]>>k&1][i]++;
		}
		for(int i=0;i<=n;i++) if(f[k][i]<0x3f3f3f3f){
			chkmin(f[k+1][ cnt[1][n]-cnt[1][n-i] ], f[k][i] + cnt[1][n-i]+cnt[0][n]-cnt[0][n-i]);
			chkmin(f[k+1][ n-cnt[0][n-i] ], f[k][i] + cnt[0][n-i]+cnt[1][n]-cnt[1][n-i]);
		}
	}
	printf("%d\n",f[59][0]);
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值