Atcoder DP Contest

好像都在说这套题很好,所以来刷一遍

太水的就只放代码了

尚未完工,先发一下

猫猫可爱捏 https://www.tldraw.com/ro/1g8hQBpWTkduIlFxT3c0P?d=v-1275.1523.960.968.page

A.Frog 1

#include<bits/stdc++.h>
using namespace std;
int n;
int h[100001];
int f[100001];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&h[i]);
	}
	memset(f,0x3f,sizeof f);
	f[1]=0;
	for(int i=1;i<=n;++i){
		if(i+1<=n) f[i+1]=min(f[i+1],f[i]+abs(h[i]-h[i+1]));
		if(i+1<=n) f[i+2]=min(f[i+2],f[i]+abs(h[i]-h[i+2]));
	}
	cout<<f[n]<<endl;
}

B.Frog 2

#include<bits/stdc++.h>
using namespace std;
int n,k;
int h[100001];
int f[100001];
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i){
		scanf("%d",&h[i]);
	}
	memset(f,0x3f,sizeof f);
	f[1]=0;
	for(int i=1;i<=n;++i){
		for(int j=i+1;j<=i+k;++j){
			if(j<=n) f[j]=min(f[j],f[i]+abs(h[i]-h[j]));
		}
	}
	cout<<f[n]<<endl;
}

C.Vacation

#include<bits/stdc++.h>
using namespace std;
int n;
int a[100001],b[100001],c[100001];
int f[100001][3];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d %d %d",&a[i],&b[i],&c[i]);
	}
	for(int i=1;i<=n;++i){
		f[i][0]=max(f[i-1][1],f[i-1][2])+a[i];
		f[i][1]=max(f[i-1][0],f[i-1][2])+b[i];
		f[i][2]=max(f[i-1][1],f[i-1][0])+c[i];
	}
	cout<<max({f[n][0],f[n][1],f[n][2]});
} 

D.Knapsack 1

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int w[101],v[101];
int f[2][100001];
int ans=0;
signed main(){
	scanf("%lld %lld",&n,&m);
	for(int i=1;i<=n;++i){
		scanf("%lld %lld",&w[i],&v[i]);
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			f[i&1][j]=f[(i-1)&1][j];
		}
		for(int j=w[i];j<=m;++j){
			f[i&1][j]=max(f[i&1][j],f[(i-1)&1][j-w[i]]+v[i]);
			ans=max(ans,f[i&1][j]);
		}
	}
	cout<<ans;
}

E.Knapsack 2

这题还挺有意思的,就是一个简单背包,但是背包容量在 \(10^9\) 级别,正常做无论空间还是时间都会寄掉

但是 \(N\le 100\),且单个物品权值在 \(10^4\) 以内,算了一下总和也只有 \(10^6\),应该是想让开这一维

所以设 \(f_{i,j}\) 表示考虑前 \(i\) 个物品,价值总和为 \(j\) 的花费容量最小值(这种类似的状态设计看的不少了,也是挺重要的一类 DP)

然后直接转移就行

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int w[101],v[101];
int f[2][100000];
int ans=0;
int sumv;
signed main(){
	scanf("%lld %lld",&n,&m);
	for(int i=1;i<=n;++i){
		scanf("%lld %lld",&w[i],&v[i]);
		sumv+=v[i];
	}
	memset(f,0x3f,sizeof f);
	f[0][0]=0;
	for(int i=1;i<=n;++i){
		for(int j=0;j<=sumv;++j){
			f[i&1][j]=f[(i-1)&1][j];
		}
		for(int j=v[i];j<=sumv;++j){
			f[i&1][j]=min(f[i&1][j],f[(i-1)&1][j-v[i]]+w[i]);
			if(f[i&1][j]<=m){
				ans=max(ans,j);
			}
		}
	}
	cout<<ans<<endl;
}

F.LCS

最长公共子序列板子,还是 \(n^2\) 级别的,一眼水题

这道题输出路径比较有启发意义,一开始不会写,单独开了一个 pair 数组记录前驱,虽然这样也能做,但是很麻烦

