【做题记录】2025暑假-dp专题

A. The Bakery

\(dp_{i,j}\) 表示 \(i\) 为第 \(j\) 段的中点的最大价值,容易写出转移式:\(dp_{i,j}=\max_{k=0}^{i-1}\{dp_{k,j-1}+cost[k+1,i]\}\)。看到 \(\max\) 可以考虑线段树优化 DP,因此我们需要在 \(O(\log n)\) 的时间内求出 \(cost\)。考虑对于 \(i\),它的值上一次出现的位置为 \(pre_i\),那么 \([pre_i+1,i]\) 都会有 \(1\) 的贡献。于是进行一个区间加操作即可。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
#define lid id<<1
#define rid id<<1|1
using namespace std;
namespace asbt{
const int maxn=4e4+5;
int n,m,a[maxn],pre[maxn],pos[maxn];
struct{
	int tr[maxn<<2],tag[maxn<<2];
	il void pushup(int id){
		tr[id]=max(tr[lid],tr[rid]);
	}
	il void pushtag(int id,int v){
		tr[id]+=v,tag[id]+=v;
	}
	il void pushdown(int id){
		if(tag[id]){
			pushtag(lid,tag[id]);
			pushtag(rid,tag[id]);
			tag[id]=0;
		}
	}
	il void build(int id,int l,int r){
		tr[id]=tag[id]=0;
		if(l==r){
			return ;
		}
		int mid=(l+r)>>1;
		build(lid,l,mid);
		build(rid,mid+1,r);
	}
	il void add(int id,int L,int R,int l,int r,int v){
		if(L>=l&&R<=r){
			pushtag(id,v);
			return ;
		}
		pushdown(id);
		int mid=(L+R)>>1;
		if(l<=mid){
			add(lid,L,mid,l,r,v);
		}
		if(r>mid){
			add(rid,mid+1,R,l,r,v);
		}
		pushup(id);
	}
	il int query(int id,int L,int R,int l,int r){
		if(L>=l&&R<=r){
			return tr[id];
		}
		pushdown(id);
		int mid=(L+R)>>1,res=0;
		if(l<=mid){
			res=max(res,query(lid,L,mid,l,r));
		}
		if(r>mid){
			res=max(res,query(rid,mid+1,R,l,r));
		}
		return res;
	}
}S[2];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		pre[i]=pos[a[i]];
		pos[a[i]]=i;
	}
	for(int i=1,j=1;i<=m;i++,j^=1){
		S[j].build(1,0,n);
		for(int k=1;k<=n;k++){
			S[j^1].add(1,0,n,pre[k],k-1,1);
			S[j].add(1,0,n,k,k,S[j^1].query(1,0,n,0,k-1));
		}
	}
	cout<<S[m&1].query(1,0,n,n,n);
	return 0;
}
}
signed main(){return asbt::main();}

B. Birds

\(f_{i,j}\) 表示走到第 \(i\) 棵树,召唤了 \(j\) 只鸟的最大剩余魔法。时间复杂度 \(O((\sum c_i)^2)\)

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e3+5;
int n,W,B,X,a[maxn],b[maxn],sa[maxn],f[maxn][maxn*10];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>W>>B>>X;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sa[i]=sa[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		cin>>b[i];
	}
	memset(f,-1,sizeof(f));
	f[0][0]=W;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=sa[i];j++){
			for(int k=0;k<=min(a[i],j);k++){
				if(~f[i-1][j-k]&&f[i-1][j-k]>=b[i]*k){
					f[i][j]=max(f[i][j],f[i-1][j-k]-b[i]*k+X);
				}
			}
			f[i][j]=min(f[i][j],W+B*j);
		}
	}
	for(int i=sa[n];;i--){
		if(~f[n][i]){
			cout<<i;
			break;
		}
	}
	return 0;
}
}
signed main(){return asbt::main();}

C. Helping People

区间不交错,这让我们联想到建树。然而这是一个森林,再建一个虚点即可。

然后考虑树形 DP,设 \(f_{u,i}\) 表示 \(u\) 的区间,最大值为 \(i\) 的概率。显然时空都会爆炸,考虑 \(i\in[mx_u,mx_u+m]\),其中 \(mx_u=\max[l_u,r_u]\),因此设 \(f_{u,i}\) 表示 \(u\) 的区间,最大值为 \(mx_u+i\) 的概率。于是有转移:

\[f_{u,i}=p_u\times\prod f_{v,i+mx_u-mx_v-1}+(1-p_u)\times\prod f_{v,i+mx_u-mx_v} \]

时间复杂度 \(O(m^2)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=1e5+5,maxm=5e3+5;
int n,m,rt,a[maxn],deg[maxn];
double f[maxm][maxm];
vector<int> E[maxn],e[maxn];
queue<int> q;
struct{
	int l,r,mx;
	double p;
}b[maxm];
struct{
	int st[maxn][22],Log[maxn];
	il void build(){
		for(int i=2;i<=n;i++){
			Log[i]=Log[i>>1]+1;
		}
		for(int i=1;i<=n;i++){
			st[i][0]=a[i];
		}
		for(int j=1;j<=Log[n];j++){
			for(int i=1;i+(1<<j)-1<=n;i++){
				st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
			}
		}
	}
	il int query(int l,int r){
		int p=Log[r-l+1];
		return max(st[l][p],st[r-(1<<p)+1][p]);
	}
}ST;
il void dfs(int u){
//	cout<<u<<" "<<b[u].l<<" "<<b[u].r<<" "<<b[u].p<<"\n";
	for(int v:e[u]){
		dfs(v);
	}
	for(int i=0;i<=m;i++){
		double p1=b[u].p,p2=1-p1;
		for(int v:e[u]){
			int t1=i+b[u].mx-b[v].mx-1,t2=t1+1;
			p1*=f[v][min(t1,m)];
			p2*=f[v][min(t2,m)];
		}
		f[u][i]=i?p1+p2:p2;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",a+i);
	}
//	cerr<<"6666666\n";
	ST.build();
	for(int i=1;i<=m;i++){
//		cin>>b[i].l>>b[i].r>>b[i].p;
		scanf("%d%d%lf",&b[i].l,&b[i].r,&b[i].p);
	}
	b[++m].l=1,b[m].r=n,b[m].p=0;
	for(int i=1;i<=m;i++){
		b[i].mx=ST.query(b[i].l,b[i].r);
		for(int j=1;j<=m;j++){
			if(i==j){
				continue;
			}
			if(b[i].l==b[j].l&&b[i].r==b[j].r){
				if(i<j){
					E[i].pb(j),deg[j]++;
				}
				else{
					E[j].pb(i),deg[i]++;					
				}
			}
			else if(b[i].l<=b[j].l&&b[i].r>=b[j].r){
				E[i].pb(j),deg[j]++;
			}
			else if(b[i].l>=b[j].l&&b[i].r<=b[j].r){
				E[j].pb(i),deg[i]++;
			}
		}
	}
	for(int i=1;i<=m;i++){
		if(!deg[i]){
			rt=i;
			q.push(i);
			break;
		}
	}
	while(q.size()){
		int u=q.front();
		q.pop();
		for(int v:E[u]){
			if(--deg[v]==0){
				e[u].pb(v);
				q.push(v);
			}
		}
	}
	dfs(rt);
//	for(int i=1;i<=m;i++){
//		for(int j=0;j<=m;j++){
//			cout<<f[i][j]<<" ";
//		}
//		cout<<"\n";
//	}
	double ans=0;
	for(int i=0;i<=m;i++){
		ans+=(f[rt][i]-(i?f[rt][i-1]:0))*(i+b[rt].mx);
	}
	printf("%.8f",ans);
	return 0;
}
}
int main(){return asbt::main();}

D. Game on Sum (Easy Version)

我们发现,每一步的操作都和前面的操作无关,只和后面的操作有关,即有后效性而无前效性,于是考虑倒着 DP。设 \(f_{i,j}\) 表示 \(n=i\)\(m=j\) 时的答案,于是有 \(f_{i,j}=\frac{f_{i-1,j-1}+f_{i-1,j}}{2}\)。边界条件为 \(f_{i,0}=0\)\(f_{i,i}=i\times K\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e3+5,mod=1e9+7,_2=5e8+4;
int T,n,m,kk,f[maxn][maxn];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>T;
	while(T--){
		cin>>n>>m>>kk;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=min(i-1,m);j++){
				f[i][j]=(f[i-1][j-1]+f[i-1][j])*1ll*_2%mod;
			}
			if(i<=m){
				f[i][i]=i*1ll*kk%mod;
			}
		}
