关于dp

发扬多头精神,质疑dp,理解dp,成为dp!

由浅入深

ATcoder Dp 普及~提高的版子记录

Link

  1. A - Frog 1
    和走楼梯很像的最优式dp
    发现每一个石头只与前两个有关,那么选取前两个的dp值于高度差的和作动态调整即可
    转移显然\(O(1)\)
int n=read(),h[M],f[M];
signed main(){
	rep(i,1,n,1){
		h[i]=read();
		if(i>1) f[i]=f[i-1]+abs(h[i]-h[i-1]);
		if(i>2) f[i]=min(f[i],f[i-2]+abs(h[i]-h[i-2]));
	}wr(f[n]),pr(10);
	return 0;
}
  1. B - Frog 2
    这种题其实最优的是单调队列,也就是P1725 琪露诺
    但是一般也会放数据结构过去,考虑区间查最大,可以做到\(\log n\)的转移
    转移\(O(1)\to\)单调队列,\(O(\log n)\to\) ds
int n=read(),k=read(),h[M],f[M];
signed main(){
	rep(i,1,n,1){
		h[i]=read();
		f[i]=((i==1)?0:INF);
		rep(fr,1,k,1) if(i>fr)
			f[i]=min(f[i],f[i-fr]+abs(h[i]-h[i-fr]));
	}
	wr(f[n]),pr(10);
	return 0;
}
  1. C - Vacation
    基础的动态规划概念
    记录每天的三样事情最优解,转移从上一天不同的最优状态转移,显然\(O(1)\)
int n=read(),dp[M][3],a,b,c;
signed main(){
	rep(i,1,n,1){
		a=read(),b=read(),c=read();
		dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a;
		dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b;
		dp[i][2]=max(dp[i-1][1],dp[i-1][0])+c;
	}
	wr(max(dp[n][0],max(dp[n][1],dp[n][2])));
	
	return 0;
}
  1. D - Knapsack 1
    背包的版子题,背包的动规用一句话概括就是\(\to\) 该重量的最大价值
    对于所有物品去枚举可能重量,\(n\times v\) 的复杂度,\(v\) 是最大重量
int n=read(),W=read(),f[M],w,v,ans;
signed main(){
	me(f,-1);f[0]=0;
	rep(i,1,n,1){
		w=read(),v=read();
		dep(j,W,w,1) if(f[j-w]==-1) continue;
		else f[j]=max(f[j],f[j-w]+v),ans=max(ans,f[j]);
	}
	wr(ans);
	return 0;
}
  1. E - Knapsack 2
    转换背包的动规思想,\(\to\) 得到该价值的最小重量
    那么只要重量比我小,我就可以取这个价值,这样复杂度是 \(n\times w\)的,w是最大价值
int n=read(),W=read(),w,v,f[M],ans=0;
signed main(){
	me(f,0x3f);f[0]=0;
	rep(i,1,n,1){
		w=read(),v=read();
		dep(j,100000,v,1){
			if(f[j-v]>INF) continue;
			else f[j]=min(f[j-v]+w,f[j]);
			if(f[j]<=W) ans=max(ans,j);
		}
	}
	wr(ans),pr(10);	
	return 0;
}
  1. F - LCS
    LIS问题,考虑对字符串dp,查了很多资料,在算法竞赛中,LIS问题最多只能做到\(n\times m\)
    其中 n,m是两个串的长度,这个dp方程很版,要注意转移顺序
    其实也就是在i,j未完成匹配的最大匹配数

\[f_{i,j}=\begin{cases}f_{i-1,j-1}+1&s[i]=t[i]\\\max{f_{i,j-1},f_{i-1,j}}&s[i]\ne t[i]\end{cases} \]