记录前驱的写法
#include<bits/stdc++.h>
using namespace std;
string s,t;
int f[3001][3001];
pair<int,int> path[3001][3001];
int ans=0;
int ansi,ansj;
int main(){
	cin>>s>>t;
	s="."+s;t="."+t;
	for(int i=1;i<=(int)s.length()-1;++i){
		for(int j=1;j<=(int)t.length()-1;++j){
			if(f[i][j-1]>f[i-1][j]) path[i][j]=path[i][j-1];
			else path[i][j]=path[i-1][j];
			f[i][j]=max(f[i][j-1],f[i-1][j]);
			if(f[i-1][j-1]+(s[i]==t[j])>f[i][j]){
				f[i][j]=f[i-1][j-1]+(s[i]==t[j]);
				path[i][j]={i-1,j-1};
			}
			if(ans<f[i][j]){
				ans=f[i][j];
				ansi=i;ansj=j;
			}
		}
	}
	string anss;
	int cnt=0;
	while(ansi and ansj){
		if(cnt==ans) break;
		pair<int,int>x=path[ansi][ansj];
		ansi=x.first;ansj=x.second;
		anss.push_back(s[ansi+1]);
		cnt++;
	}
	reverse(anss.begin(),anss.end());
	cout<<anss<<endl;
}

去题解区翻了一下,其实输出路径可以从 DP 数组里倒着往回找,比如你要找最后一位的答案,只需要从 \(f_{i,j}=f_{n,m}\)\((i,j)\) 里面找一个 \(s_i=t_j\) 的位置,这一点开个双指针就能做

否则,如果 \(f_{i,j}\)\(f_{i-1,j}/f_{i,j-1}\) 一样,意味着这一位没啥大用,可以直接跳过去

#include<bits/stdc++.h>
using namespace std;
string s,t;
int f[3001][3001];
int ans=0;
int ansi,ansj;
int main(){
	cin>>s>>t;
	s="."+s;t="."+t;
	for(int i=1;i<=(int)s.length()-1;++i){
		for(int j=1;j<=(int)t.length()-1;++j){
			f[i][j]=max(f[i][j-1],f[i-1][j]);
			f[i][j]=max(f[i][j],f[i-1][j-1]+(s[i]==t[j]));
		}
	}
	string anss;
	int i=(int)s.length()-1,j=(int)t.length()-1;
	while(f[i][j]!=0){
		if(s[i]==t[j]){
			anss.push_back(s[i]);
			i--;j--;
		}
		else{
			if(f[i][j]==f[i-1][j]) i--;
			else j--;
		}
	}
	reverse(anss.begin(),anss.end());
	cout<<anss<<endl;
}

G.Longest Path

拓扑一遍即可

#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int>e[100001];
int f[100001];
int inde[100001];
bool vis[100001];
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;++i){
		int x,y;scanf("%d %d",&x,&y);
		e[x].push_back(y);
		inde[y]++;
	}
	queue<int>q;
	for(int i=1;i<=n;++i){
		if(inde[i]==0){
			q.push(i);
		}
	}
	int ans=0;
	while(!q.empty()){
		int u=q.front();q.pop();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i:e[u]){
			if(!vis[i]){
				inde[i]--;
				f[i]=max(f[i],f[u]+1);
				ans=max(ans,f[i]);
				if(inde[i]==0){
					q.push(i);
				}
			}
		}
	}
	cout<<ans;
} 

H.Grid 1

#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n,m;
char mp[1001][1001];
int f[1001][1001];
char getchar(const vector<char>&A){
	char ch=getchar();
	while(1){
		for(char i:A) if(i==ch) return i;
		ch=getchar();
	}
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			mp[i][j]=getchar({'.','#'});
		}
	}
	f[1][1]=1;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			if((i!=1 or j!=1) and mp[i][j]!='#'){
				if(i!=1 and mp[i-1][j]!='#') f[i][j]=(f[i][j]+f[i-1][j])%p;
				if(j!=1 and mp[i][j-1]!='#') f[i][j]=(f[i][j]+f[i][j-1])%p;
			}
		}
	}
	cout<<f[n][m];
} 

I.Coins

