• 动 态♂规 划 整 理


    Dynamic Programming(DP)

    动态规划刷题小结

    例题1:乌龟棋

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

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

    另外,有四种卡片,每一种可以走不同的步数,当走到第(i)个格子,就能得到(g_i)的分数,求最大得分

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

    于是,现在我们可以用(f_{ijkl})表示第一、二、三、四种牌分别用了(i、j、k、l)

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

    题目中给出:我们可以直接获得(g_1)的分数,所以(f_{0000}=g_1),这是我们的初始状态

    现在考虑(f_{ijkl})可以由哪些状态转移得到:

    ( egin{cases} if(i-1geq 0) f_{(i-1)jkl} ightarrow f_{ijkl}\ \ if(j-1geq 0) f_{i(j-1)kl} ightarrow f_{ijkl}\ \ if(k-1geq 0) f_{ij(k-1)l} ightarrow f_{ijkl}\ \ if(l-1geq 0) f_{ijk(l-1)} ightarrow f_{ijkl}\ end{cases} )

    其中,“( ightarrow)”表示可以由前一种状态转移到后一种状态

    可以发现,转移时就是相当于选了一张卡牌,我们用(ovo)表示转移后到了第(ovo)格,所以有(ovo=i+2j+3k+4l)

    那么我们这次转移的新增得分也就是(g_{ovo})了,现在我们用(f_{ijkl})和转移之前的分数(+g_{ovo})(max)就得到了状态转移方程

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

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

    有了状态转移方程,代码就(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
    ",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_{ij})表示((i,j))这个点的高度

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

    我们新建一个矩阵:(f),用(f_{ij})表示以((i,j))为结尾的最长下降序列长度

    现在考虑(f_{ij})可以由哪些状态转移得到,:

    ( egin{cases} if(g_{ij}<g_{(i-1)j}) f_{(i-1)j} ightarrow f_{ij}\ \ if(g_{ij}<g_{(i+1)j}) f_{(i+1)j} ightarrow f_{ij}\ \ if(g_{ij}<g_{i(j-1)}) f_{i(j-1)} ightarrow f_{ij}\ \ if(g_{ij}<g_{i(j+1)}) f_{i(j+1)} ightarrow f_{ij}\ end{cases} )

    可以发现,转移时就是从周围四个比较高的地方走到(f_{ij}),那么新增的长度就是(1)

    我们用(f_{ij})和转移之前的长度(+1)再取(max),就得到了状态转移方程:

    ( egin{cases} if(g_{ij}<g_{(i-1)j}) f_{ij}=max(f_{ij},f_{(i-1)j})\ \ if(g_{ij}<g_{(i+1)j}) f_{ij}=max(f_{ij},f_{(i+1)j})\ \ if(g_{ij}<g_{i(j-1)}) f_{ij}=max(f_{ij},f_{i(j-1)})\ \ if(g_{ij}<g_{i(j+1)}) f_{ij}=max(f_{ij},f_{i(j+1)})\ end{cases} )

    但是,这个状态转移方程的条件是在计算(f_{ij})之前,它周围比它高的格子已经被计算过了

    那么解决这个问题的最简单的办法就是:直接(DP)(nm)遍,但是看到(nleq 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
    ",ans);
    	return 0;
    }
    

    其实这题数据出水了,结果这份(O(n^2m^2))的代码勇夺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
    ",ans);
    	return 0;
    }
    

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

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

    PS:感谢您的阅读(qwq)

  • 相关阅读:
    看到关于java资料比较全的,自己收藏
    ie6下pnghack——css方案
    git基本操作
    购物车功能实现
    jquery学习:获取位置position(),offset(),scrollTop困惑
    Datax3.0使用说明
    Scala字符串操作
    Scala中class、object、case class、case object区别
    Scala构造函数
    scala数组操作
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/13354156.html
Copyright © 2020-2023  润新知