string s,t,qqt;
int dp[3005][3005],f[3005][3005];
stack<char>q;
inline void dfs(int i,int j){
	if(i<1||j<1||dp[i][j]==0) return void();
	if(s[i]==t[j]) q.push(s[i]);
	if(f[i][j]==1) dfs(i-1,j);
	if(f[i][j]==2) dfs(i,j-1);
	if(f[i][j]==3) dfs(i-1,j-1);
}
signed main(){
	s+='@';t+='@';
	cin>>qqt;s+=qqt;
	cin>>qqt;t+=qqt;
	int n=(int)s.size()-1,m=(int)t.size()-1;
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
		if(s[i]==t[j]) dp[i][j]=dp[i-1][j-1]+1,f[i][j]=3;
		else dp[i][j]=max(dp[i-1][j],dp[i][j-1]),
			f[i][j]=(dp[i][j]==dp[i-1][j]?1:2);
	dfs(n,m);
	while(!q.empty()) cout<<q.top(),q.pop();
	return 0;
}
  1. G - Longest Path
    这个东西是有向图+没有环
    那么拓扑序跑一遍,更新最长链即可
int n=read(),m=read(),f[M],in[M],ans;
vector<int>Ed[M];queue<int>q;
signed main(){
	rep(i,1,m,1){
		int u=read(),v=read();
		Ed[u].pb(v);in[v]++;
	}
	rep(i,1,n,1)if(in[i]==0) q.push(i);
	while(!q.empty()){
		int u=q.front();q.pop();
		for(auto v:Ed[u]){
			f[v]=max(f[v],f[u]+1);in[v]--;
			ans=max(ans,f[v]);
			if(in[v]==0) q.push(v);
		}
	}
	wr(ans),pr(10);
	return 0;
}
  1. H - Grid 1
    这个一样的,\(f_{i,j}=f_{i,j-1}+f_{i-1,j}\)
    由于只能向下向右,所以到这个点的路线数量就是来的总路线数量
int H=read(),W=read(),dp[1005][1005],mp[1005][1005],vis[1005][1005];
queue<pii>q;inline void ad(int &x,int y,int z){
	x+=y;x+=z;if(x>mod) x=x-mod;
}
signed main(){
	rep(i,1,H,1) rep(j,1,W,1){
		char ch;cin>>ch;mp[i][j]=(ch=='#'?0:1);
	}
	q.push(mk(1,1));dp[1][1]=1;
	while(!q.empty()){
		auto [i,j]=q.front();q.pop();
		ad(dp[i][j],dp[i-1][j],dp[i][j-1]);
		if(mp[i+1][j]==1&&!vis[i+1][j]) q.push(mk(i+1,j)),vis[i+1][j]=1;
		if(mp[i][j+1]==1&&!vis[i][j+1]) q.push(mk(i,j+1)),vis[i][j+1]=1;
	}wr(dp[H][W]),pr(10);
	
	return 0;
}
  1. I - Coins
    简单概率dp入门题,考虑每次掷硬币后正面朝上 \(k\) 个的概率
    方程就非常简单\(f_{i}=f_{i-1}\times(p)+f{i}\times(1-p)\)
    每次这样\(n^2\)遍历一遍即可