还是看看远处的记搜把家人们

#include<bits/stdc++.h>
using namespace std;
int n;
long double p[3000];
long double f[3001][3001];
long double dfs(int now,int poscnt){
	if(now>n){
		int negcnt=n-poscnt;
		if(poscnt>negcnt) return 1;
		return 0;
	}
	if(f[now][poscnt]>=0) return f[now][poscnt];
	return f[now][poscnt]=dfs(now+1,poscnt+1)*p[now]+dfs(now+1,poscnt)*(1-p[now]);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		for(int j=0;j<=n;++j){
			f[i][j]=-1;
		}
		scanf("%Lf",&p[i]);
	}
	printf("%.20Lf",dfs(1,0));
} 

J.Sushi

苏轼

不愧是苏轼,给我整不会了

记搜,首先你可以注意到,剩余sushi相同的盘子是等价的

又因为这个题值域只有四种情况,因此可以直接考虑开桶

设计 \(f_{i,j,k}\) 分别表示剩余 \(1,2,3\) 个sushi的盘子数量,剩余 \(0\) 个的情况可以通过 \(n-i-j-k\) 算出

然后直接转移就好了,唯一的问题是怎么考虑不拿的情况

显然你不能直接搜下去,而是应该考虑这些不拿的次数在平均情况下会造成多少贡献(因为你不管在何种状态下,最终都要靠下面三种情况来转移状态,也就是不管咋整都得加上这些期望,你可以直接把不拿的作为一个基础概率加上)

还是根据经典结论,如果一件事发生的概率为 \(m\),你需要靠平均 \(\frac{1}{m}\) 次才能使其发生

这里显然要求的就是 “选到一个还有 sushi 的盘子” 的发生概率,显然是 \(\frac{i+j+k}{n}\),取个倒数就是基础概率

#include<bits/stdc++.h>
using namespace std;
int n;
int a[301];
int cnt[4];
double f[301][301][301];
double dfs(int cnt1,int cnt2,int cnt3){
	int tot=cnt1+cnt2+cnt3;
	if(tot==0) return 0;
	if(f[cnt1][cnt2][cnt3]>=0) return f[cnt1][cnt2][cnt3];
	double res=n*1.0/tot;
	if(cnt1) res+=dfs(cnt1-1,cnt2,cnt3)*cnt1/tot;
	if(cnt2) res+=dfs(cnt1+1,cnt2-1,cnt3)*cnt2/tot;
	if(cnt3) res+=dfs(cnt1,cnt2+1,cnt3-1)*cnt3/tot;
	return f[cnt1][cnt2][cnt3]=res;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		cnt[a[i]]++;
	}
	for(int i=0;i<=n;++i){
		for(int j=0;j<=n;++j){
			for(int k=0;k<=n;++k){
				f[i][j][k]=-1;
			}
		}
	} 
	printf("%.20lf",dfs(cnt[1],cnt[2],cnt[3]));
}

K.Stones

沾点小博弈,挺有意思的

\(f_i\) 表示如果初态有 \(i\) 个数的时候先手是否有必胜策略

显然,\(f_0\) 时先手无论如何不会赢

然后是转移,可以考虑枚举集合中的数,如果存在 \(f_{i-a_j}\) 不是先手必胜的,那么只需要先手一步走到 \(i-a_j\) 然后取得胜利,因此 \(f_i\) 就是先手必胜的

#include<bits/stdc++.h>
using namespace std;
int n,k;
int a[100001];
int f[100001];
signed main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=k;++i){
		for(int j=1;j<=n;++j){
			if(i-a[j]>=0 and !f[i-a[j]]){
				f[i]=true;
			}
		}
	}
	cout<<(f[k]?"First":"Second");
} 

L.Deque

感觉这两道博弈论题都很好玩

考虑直接模拟博弈,注意到任何时刻剩余的一定都是一个连续的区间,设 \(f_{i,j}\) 表示剩了 \([i,j]\) 这个区间的最大差值

如果当前是先手行动,一定倾向于增大这个差值,并且行动后会对差值造成正贡献