//		for(int i=0;i<=n;i++){
//			for(int j=0;j<=m;j++){
//				cout<<f[i][j]<<" ";
//			}
//			cout<<"\n";
//		}
		cout<<f[n][m]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

E. Positions in Permutations

考虑二项式反演,设 \(F_i\) 表示钦定有 \(i\) 个位置满足条件,其它的随便选的方案数。于是 \(ans=\sum_{i=K}^{n}(-1)^{i-m}{i\choose m}F_i\)。考虑 DP 求出 \(F\)

\(f_{i,j,0/1,0/1}\) 表示考虑了前 \(i\) 个位置,有 \(j\) 个位置满足条件,有没有选 \(i\),有没有选 \(i+1\) 的方案数。注意这里只考虑那些满足条件的位置,而不考虑不满足条件的位置,即不满足的位置此时是空的。不难得出 \(F_i=(f_{n,i,0,0}+f_{n,i,1,0})\times(n-i)!\)。转移方程考虑在 \(i\) 这一位填 \(i-1\)、填 \(i+1\) 或不填即可。

因为 CF 目前坏了所以暂时不能保证代码正确性,先不贴 code 了。(F 题同)

upd:CF 好了。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e3+5,mod=1e9+7;
int n,m,fac[maxn],C[maxn][maxn],f[maxn][maxn][2][2];
il int add(int x,int y){
	return x+y<mod?x+y:x+y-mod;
}
il int sub(int x,int y){
	return x<y?x-y+mod:x-y;
}
il void upd(int &x,int y){
	x=x+y<mod?x+y:x+y-mod;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	fac[0]=C[0][0]=1;
	for(int i=1;i<=n;i++){
		fac[i]=fac[i-1]*1ll*i%mod;
		C[i][0]=1;
		for(int j=1;j<=i;j++){
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
		}
	}
	f[1][0][0][0]=f[1][1][0][1]=1;
	for(int i=2;i<=n;i++){
		f[i][0][0][0]=1;
		for(int j=1;j<=i;j++){
			upd(f[i][j][0][0],f[i-1][j-1][0][0]);
			upd(f[i][j][1][0],f[i-1][j-1][0][1]);
			upd(f[i][j][0][1],add(f[i-1][j-1][0][0],f[i-1][j-1][1][0]));
			upd(f[i][j][1][1],add(f[i-1][j-1][0][1],f[i-1][j-1][1][1]));
			upd(f[i][j][0][0],add(f[i-1][j][0][0],f[i-1][j][1][0]));
			upd(f[i][j][1][0],add(f[i-1][j][0][1],f[i-1][j][1][1]));
		}
	}
	int ans=0;
	for(int i=m;i<=n;i++){
		ans=((i-m)&1?sub:add)(ans,(f[n][i][0][0]+f[n][i][1][0])*1ll*C[i][m]%mod*fac[n-i]%mod);
	}
	cout<<ans;
	return 0;
}
}
int main(){return asbt::main();}

F. Divan and Kostomuksha (easy version)

\(cnt_i\) 表示能被 \(i\) 整除的元素数量,\(dp_i\) 表示全局 \(\gcd\)\(i\) 时的最大权值。有方程 \(dp_i=\max_{i\mid j}\{dp_j+i\times(cnt_i-cnt_j)\}\)。时间复杂度 \(O(V\ln V)\)

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=5e6+5,m=5e6;
int n,cnt[maxn],dp[maxn];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=1,x;i<=n;i++){
		cin>>x;
		cnt[x]++;
	}
	for(int i=1;i<=m;i++){
		for(int j=i<<1;j<=m;j+=i){
			cnt[i]+=cnt[j];
		}
	}
	for(int i=m;i;i--){
		dp[i]=cnt[i]*i;
		for(int j=i<<1;j<=m;j+=i){
			dp[i]=max(dp[i],dp[j]+i*(cnt[i]-cnt[j]));
		}
	}
	cout<<dp[1];
	return 0;
}
}
signed main(){return asbt::main();}