int n=read();lb dp[3005],x,ans;
signed main(){
	dp[0]=1;
	rep(i,1,n,1){
		cin>>x;
		dep(j,i,1,1) dp[j]=dp[j-1]*x+dp[j]*(1-x);
		dp[0]=dp[0]*(1-x);
	}
	rep(i,1,n,1) if(i>n-i) ans+=dp[i];
	
	printf("%.15Lf",ans);
	return 0;
}
  1. J - Sushi
    期望dp入门题
    期望:概率乘上答案值的总和
    解释一下吧
  • 概率(Probability)
    描述某个事件发生的可能性大小,取值在\([0,1]\)
    使用\(P(X)\)表示事件\(X\)发生的可能性
    比如抛一枚质地均匀的硬币,正反两面出现的可能性都是 \(\frac{1}{2}=0.5\)
    使用概率的说法就是 \(P(抛硬币抛出正面)=0.5\)
    扔一枚六点骰子,那么就可以说 \(P(扔出1点)=\frac{1}{6}\)
  • 期望(Expectation)
    期望是随机变量取值的加权平均,权重是取该值的概率。
    对于离散随机变量 \(X\) ,它的期望公式为 $E[X]=\sum_{k}k\times P(X=k) $
    什么叫离散随机变量?
    就是这个事件可能出现的结果表达
    比如我们用 \(X=1\) 表示抛硬币抛出了正面 ,\(X=0\) 表示抛出反面
    \(E[抛硬币]=0\times P(抛出反面)+1 \times P(抛出正面)\)
    由于正反面概率相同,那么可以得出\(E[抛硬币]=0\times 0.5+1\times 0.5=0.5\)
    也就是说,一直抛硬币,正面为1,反面为0,最后加起来的平均数基本是0.5
    同理,我们用1,2,3,4,5,6表示掷骰子掷出六个数值
    \(E[掷骰子]=1\times \frac{1}{6}+2\times \frac{1}{6}+3\times \frac{1}{6}+4\times \frac{1}{6}+5\times \frac{1}{6}+6\times \frac{1}{6}=3.5\)
    也就是说,一直掷骰子,最后骰子上的值加起来的平均数基本是3.5
    期望反映的是长期重复试验中随机变量的平均结果。
  • 区别
    期望求取要用到概率,而求概率不需要期望
    概率是具体结果的可能性,期望是长期平均值
  • 结论
    期望是加权平均数,把结果乘上概率加起来就是期望了
  • 例子的具体深入
    比如说这道题,也就是问你平均掷多少次骰子才可以把寿司吃完
    那么考虑开一个三维的\(f_{i,j,k}\)数组,表示还有 \(i\) 个1分盘, \(j\) 个二分盘, \(k\)个 三分盘的期望步数
    记住期望dp的重要建模套路,dp递推!
    将当前状态到目标结束所需要的期望次数分解成
  • 当前一步(消耗1次)
  • 从新状态到结束状态的期望次数
    \(E[当前状态]=1+\sum_{新状态}(P(转移到新状态)\times E[新状态])\)
    本题以\(f_{i,j,k}\)作为状态,新状态有
    \(f_{i-1,j,k}\)(吃了一个1分装)
    \(f_{i+1,j-1,k}\)(吃了一个2分装)
    \(f_{i,j+1,k-1}\)(吃了一个3分装)
    就可以得到方程了:

\[E[i,j,k]=1+\frac{空盘数}{n}\times E[i,j,k]+\frac{i}{n}\times E[i-1,j-1,k]+\frac{j}{n}\times E[i+1,j-1,k-1]+\frac{k}{n}\times E[i,j+1,k-1] \]

  • 记住边界条件是\(f_{0,0,0}\)
    递推或是记搜都可以
double f[305][305][305];
int a[5],n; 
int main(int argc,char const *argv[]){
	scanf("%d",&n);
	for(int i=1,x;i<=n;++i){
		scanf("%d",&x);
		a[x]++;
	}	
	for(int k=0;k<=n;++k){
		for(int j=0;j<=n;++j){
			for(int i=0;i<=n;++i){
				if(i||j||k){
					if(i)f[i][j][k]+=f[i-1][j][k]*i/(i+j+k);
        			if(j)f[i][j][k]+=f[i+1][j-1][k]*j/(i+j+k);
    	    		if(k)f[i][j][k]+=f[i][j+1][k-1]*k/(i+j+k);
	        		f[i][j][k]+=(double)n/(i+j+k);
         		}
			}
		}
	}
	printf("%.15lf\n",f[a[1]][a[2]][a[3]]);
	return 0;
}

  1. K - Stones
    最简单的博弈论递推
    从小到大枚举,如果这个点的所有转移点都是必胜点,那么这个点就是比败点,否则就是必胜点
    判断一下就可以了