后手行动,一定倾向于减小这个差值,并且行动后会对差值造成负贡献

按照这个思路转移即可

没处理边界,小朋友不要学

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int a[3001];
int f[3001][3001];
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
	}
	for(int len=1;len<=n;++len){
		for(int i=1;i+len-1<=n;++i){
			int j=i+len-1;
			if((n&1)==(len&1)) f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
			else f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
		}
	}
	cout<<f[1][n]<<endl;
}

题解区还有一种做法也很有意思,在这里贴一下

考虑数列中存在 \(3\) 个连续的数 \(a_i,a_{i+1},a_{i+2}\) 满足 \(a_i \le a_{i+1}\) 并且 \(a_{i+1} \ge a_{i+2}\),此时一方任取两边的数,则另一方取中间的数最优,所以,这 \(3\) 个数可以等效合并为 \(1\) 个大小为 \(a_i+a_{i+2}-a_{i+1}\) 的数。

将数列中满足条件的数等效合并完后,数列一定满足先递减后递增,此时从两端贪心即可。

M.Candies

前缀和优化板子题

设计 \(f_{i,j}\) 表示考虑到第 \(i\) 个时已经花费了 \(j\) 的容量

\(n^3\) 的 DP 是很好想的

$n^3$
#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n,k;
int a[101];
int f[101][100001];
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
	}
	f[0][0]=1;
	for(int i=1;i<=n;++i){
		for(int j=0;j<=k;++j){
			for(int l=max(0,j-a[i]);l<=j;++l){
				f[i][j]=(f[i][j]+f[i-1][l])%p;
			}
		}
	}
	cout<<f[n][k]<<endl;
} 

可以发现内层是连续段求和,可以直接上前缀和优化

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int n,k;
int a[101];
int f[101][100001];
int sum[101][100001];
signed main(){
	scanf("%lld %lld",&n,&k);
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
	}
	f[0][0]=1;
	sum[0][0]=1;
	for(int i=1;i<=n;++i){
		sum[i-1][0]=f[i-1][0];
		for(int j=1;j<=k;++j){
			sum[i-1][j]=sum[i-1][j-1]+f[i-1][j];
		}
		for(int j=0;j<=k;++j){
			f[i][j]=(sum[i-1][j]-(j-a[i]<=0?0:sum[i-1][j-a[i]-1])+p)%p;
		}
	}
	cout<<f[n][k]<<endl;
} 

N.Slimes

石子合并

一开始没想到区间 DP,后来想到这玩意是石子合并,然后才想到的区间 DP

区间 DP 的标准转移

#include<bits/stdc++.h>
using namespace std;
#define int long long 
int n;
int a[401];
int f[401][401];
int sum[401];
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
		sum[i]=sum[i-1]+a[i];
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			f[i][j]=0x7fffffffffff;
		}
	}
	for(int i=1;i<=n;++i) f[i][i]=0;
	for(int len=2;len<=n;++len){
		for(int i=1;i+len-1<=n;++i){
			int j=i+len-1;
			for(int k=i;k<j;++k){
				f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
			}
		}
	}
	cout<<f[1][n]<<endl;
}

O.Matching

一开始打了个记忆化搜索,用的 map 带了个 \(\log\)

对于这道题,可以注意到的是,如果前面选择的决策集合是相等的,那么最后的答案就是相等的,可以基于这一点记搜

那么显然你需要设计一个哈希,满足可以集合判等

由于 \(n\le 21\),因此考虑用状压代替这里的哈希

又因为 \(n\times 2^n\) 的空间复杂度可以接受,可以开静态数组卡掉 map 的 \(\log\)

一个小优化:记忆化数组初值赋值成 \(-1\) 而不是 \(0\),因为有很多答案是 \(0\) 的搜索区间,你直接用 \(0\) 初始化会造成大量的冗余

