动 态♂规 划 整 理

#Dynamic Programming(DP) 动态规划刷题小结html

例题1:乌龟棋

传送门:https://www.luogu.com.cn/problem/P1541c++

题目中先给出一个长度为$n$的序列(咱们把它叫作序列$g$),其中$g_i$表示第$i$个格子的得分数组

另外,有四种卡片,每一种能够走不一样的步数,当走到第$i$个格子,就能获得$g_i$的分数,求最大得分优化

本题数据范围较小$(\leq40)$,咱们根据DP的多一种状态升一维的原则,(反正数据范围小那我数组不是乱开吗qwq),能够考虑开四维数组$f[40][40][40][40]$spa

因而,如今咱们能够用$f_$表示第1、2、3、四种牌分别用了$i、j、k、l$张code

注意到,第1、2、3、四种牌分别能够走$一、二、三、4$步,那么$f_\(就表示当前在第\)(i+2j+3k+4l)$个格子的最大得分htm

题目中给出:咱们能够直接得到$g_1$的分数,因此$f_{0000}=g_1$,这是咱们的初始状态blog

如今考虑$f_$能够由哪些状态转移获得:get

$ \begin if(i-1\geq 0) f_{(i-1)jkl}\rightarrow f_\ \ if(j-1\geq 0) f_{i(j-1)kl}\rightarrow f_\ \ if(k-1\geq 0) f_{ij(k-1)l}\rightarrow f_\ \ if(l-1\geq 0) f_{ijk(l-1)}\rightarrow f_\ \end $string

其中,“\(\rightarrow\)”表示能够由前一种状态转移到后一种状态

能够发现,转移时就是至关于选了一张卡牌,咱们用$ovo$表示转移后到了第$ovo$格,因此有$ovo=i+2j+3k+4l$

那么咱们此次转移的新增得分也就是$g_$了,如今咱们用$f_\(和转移以前的分数\)+g_$取$max$就获得了状态转移方程

下面是书面的状态转移方程:

$ \begin if(i-1\geq 0) f_=max(f_,f_{(i-1)jkl})\ \ if(j-1\geq 0) f_=max(f_,f_{i(j-1)kl})\ \ if(k-1\geq 0) f_=max(f_,f_{ij(k-1)l})\ \ if(l-1\geq 0) f_=max(f_,f_{ijk(l-1)})\ \end $

有了状态转移方程,代码就$so easy$了!!!

###\(Code\)

#include<algorithm>
#include<cstdio>
const int maxn=45;
int f[maxn][maxn][maxn][maxn],g[355];
int n,m,a,b,c,d;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;i++)
		scanf("%d",g+i);
	for(int i=0,x;i<m;i++){
		scanf("%d",&x);
		if(x==1) a++;
		else if(x==2) b++;
		else if(x==3) c++;
		else if(x==4) d++;
	}//统计每一种牌出现的数量,分别存到a,b,c,d里 
	f[0][0][0][0]=g[0];
	for(int i=0;i<=a;i++)//从一张都没有到所有选择完循环四种牌 
		for(int j=0;j<=b;j++)
			for(int k=0;k<=c;k++)
				for(int l=0;l<=d;l++){
					int OvO=i*1+j*2+k*3+l*4;//注意不能选负数张牌,因此特判一下 
					if(i!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i-1][j][k][l]+g[OvO]);
					if(j!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j-1][k][l]+g[OvO]);
					if(k!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j][k-1][l]+g[OvO]);
					if(l!=0) f[i][j][k][l]=std::max(f[i][j][k][l],f[i][j][k][l-1]+g[OvO]);
				}//这四个都是刚才的转移方程
	printf("%d\n",f[a][b][c][d]);//题目保证全部牌恰好用完,因此答案就是f[a][b][c][d] 
	return 0;
}

##例题2:滑雪 传送门:https://www.luogu.com.cn/problem/P1434

本题正解是一个记忆化搜索,可是因为本人太蒻,不会记搜,只好用DP来作这个题

安利一下大佬的记搜题解叭:https://www.cnblogs.com/sxy2004/p/13353747.html

题目中给出一个大小为$rc$的矩阵(咱们叫它矩阵$g$),其中$g_\(表示\)(i,j)$这个点的高度

要求也很简单,咱们在只向上下左右四个方向走的状况下,求出最长降低的序列的长度

咱们新建一个矩阵:\(f\),用$f_\(表示以\)(i,j)$为结尾的最长降低序列长度

如今考虑$f_$能够由哪些状态转移获得,:

$ \begin if(g_<g_{(i-1)j}) f_{(i-1)j}\rightarrow f_\ \ if(g_<g_{(i+1)j}) f_{(i+1)j}\rightarrow f_\ \ if(g_<g_{i(j-1)}) f_{i(j-1)}\rightarrow f_\ \ if(g_<g_{i(j+1)}) f_{i(j+1)}\rightarrow f_\ \end $

能够发现,转移时就是从周围四个比较高的地方走到$f_$,那么新增的长度就是$1$

咱们用$f_\(和转移以前的长度\)+1$再取$max$,就获得了状态转移方程:

$ \begin if(g_<g_{(i-1)j}) f_=max(f_,f_{(i-1)j})\ \ if(g_<g_{(i+1)j}) f_=max(f_,f_{(i+1)j})\ \ if(g_<g_{i(j-1)}) f_=max(f_,f_{i(j-1)})\ \ if(g_<g_{i(j+1)}) f_=max(f_,f_{i(j+1)})\ \end $

可是,这个状态转移方程的条件是在计算$f_$以前,它周围比它高的格子已经被计算过了

那么解决这个问题的最简单的办法就是:直接$DP$$nm$遍,可是看到$n\leq 100$的范围,我就抱着试一试的心态上了

###\(Code\)

#include<algorithm>
#include<cstring>
#include<cstdio>
const int maxn=105;
int f[maxn][maxn],g[maxn][maxn],n,m,ans;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			scanf("%d",&g[i][j]);
			f[i][j]=1;
		}
	for(int k=1;k<=n*m;k++)//直接DPn*m遍
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++){
				if(g[i][j]<g[i-1][j]) f[i][j]=std::max(f[i][j],f[i-1][j]+1);
				if(g[i][j]<g[i+1][j]) f[i][j]=std::max(f[i][j],f[i+1][j]+1);
				if(g[i][j]<g[i][j-1]) f[i][j]=std::max(f[i][j],f[i][j-1]+1);
				if(g[i][j]<g[i][j+1]) f[i][j]=std::max(f[i][j],f[i][j+1]+1);
			}//上面四句都是推过的状态转移方程
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			ans=std::max(ans,f[i][j]);//自带大常数的扫一遍求最大值
	printf("%d\n",ans);
	return 0;
}

其实这题数据出水了,结果这份$O(n2m2)\(的代码勇夺90分\)(Test #10 TLE)$

如今考虑怎么优化:

首先,上面的这个思路的复杂度瓶颈就在于:为了解决上面标红的那个问题,咱们进行了$nm$次$DP$,这致使了时间的浪费

我忽然想到一个玄学作法:周围比它高的格子要先于他计算,那我从最高的格子开始高度依次递减DP不就得了!

因而要维护一个高度最大值和坐标,我又想到了$priority$_\(queue\),这样时间复杂度猛降到大约$O(nmlogn)$

##\(Code\)

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
const int maxn=105;
std::priority_queue< std::pair<int, std::pair<int,int> > >q;//pair套pair,第一个pair的第一维表示高度,维护最大高度,第二维的第一维表示横坐标,第二维表示纵坐标
int f[maxn][maxn],g[maxn][maxn],n,m,ans;
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9'){ if(ch=='-') f=-1;ch=getchar();}
    while(ch>='0' && ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
    return x*f;
}//乱写的快读
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			g[i][j]=read();
			q.push(std::make_pair(g[i][j],std::make_pair(i,j)));//进入大根堆
			f[i][j]=1;//全部点初始化为1(一个点就算不走,他本身就是1的长度)
		}
	while(q.size()!=0){//当大根堆不为空,循环DP
		int height=q.top().first;
		int row=q.top().second.first;
		int line=q.top().second.second;
		q.pop();
		if(g[row][line]<g[row-1][line]) f[row][line]=std::max(f[row][line],f[row-1][line]+1);
		if(g[row][line]<g[row+1][line]) f[row][line]=std::max(f[row][line],f[row+1][line]+1);
		if(g[row][line]<g[row][line-1]) f[row][line]=std::max(f[row][line],f[row][line-1]+1);
		if(g[row][line]<g[row][line+1]) f[row][line]=std::max(f[row][line],f[row][line+1]+1);
		ans=std::max(f[row][line],ans);//去掉了大常数qwq
	}//上面是状态转移方程(和第一个同样只是换了名字
	printf("%d\n",ans);
	return 0;
}

事实上证实,这份代码跑的飞起

关于今天的$DP$刷题整理完成啦!\(OvO\)

PS:感谢您的阅读$qwq$