int n=read(),k=read(),dp[M],a[M];
signed main(){
	dp[0]=2;
	rep(i,1,n,1) a[i]=read();
	rep(i,1,k,1){
		dp[i]=2;
		rep(j,1,n,1){
			if(a[j]<=i){
				if(dp[i-a[j]]==2) dp[i]=1;
			}
		}
	}
	if(dp[k]==1) puts("First");
	else puts("Second");
	return 0;
}
  1. L - Deque
    最简单的区间dp模板,考虑利用前缀和快速获取一段区间的值
    那么会有非常明显的dp方程,考虑取队首队尾的区别
    \(f_{l,r}=\max\begin{cases}sum_{l,r}-f_{l+1,r}&\\sum_{l,r}-f_{l,r-1}\end{cases}\)
    边界条件就是\(f_{i,i}=a_i\)
    答案就是\(f_{1,n}-(sum_{1,n}-f_{1,n})\)
	n=read();rep(i,1,n,1)a[i]=read(),dp[i][i]=a[i],sum[i]=sum[i-1]+a[i];	
	rep(len,2,n,1)
	for(int l=1,r=l+len-1;r<=n;l++,r++)
		dp[l][r]=max(
			sum[r]-sum[l-1]-dp[l+1][r],
			sum[r]-sum[l-1]-dp[l][r-1]
			); // 进行 dp
	cout<<dp[1][n]-(sum[n]-dp[1][n]);
	return 0;
}
/
  1. M - Candies
    前缀和优化dp的版子,先考虑最简单的dp方程:
    \(f_{i,j}=\sum^{a_i}_{x=0}f_{i-1,j-x}\)
    发现每次搞sum累加很麻烦,前缀和优化即可)