#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n;
bool t[23][23];
bool vis[23];
int f[21][1<<21];
int dfs(int now,int _hash){
	if(now>n) return 1;
	if(f[now-1][_hash]!=-1) return f[now-1][_hash];
	int res=0;
	for(int i=1;i<=n;++i){
		if(!vis[i] and t[now][i]){
			vis[i]=true;
			res=(res+dfs(now+1,_hash|(1ll<<(i-1))))%p;
			vis[i]=false;
		}
	}
	return f[now-1][_hash]=res;
}
int main(){
	scanf("%d",&n);
	memset(f,-1,sizeof f);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			scanf("%d",&t[i][j]);
		}
	}
	cout<<dfs(1,0);
}

P.Independent Set

树形 DP 板子

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int n;
vector<int>e[100001];
int f[100001][2];
void dfs(int now,int last){
	f[now][1]=1;
	f[now][0]=1;
	for(int i:e[now]){
		if(i!=last){
			dfs(i,now);
			f[now][1]=f[now][1]*(f[i][0]+f[i][1])%p;
			f[now][0]=f[now][0]*f[i][1]%p;
		}
	}
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n-1;++i){
		int x,y;scanf("%lld %lld",&x,&y);
		e[x].push_back(y);
		e[y].push_back(x);
	}
	dfs(1,0);
	cout<<(f[1][0]+f[1][1])%p;
}

Q.Flowers

\(f_i\) 表示考虑到第 \(i\) 位的最大权值

\(n^2\) 很好写

$n^2$
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int n;
int h[200001];
int a[200001];
int f[200001];
int ans=0;
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&h[i]);
	}
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
	}
	for(int i=1;i<=n;++i){
		f[i]=a[i];
		ans=max(ans,a[i]);
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<i;++j){
			if(h[i]>h[j]){
				f[i]=max(f[i],f[j]+a[i]);
				ans=max(ans,f[i]);
			}
		}
	}
	cout<<ans;
}

注意到内层可以线段树处理

良心出题人甚至已经帮你离散化好了

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int n;
int h[200001];
int a[200001];
int f[200001];
int ans=0;
namespace stree{
	struct tree{
		int l,r;
		int maxn;
	}t[200001*4];
	#define tol (id*2)
	#define tor (id*2+1)
	#define mid(l,r) mid=((l)+(r))/2
	void build(int id,int l,int r){
		t[id].l=l;t[id].r=r;
		if(l==r){
			t[id].maxn=0;
			return;
		}
		int mid(l,r);
		build(tol,l,mid);
		build(tor,mid+1,r);
		t[id].maxn=max(t[tol].maxn,t[tor].maxn);
	}
	void change(int id,int pos,int val){
		if(t[id].l==t[id].r){
			t[id].maxn=val;
			return;
		}
		if(pos<=t[tol].r) change(tol,pos,val);
		else change(tor,pos,val);
		t[id].maxn=max(t[tol].maxn,t[tor].maxn);
	}
	int ask(int id,int l,int r){
		if(l<=t[id].l and t[id].r<=r){
			return t[id].maxn;
		}
		if(r<=t[tol].r) return ask(tol,l,r);
		else if(l>=t[tor].l) return ask(tor,l,r);
		return max(ask(tol,l,t[tol].r),ask(tor,t[tor].l,r));
	}
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&h[i]);
	}
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
	}
	stree::build(1,1,n);
	for(int i=1;i<=n;++i){
		f[i]=stree::ask(1,1,h[i])+a[i];
		ans=max(ans,f[i]);
		stree::change(1,h[i],f[i]); 
	}
	cout<<ans;
}

R.Walk

这么会出题

\(f_{i,j,k}\) 是从 \(i\)\(j\) 的路径长度为 \(t\) 的条数

\(f_{i,j,k}=\sum_{t}(f_{i,t,k-1}\times f_{t,j,1})\)

这么算刚好不重不漏

这个东西长得极其像矩阵乘法,你想象成 \(f_{i,j,k}\)\(f^k_{i,j}\),那就是完完全全的矩阵乘法的式子

并且 \(f_{t,j,1}\) 就是输入的初始矩阵中的元素

因此直接对输入矩阵做快速幂即可

#include<bits/stdc++.h>
using namespace std;
#include"include/hdk/matrix.h"
#define int long long
hdk::matrix<int,50> a;
int n,k;
signed main(){
	cin>>n>>k;
	a.input(n,n);
	a.setmod(1000000007);
	a|=k;
	cout<<a.sum();
} 