G. Future Failure

分析这个游戏先手必胜的条件,发现如果此字符串有偶数种不同的排列,先手是必胜的。原因是,如果有一个删字母的方案使先手必胜,则先手必胜;如果没有,那么双方一定不停地重排,又因为有偶数种排列,所以还是先手必胜的。

然后分析奇数种排列的情况。首先,如果双方不停地重排,显然是先手不胜的,于是先手必然要删字符。一个字符串的排列个数为 \(\frac{n!}{\prod a_i!}\),注意到删一个字符 \(i\) 相当于给这个东西乘上 \(\frac{a_i}{n}\),不会影响奇偶性,于是双方都不停地删字符,于是 \(n\) 为奇数先手必胜,为偶数先手必败。

那么我们可以得到一个大致思路:如果 \(n\) 为奇数那么直接输出 \(k^n\),否则 DP 求出先手必胜,即排列数为偶数的方案数。然而这实际上不好求,正难则反。考虑分析 \(\frac{n!}{\prod a_i!}\) 的奇偶性,即分子与分母包含 \(2\) 的个数。对于 \(n!\),它包含的 \(2\) 的个数即为 \(\sum_{i\in\mathbb{N}^*}\lfloor\frac{n}{i}\rfloor\)。又因为 \(\lfloor\frac{a}{x}\rfloor+\lfloor\frac{b}{x}\rfloor\le\lfloor\frac{a+b}{x}\rfloor\),于是我们的要求即为:

\[\forall x\in\mathbb{N}^*,\sum\lfloor\frac{a_i}{x}\rfloor=\lfloor\frac{n}{x}\rfloor \]

考虑将 \(n\) 拆成二进制,对于最高位,显然要求恰有一个 \(a_i\) 该位为 \(1\)。进而,我们实际上要求的就是对于 \(n\) 中的每个 \(1\),都恰有一个 \(a_i\) 该位为 \(1\),而对于每个 \(0\) 则要求所有 \(a_i\) 该位都是 \(0\)。于是可以 DP 了。设 \(f_{x,S}\) 表示考虑前 \(x\)\(a\),还剩下 \(S\) 个字符分配的\(\prod\frac{1}{a_i!}\) 的和。故有转移:\(f_{x,S}\leftarrow f_{x-1,S+T}\times\frac{1}{T!}\)。不过枚举子集转移时间复杂度较大,可以钦定这个位置选择 \(\operatorname{lowbit}(S)\),最后再乘上排列数。代码中使用的是记忆化搜索,为了方便转移,传的参数实际上是 \(k-x\)\(n-S\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=3e5+5;
int n,m,mod,fac[maxn],inv[maxn],f[30][maxn];
il int qpow(int x,int y){
	int res=1;
	for(;y;y>>=1,x=x*1ll*x%mod){
		if(y&1){
			res=res*1ll*x%mod;
		}
	}
	return res;
}
il int lowbit(int x){
	return x&-x;
}
il int dfs(int x,int S){
//	cout<<x<<" "<<S<<"\n";
	if(~f[x][S]){
		return f[x][S];
	}
	if(!S){
		int res=1;
		for(int i=1;i<=x;i++){
			res=res*1ll*(m-i+1)%mod;
		}
		return f[x][S]=res;
	}
	int res=0,U=S-lowbit(S);
	for(int T=U;;T=(T-1)&U){
		res=(res+inv[S-T]*1ll*dfs(x+1,T))%mod;
		if(!T){
			break;
		}
	}
	return f[x][S]=res;
}
il void init(int n=3e5){
	fac[0]=1;
	for(int i=1;i<=n;i++){
		fac[i]=fac[i-1]*1ll*i%mod;
	}
	inv[n]=qpow(fac[n],mod-2);
	for(int i=n;i;i--){
		inv[i-1]=inv[i]*1ll*i%mod;
	}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m>>mod;
	init();
	memset(f,-1,sizeof(f));
//	puts("666");
//	cout<<dfs(0,n)*1ll*fac[n]%mod<<"\n";
	cout<<(qpow(m,n)+(n&1?0:mod-dfs(0,n)*1ll*fac[n]%mod))%mod;
	return 0;
}
}
int main(){return asbt::main();}