int n=read(),k=read(),a[M],dp[105][M],sum[105][M];
signed main(){
	rep(i,1,n,1) a[i]=read();
	dp[1][0]=sum[1][0]=1;
	for(int i=1;i<=k;i++)
		dp[1][i]=(i<=a[1]),sum[1][i]=dp[1][i]+sum[1][i-1];
	for(int i=2;i<=n;i++){
		dp[i][0]=sum[i][0]=1;
		for(int j=1;j<=k;j++){
			if(j<=a[i])dp[i][j]=sum[i-1][j]%mod;
			else dp[i][j]=(sum[i-1][j]-sum[i-1][j-a[i]-1]+mod)%mod;
			sum[i][j]=(sum[i][j-1]+dp[i][j])%mod;;
		}
	}
	cout<<dp[n][k]<<endl;
	return 0;
}
/*
  1. N - Slimes
    模板,区间dp
    方程为\(f_{i,j}=\min{f_{i,j-1}+a_j,f_{i+1,j}+a_i}\)
    边界条件为\(f_{i,i}=a_i\)
long long a[400],dp[500][500],sum[10050]; 
long long n,x;
int main(){
	memset(dp,0x3f3f3f3f,sizeof(dp));
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
		dp[i][i]=0;
	}
	for(int len=1;len<n;len++){
		for(int i=1;i+len<=n;i++){
			int j=i+len;
			for(int k=i;k<j;k++){
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
			}
		}
	}
	cout<<dp[1][n]<<endl;	
	return 0;
} 
  1. O - Matching
    状压版子,用01序列表示选了那些人,可以配成那些对
int n=read(),vis[30][30],f[30][M],sum[M];
bitset<25>qqt,bbb;
signed main(){
	rep(i,1,n,1) rep(j,1,n,1) vis[i][j]=read();
	rep(i,0,(1<<n)-1,1){
		qqt=i;
		sum[i]=qqt.count();
	}
	f[0][0]=1;//没有用1个女的
	rep(i,1,n,1){
		rep(s,0,(1<<n)-1,1){
			if(sum[s]!=i) continue;//人数不对
			qqt=s;//局面
			rep(j,1,n,1){//枚举上一个选的是那个女的
				if(vis[i][j]==1){
					if(qqt.test(j-1)){
						bbb=qqt;
						bbb.reset(j-1);
						f[i][s]=(f[i][s]+f[i-1][bbb.to_ulong()])%mod;
					}
				}
			}
		}
	}
	wr(f[n][(1<<n)-1]);
	return 0;
}
  1. P - Independent Set
    简单树形dp版子
    \(f_{u,0/1}\)表示该节点涂黑涂白的方案数,考虑合并子树
    分讨子树不同涂色后乘起来就可以了
int n=read(),f[M][2];vector<int>Ed[M];
inline void dfs(int u,int fa){
	f[u][0]=f[u][1]=1;
	for(auto v:Ed[u]){
		if(v==fa) continue;
		dfs(v,u);
		f[u][0]=(f[u][0]*f[v][1])%mod;//黑
		f[u][1]=(f[u][1]*(f[v][1]+f[v][0]))%mod;//白
	}
}
signed main(){
	rep(i,2,n,1){
		int u=read(),v=read();
		Ed[u].pb(v);
		Ed[v].pb(u);
	}
	dfs(1,0);
	wr((f[1][0]+f[1][1])%mod);
	return 0;
}
  1. Q - Flowers
    线段树优化转移dp版子
    考虑到每次转移只能从比自己小的节点传过来,考虑权值线段树存f值max
int mx[M<<2];
inline void update(int id,int l,int r,int pos,int val){
	if(l==r) return mx[id]=val,void();
	int Mid=(l+r)>>1;
	if(Mid>=pos) update(ls,l,Mid,pos,val);
	else update(rs,Mid+1,r,pos,val);
	mx[id]=std::max(mx[ls],mx[rs]);
}
inline int ask(int id,int l,int r,int L,int R){
	if(l>=L&&r<=R) return mx[id];
	int Mid=(l+r)>>1,maxx=0;
	if(Mid>=L)  maxx=std::max(maxx,ask(ls,l,Mid,L,R));
	if(Mid<R) maxx=std::max(maxx,ask(rs,Mid+1,r,L,R));
	return maxx;
}
int n,ans,a[M],f[M],h[M];
signed main(){
	n=read();
	rep(i,1,n,1) h[i]=read();
	rep(i,1,n,1){
		a[i]=read();
		int maxx=ask(1,1,n,1,h[i]);
		f[i]=a[i]+maxx;
		ans=std::max(f[i],ans);
		update(1,1,n,h[i],f[i]);
	}
	wr(ans),pr(10);
	return 0;
}
  1. R - Walk
    矩阵优化dp版子
    每次最短路作floyd,相当于矩阵自乘
    考虑重载运算符快速幂即可
struct Mat{
	int H,W;
	int Sum[55][55];
	inline void clear(int h,int w){
		H=h;W=w;
		rep(i,1,H,1) rep(j,1,W,1) Sum[i][j]=0;
	}
	inline Mat operator *(const Mat &b)const{
		Mat c;
		c.clear(H,b.W);
		rep(i,1,H,1) rep(k,1,W,1) rep(j,1,b.W,1)
		c.Sum[i][j]=(c.Sum[i][j]+Sum[i][k]*b.Sum[k][j])%mod;
		return c;
	}
}a;
int n,k,ans=0;
inline Mat qp(Mat a,int y){
	Mat res;res.clear(a.H,a.W);
	rep(i,1,n,1) res.Sum[i][i]=1;
	while(y){
		if(y&1) res=res*a;
		a=a*a;y>>=1;
	}return res;
}
signed main(){
	n=read();k=read();a.clear(n,n);
	rep(i,1,n,1) rep(j,1,n,1) a.Sum[i][j]=read();
	a=qp(a,k);
	rep(i,1,n,1) rep(j,1,n,1) ans=(ans+a.Sum[i][j])%mod;
	wr(ans);
	return 0;
}
  1. S - Digit Sum
    数位dp版子,考虑到d很小,所以对取模d作维护
    记得记忆化
int f[10005][105],a[10005],D;

inline int dfs(int pos,int sum,bool limit,int res=0){
	if(pos==0) return (sum==0)?1:0;
	if(!limit&&f[pos][sum]!=-1) return f[pos][sum]%mod;
	int up=limit?a[pos]:9;
	rep(i,0,up,1)
		res=(res+dfs(pos-1,(sum+i)%D,limit&&(i==up)))%mod;
	if(!limit) f[pos][sum]=res;
	return res;
}
string s;
signed main(){
	cin>>s;D=read();
	rep(i,1,(int)s.size(),1) a[i]=(int)(s[(int)s.size()-i]-'0');
	me(f,-1); wr(((dfs((int)s.size(),0,1)-1)%mod+mod)%mod);
	return 0;
}
  1. T - Permutation
    首先考虑最基本的dp转移方程
    \(f_{i,j}\) 表示在前 \(i\) 个位置上填 \(1 \sim i\) 的数
    且最后一个数填了 \(j\) 的方案数.
    那么就会有转移方程
    \(f_{i,j}\left\{\begin{matrix}\sum^{j-1}_{k=1} f_{i-1,k} && s[i]="< " & \\\sum_{k=i-1}^{j} &&s[i]=">" & \end{matrix}\right.\)
    对这个东西作前缀和转移,再滚动一下
    可以做到时间\(n^2\),空间\(n\)的优秀复杂度
signed main(){
	n=read();f[1]=1;//初始化
	rep(i,1,n-1,1){
		char ch;cin>>ch;//处理前缀和
		rep(j,1,i,1) he[j]=(he[j-1]+f[j]+mod)%mod;
		rep(j,1,i+1,1) //如转移方程
			if(ch=='<') f[j]=he[j-1];
			else f[j]=(he[i]-he[j-1]+mod)%mod;
	}//总方案累加
	rep(i,1,n,1) ans=(ans+f[i]+mod)%mod;
	wr(ans),pr(10);
	return 0;
}
  1. U - Grouping
    这个原本是状压,但是模拟退火太爽了,看我模拟退火!
int n,a[20][20],he[20],pos[25];
int ans=-INF;
inline int g(){
	int res=0;
	rep(i,1,n,1){
		rep(j,1,n,1){
			if(i!=j&&pos[i]==pos[j])
				res+=a[i][j];
		}
	}
	return res/2;
}
inline void solve(){
	lb t=1000,eps=1e-15;
	rep(i,1,n,1)
		pos[i]=Rand()%n+1;
	lb cur=g();
	while(t>eps){
		int pl=Rand()%n+1;
		int old=pos[pl];
		int new_=Rand()%n+1;
		while(new_==old) new_=Rand()%n+1;
		pos[pl]=new_;		
		int now=g(); 
		int E=now-cur;
		if(E>0){
			cur=now;
		}else if(exp(-E/t)*RAND_MAX>rand()){
			pos[pl]=old;
		}
		t*=0.998;
	}
	ans=max(ans,cur);
}

signed main(){
	cin>>n;
	if(n==1){
		cout<<0<<'\n';
		return 0;
	}
	rep(i,1,n,1) rep(j,1,n,1) cin>>a[i][j];	
	rep(i,1,100,1) solve();
	wr(ans),pr(10);
	return 0;
}
  1. V - Subtree
    经典树形dp
    考虑计\(f_i\)表示以i为根的子树中出现黑点的次数,\(g_i\)表示子树外选到i的方案数
    那么f数组的更新是非常明显的
    \(f_u=\prod_{v\in Son_u}^{} (f_v+1)\)
    对于子树外的方案,\(g_u=\)
int n,m,qx,f[M],g[M],px[M];vector<int>Ed[M];
inline void Adde(int u,int v){Ed[u].pb(v);}
inline void dfs1(int u,int fa){
	f[u]=1;tep(v,Ed[u]){
		if(v==fa) continue;
		dfs1(v,u);
		f[u]=((f[u]*((f[v]+1+m)%m))+m)%m;
	}
}
inline void dfs2(int u,int fa){
	px[(int)Ed[u].size()]=qx=1;
	dep(i,(int)Ed[u].size()-1,0,1){
		int v=Ed[u][i];
		if(v==fa){px[i]=px[i+1];continue;}
		px[i]=((px[i+1]*(f[v]+1))+m)%m;
	}
	rep(i,0,(int)Ed[u].size()-1,1){
		int v=Ed[u][i];
		if(v==fa)continue;
		g[v]=(g[u]*qx*px[i+1]+1+m)%m;
		qx=(qx*(f[v]+1)+m)%m;
	}
	tep(v,Ed[u]) if(v!=fa) dfs2(v,u);
}
int u,v;
signed main(){
	n=read();m=read();g[1]=1;
	rep(i,2,n,1)
		u=read(),v=read(),Adde(u,v),Adde(v,u);
	dfs1(1,0);dfs2(1,0);
	rep(i,1,n,1)
		wr((f[i]*g[i]+m)%m),pr(10);
	return 0;
}
  1. W - Intervals

  2. X - Tower

  3. Y - Grid 2

  4. Z - Frog 3

posted @ 2025-11-02 18:37  rerecloud  阅读(4)  评论(0)    收藏  举报