S.Digit Sum

数位 DP 板子题

然而我从来没会过数位 DP

借这个题复习了已经生疏的数位 DP

在这就不写了,待会写 DP 专题里

记得最后减一要加成正数

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
string k;
int p;
const int mod=1e9+7;
int f[10001][101][2];
signed dfs(int now,int r,bool limit){
	if(now>=n) return r==0;
	if(f[now][r][limit]!=-1) return f[now][r][limit];
	int res=0;
	if(limit){
		for(int i=0;i<=k[now]-'0';++i){
			res=(res+dfs(now+1,(r+i)%p,i==k[now]-'0'))%mod;
		}
	}
	else{
		for(int i=0;i<=9;++i){
			res=(res+dfs(now+1,(r+i)%p,false))%mod;
		}
	}
	return f[now][r][limit]=res;
}
signed main(){
	memset(f,-1,sizeof f);
	cin>>k>>p;n=(int)k.length();
	cout<<(dfs(0,0,true)-1)%mod;
} 

T.Permutation

长得有点像地精部落

这种大小关系的题,一般都是把相对大小拿来当 DP 第二维的,这也是一种挺重要的 trick

然后这种相对大小的 DP,该枚举就大力枚举,因为你不知道相对排名,所以什么都有可能是合法的

这题的 \(n^3\) 可以通过这种方法枚举出来

$n^3$
#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n;
string x;
int f[3001][3001];
int main(){
	cin>>n>>x;
	f[1][1]=1; 
	for(int i=2;i<=n;++i){
		for(int j=1;j<=i;++j){
			if(x[i-2]=='<'){
				for(int k=1;k<=j-1;++k){
					f[i][j]=(f[i][j]+f[i-1][k])%p;
				}
			}
			else{
				for(int k=j;k<=i-1;++k){
					f[i][j]=(f[i][j]+f[i-1][k])%p;
				}
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;++i){
		ans=(ans+f[n][i])%p;
	}
	cout<<ans;
}

这种题需要注意的就是边界,注意到在前 \(i-1\) 个数中排名为 \(j\) 的数,在插入排名为 \(j\) 的数时一定会被顶到 \(j+1\) 位置,而不是 \(j-1\) 位置,因此应该将其放在后面

做出 \(n^3\) 以后前缀和砍掉里层即可

#include<bits/stdc++.h>
using namespace std;
const int p=1e9+7;
int n;
string x;
int f[3001][3001];
int sum[3001][3001];
int main(){
	cin>>n>>x;
	f[1][1]=1; 
	sum[1][1]=1;
	for(int i=2;i<=n;++i){
		for(int j=1;j<=i;++j){
			if(x[i-2]=='<'){
				f[i][j]=sum[i-1][j-1];
			}
			else{
				f[i][j]=(sum[i-1][i-1]-sum[i-1][j-1]+p)%p;
			}
			sum[i][j]=(sum[i][j-1]+f[i][j])%p;
		}
	}
	int ans=0;
	for(int i=1;i<=n;++i){
		ans=(ans+f[n][i])%p;
	}
	cout<<ans;
}

U.Grouping

好玄幻的题

\(n\le 16\),考虑直接状压

有一个很玄幻的 DP,是设 \(f_i\) 为只考虑 \(i\)(二进制状压)集合内的数的最大权值

然后考虑怎么能转移到这个状态来

首先可以确定的是应该去枚举 \(i\) 的全部子集 \(j\)

为了避免处理分组的问题,钦定这些新增的点都在同一组里,那么有

\[f_i=\max_j(f_j+cost_{i\operatorname{xor}j}) \]

这里需要保证 \(j\)\(i\) 的子集

那个问题就变为了这个 \(cost\) 怎么求,其实 \(cost_i\) 就是在二进制状态下集合 \(i\) 内所有数互相造成的贡献,这个可以直接 \(2^nn^2\) 算出来,复杂度可以接受

但是枚举子集这一块复杂度是 \((2^n)^2\),复杂度不能接受

考虑优化,在里层仅枚举 \(i\) 的子集

一个比较神奇的方法:

for(int j=i;j;j=(j-1)&i)

仅限于隐约感觉这玩意是对的,首先这东西只包含减法和与运算,一定是单调递减的,其次它与的是 \(i\),一定是 \(i\) 的子集,但是不明白这东西为什么不会少情况,看了一下也没有题解讲这个

复杂度是 \(i\) 的子集个数,平均下来是 \(3^n\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int a[17][17];
int f[1<<17];
int v[1<<17];
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			scanf("%lld",&a[i][j]);
		}
	}
	for(int i=0;i<=((1<<n)-1);++i){
		for(int j=0;j<=n-1;++j){
			for(int k=0;k<j;++k){
				if(((1<<j)&i) and ((1<<k)&i)){
					v[i]+=a[j+1][k+1];
				}
			}
		}
		f[i]=v[i];
	}
	for(int i=0;i<=((1<<n)-1);++i){
		for(int j=i;j;j=(j-1)&i){
			f[i]=max(f[i],f[j]+v[i^j]);
		}
	}
	cout<<f[(1<<n)-1];
}