H. Tavas in Kansas

首先从 \(S\)\(T\) 各跑一遍最短路,并将距离离散化出来。注意到如果 \(A\) 要选 \(x\)\(y\),而 \(x\)\(A\) 又比 \(y\) 近,则 \(x\) 一定不会在 \(y\) 之后选。于是到现在这个问题已经和图论没有关系了。

考虑 DP。设 \(f_{0/1,i,j}\) 表示目前的先手是谁,还剩下 \(\operatorname{dis}(S)\ge i\land\operatorname{dis}(T)\ge j\) 的点时当前的先手的最大得分,记 \(cnt_{i,j}\) 为符合上述条件的点数,\(sum_{i,j}\) 为这些点的权值和。于是有方程:

\[f_{0,i,j}=\max_{cnt_{k,j}<cnt_{i,j}}\{sum_{i,j}-f_{1,k,j}\} \]

递推计算转移点,后缀最小值优化即可做到 \(O(n^2)\)

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
#define pii pair<int,int>
#define fir first
#define sec second
#define mp make_pair
#define pb push_back
#define lwrb lower_bound
using namespace std;
namespace asbt{
const int maxn=2e3+5;
int n,m,S,T,cs,ct,a[maxn],dis[2][maxn],lsh[maxn];
int cnt[maxn][maxn],sum[maxn][maxn];
int f[2][maxn][maxn],g[2][maxn][maxn],p[2][maxn][maxn];
bool vis[maxn];
vector<pii> e[maxn];
priority_queue<pii> q;
il void dijkstra(int u,int *dis,int &tot){
	memset(vis,0,sizeof(vis));
	dis[u]=0,q.push(mp(0,u));
	while(q.size()){
		int x=q.top().sec;
		q.pop();
		if(vis[x]){
			continue;
		}
		vis[x]=1;
		for(pii i:e[x]){
			int v=i.fir,w=i.sec;
			if(!vis[v]&&dis[v]>dis[x]+w){
				dis[v]=dis[x]+w;
				q.push(mp(-dis[v],v));
			}
		}
	}
	tot=0;
	for(int i=1;i<=n;i++){
		lsh[++tot]=dis[i];
	}
	sort(lsh+1,lsh+tot+1);
	tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	for(int i=1;i<=n;i++){
		dis[i]=lwrb(lsh+1,lsh+tot+1,dis[i])-lsh;
	}
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m>>S>>T;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1,u,v,w;i<=m;i++){
		cin>>u>>v>>w;
		e[u].pb(mp(v,w));
		e[v].pb(mp(u,w));
	}
	memset(dis,0x3f,sizeof(dis));
	dijkstra(S,dis[0],cs);
	dijkstra(T,dis[1],ct);
	for(int i=1;i<=n;i++){
		cnt[dis[0][i]][dis[1][i]]++;
		sum[dis[0][i]][dis[1][i]]+=a[i];
	}
	for(int i=cs;i;i--){
		for(int j=ct;j;j--){
			sum[i][j]+=sum[i+1][j]+sum[i][j+1]-sum[i+1][j+1];
			p[0][i][j]=min(i==cs?cs:p[0][i+1][j],j==ct?cs:p[0][i][j+1]);
			p[1][i][j]=min(i==cs?ct:p[1][i+1][j],j==ct?ct:p[1][i][j+1]);
			if(cnt[i][j]){
				p[0][i][j]=i,p[1][i][j]=j;
			}
			f[0][i][j]=sum[i][j]-g[1][p[0][i][j]+1][j];
			f[1][i][j]=sum[i][j]-g[0][i][p[1][i][j]+1];
			g[0][i][j]=min(f[0][i][j],g[0][i][j+1]);
			g[1][i][j]=min(f[1][i][j],g[1][i+1][j]);
		}
	}
	int ans=2*f[0][1][1]-sum[1][1];
//	cout<<ans<<"\n";
	cout<<(ans>0?"Break a heart":ans==0?"Flowers":"Cry");
	return 0;
}
}
signed main(){return asbt::main();}
posted @ 2025-07-14 21:04  zhangxy__hp  阅读(22)  评论(0)    收藏  举报