V.Subtree

W.Intervals

设计 \(f_{i,j}\) 表示考虑到第 \(i\) 个数,上一个 \(1\) 的位置在 \(j\) 得到情况

这里考虑一个经典 trick,为了避免区间重复贡献,钦定每个区间在右端点处取得贡献(由于区间在范围内选取,这必定不会影响最终的贡献值)

因此,考虑一个 \(f_{i,j}(i\neq j)\),当其从 \(f_{i-1,j}\) 转移过来的时候,由于 \(1\) 的状态不变,增加的只是右端点为 \(i\) 的贡献,此外还要要求在该区间内含有 \(1\),最优化地想,也就是最近的一个 \(1\) 包含在区间内,转移方程为 \(f_{i,j}=f_{i-1,j}+\sum\limits_{r_k=i\operatorname{and}l_k\le j}a_k\)

考虑 \(f_{i,i}\),由于我们在 \(i\) 新放了一个 \(1\),因此这个状态可以由任意一个 \(j\lt i\)\(f_{i-1,j}\) 转移得到,额外的区间贡献计算方式不变,转移方程为 \(f_{i,j}=(\max\limits_{j}f_{i-1,j})+\sum\limits_{r_k=i\operatorname{and}l_k\le j}a_k\)

滚动数组优化的暴力
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int f[2][1000001];
struct gz{
    int l,r,a;
}a[1000001];
int ans=-0x7fffffffffffffff;
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;++i){
        cin>>a[i].l>>a[i].r>>a[i].a;
    }
    for(int i=1;i<=n;++i){
        for(int j=1;j<=i-1;++j){
            f[i&1][j]=f[(i-1)&1][j];
            for(int k=1;k<=m;++k){
                if(a[k].r==i and a[k].l<=j){
                    f[i&1][j]+=a[k].a;
                }
            }
        }
        for(int j=1;j<=i-1;++j){
            f[i&1][i]=max(f[i&1][i],f[(i-1)&1][j]);
        }
        for(int j=1;j<=m;++j){
            if(a[j].r==i and a[j].l<=i){
                f[i&1][i]+=a[j].a;
            }
        }
    }
    for(int i=0;i<=n;++i){
        ans=max(ans,f[n&1][i]);
    }
    cout<<ans;
}

事实上也可以优化到一维,优化之后少了从 \(f_{i-1,j}\)\(f_{i,j}\) 的转移,更简洁了

优化到一维的暴力
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int f[1000001];
struct gz{
    int l,r,a;
}a[1000001];
int ans=0;
signed main(){
    cin>>n>>m;
    if(n>=5000) return 0;
    for(int i=1;i<=m;++i){
        cin>>a[i].l>>a[i].r>>a[i].a;
    }
    memset(f,-0x3f,sizeof f);
    f[0]=0;
    for(int i=1;i<=n;++i){
        for(int j=0;j<=i-1;++j){
            int t=f[j];
            for(int k=1;k<=m;++k){
                if(j<a[k].l and a[k].l<=i and a[k].r>=i){
                    t+=a[k].a;
                }
            }
            f[i]=max(f[i],t);
        }
        ans=max(ans,f[i]);
    }
    cout<<ans;
}

考虑我们的转移式 \(\sum\limits_{r_k=i\operatorname{and}l_k\le j}a_k\),可以发现这个转移式修改的位置是连续的,满足 \(j\in[l_k,i]\)\(j\) 位置均可被这个 \(k\) 修改更新

由于我们压成一维之后已经把从 \(f_{i-1,j}\)\(f_{i,j}\) 的转移省了,因此我们只需要考虑求那个形如 \(\max\) 的式子

区间修改,区间查最值,可以将 DP 数组放在线段树上处理

值得注意的是这个区间查最值的时候,由于 \(i\) 后面的值尚未更新,实际上约等于全局查最值,所以你其实根本没必要写一个查最值的函数

注意为了避免负贡献成最优解,查询的时候要对 \(0\)\(\max\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,ans;
struct qj{
    int l,r,a;
};
vector<qj>a[400001];
namespace stree{
    struct tree{
        int maxn;
        int lazy;
    }t[400001*4];
    #define tol (id*2)
    #define tor (id*2+1)
    void pushdown(int id){
        if(t[id].lazy){
            t[tol].maxn+=t[id].lazy;
            t[tol].lazy+=t[id].lazy;
            t[tor].maxn+=t[id].lazy;
            t[tor].lazy+=t[id].lazy;
            t[id].lazy=0;
        }
    }
    void change(int id,int l,int r,int L,int R,int val){
        if(L<=l and r<=R){
            t[id].maxn+=val;
            t[id].lazy+=val;
            return;
        }
        int mid=(l+r)/2;
        pushdown(id);
        if(R<=mid) change(tol,l,mid,L,R,val);
        else if(L>=mid+1) change(tor,mid+1,r,L,R,val);
        else{
            change(tol,l,mid,L,mid,val);
            change(tor,mid+1,r,mid+1,R,val);
        }
        t[id].maxn=max(t[tol].maxn,t[tor].maxn);
    }
    int ask(){
        return max(0ll,t[1].maxn);
    }
}
signed main(){
    ios::sync_with_stdio(false);
    cin>>n>>m;
	for(int i=1;i<=m;i++){
        int il,ir,ia;
        cin>>il>>ir>>ia;
        a[ir].push_back({il,ir,ia});
    }
    for(int i=1;i<=n;++i){
        stree::change(1,1,n,i,i,stree::ask());
        for(qj j:a[i]){
            stree::change(1,1,n,j.l,i,j.a);
        }
    }
    cout<<stree::ask();
}

X.Tower

首先看出这似乎是个背包,但是正常做背包显然做不出来(有后效性,先遍历过的物品有可能后选比较优),如果能找到某些性质使得先遍历的物品一定比后遍历的物品先选更优,那么这道题就能转化成背包问题

考虑两个物品 \(i,j\),设 \(i\) 放在 \(j\) 上面,那么 \(j\) 上面还能放的物品总质量为 \(s_j-w_i\),同理,如果 \(j\) 在上面,这个值为 \(s_i-w_j\)

由于 \(i,j\) 如何放,总重量都是一样的,对下面没有影响,因此 \(i\) 放在 \(j\) 上面更优,当且仅当 \(s_j-w_i\gt s_i-w_j\)

移项,\(s_i+w_i\lt s_j+w_j\),以此为比较依据直接背包即可

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
struct tower{
    int w,s,v;
    bool operator <(const tower&a)const{
        return w+s<a.w+a.s;
    }
}a[10001];
int f[20001];
int ans=0;
signed main(){
    ios::sync_with_stdio(false);
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>a[i].w>>a[i].s>>a[i].v;
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;++i){
        for(int j=a[i].s;j>=0;--j){
            f[j+a[i].w]=max(f[j+a[i].w],f[j]+a[i].v);
        }
    }
    cout<<ans;
}
posted @ 2024-10-30 11:25  HaneDaniko  阅读(111)  评论(19编辑  收藏  举报