动态规划之双进程与二维平面(c++)

本文探讨了如何使用动态规划解决涉及二维平面上的多种问题,如最长公共子序列、最短编辑距离、配置魔药等。文章通过具体题目案例,详细解析了如何将问题转化为二维动态规划的状态转移方程,并提供了相应的代码示例。动态规划方法通过抽象问题,将复杂问题简化为规模较小的子问题,从而有效解决问题。

双进程:有两个决策相互影响且同时进行

二维平面:由一般线性的动态规划转移到二维平面之中,思考方式没有太大变化

精髓:拆解较大规模的子问题时,可以拆解成i规模更小,j规模更小,或者i和j规模都减小的子问题,即dp[i][j]通常与dp[i-1][j], dp[i][j-1], dp[i-1][j-1]有关。

题目1

最长公共子序列

题目描述

字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。

令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列,使得对所有的j=0,1,…,k-1,有xij = yj。

例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。

对给定的两个字符序列,求出他们最长的公共子序列。

输入格式

第1行为第1个字符序列,都是大写字母组成,以”.”结束。长度小于5000。

第2行为第2个字符序列,都是大写字母组成,以”.”结束,长度小于5000。

输出格式

输出上述两个最长公共自序列的长度。

样例数据

input

ABCBDAB.
BACBBD.

output

4

 首先从公共可以得出这两个字符串都要包含这个子序列

所以我们状态设f[i][j]为x的前i位和y的前j为最长公共子序列的长度

易得(其实不易)

0

i=0,j=0

f [i][j]

max(f[i-1][j-1]+1,f[i][j])

i>0,j>0,xi==yj

max(f[i][j-1],f[i-1][j])

i>0,j>0,xi==yj

代码如下

#include<bits/stdc++.h>
using namespace std;
char ch1[5100],ch2[5100];
int len1,len2;
int f[5100][5100];
int main()
{
	freopen("lcs.in","r",stdin);
	freopen("lcs.out","w",stdout);
	char ch;
	len1=0;
	cin>>ch;
	while(ch!='.')
	{
		ch1[++len1]=ch;
		cin>>ch;
	}
	cin>>ch;len2=0;
	while(ch!='.')
	{
		ch2[++len2]=ch;
		cin>>ch;
	}
	memset(f,0,sizeof(f));
	for(int i=1;i<=len1;i++)
	{
		for(int j=1;j<=len2;j++)
		{
			f[i][j]=f[i][j-1];
			if(f[i][j]<f[i-1][j])
			{
					f[i][j]=f[i-1][j];
			}
			if(ch1[i]==ch2[j]&&f[i][j]<=f[i-1][j-1])
			{
				f[i][j]=f[i-1][j-1]+1;
			}	
		}
	}
	cout<<f[len1][len2];
	return 0;
}

但是我发现了一个非常厉害的转换思想,源于洛谷

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

设字符串长度为n,m,那么想象我们有一个n+1行m+1列的网格图,只能从左下角往右、上两个方向走。定义每条路径的长度都为11。记第i行第j列为(i,j)。

求最长公共子序列本质上是在两个序列中寻找最多的配对,而且这些配对的位置在序列中的位置也要分别递增。

那么,如果x_ix与y_j相等,那么我们就从(i-1,j-1)向(i,j)连一条边。这在网格图中分明是一条条捷径,那么我们要寻找最长公共子序列,可不可以转化为寻找最短路,或者说寻找经过捷径次数最多的路径呢?

这个模型是很巧妙的,满足了配对的位置在序列中的位置分别递增(因为只能往右、上走)。

那么再看第二问。显然在这个模型中,不同的公共子序列对应的,不是至少有一条边不相同的路径,而是至少有一条捷径不相同的路径。那么这个该怎么DP呢?

设到达(i,j)(i,j)最多能经过的捷径数(即序列的两个前缀的最长公共子序列长度)为mf_{i,j},方案数为f_{i,j}。显然(i,j)可以从(i-1,j)和(i,j−1)转移,如果x_i=y_j那么还可以从(i−1,j−1)转移(mf加上11)。依次转移,如果新的mf更大则直接覆盖原信息,如果mf相等则f相加。

然而,再次注意不同路径的定义。那么是不是可能存在这样一种情况:到(i−1,j−1)的一条路径,分别转移给了(i-1,j)和(i,j−1),而再一次转移给了(i,j),没有经过不同的捷径,却计算了两遍!显然只有mf_{i-1,j-1}=mf_{i,j}的时候上述情况才会发生,那么这时我们从f_{i,j}​减去f_{i-1,j-1}即可。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

这个方法把抽象的序列实体化,并且用了类似于采花生的思路,很形象

题目2

最短编辑距离

题目描述

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  • 删除–将字符串 A 中的某个字符删除。
  • 插入–在字符串 A 的某个位置插入某个字符。
  • 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式

第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大小写字母。

输出格式

输出一个整数,表示最少操作次数。

样例

输入样例1

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例1

4

 如果第一题懂了,这题就是小easy

当前的状况用f[i][j]表示的话就是指1中第i个变成2中第j个的代价

删除

f[i][j]=min(f[i-1][j],f[i][j])

插入

f[i][j]=min(f[i][j-1],f[i][j])

替换

f[i][j]=f[i-1][j-1]+1

注意相等的时候 不一定是不操作,也可能要遵循上一步的操作(之前的插入或者替换)

#include<bits/stdc++.h>
using namespace std;
int a,b;
char s1[1111],s2[1111];
int f[1111][1111];
int main()
{
	freopen("edit.in","r",stdin);
	freopen("edit.out","w",stdout);
	cin>>a;
	for(int i=1;i<=a;i++)
	{
		cin>>s1[i];
	}
	cin>>b;
	for(int i=1;i<=b;i++)
	{
		cin>>s2[i];
	}
	memset(f,10,sizeof(f));
	for(int i=0;i<=a;i++)
	{
		f[i][0]=i;
	}
	for(int i=0;i<=b;i++)
	{
		f[0][i]=i;
	}
	for(int i=1;i<=a;i++)
	{
		for(int j=1;j<=b;j++)
		{
			f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
			if(s1[i]==s2[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
			else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
		}
	}
	cout<<f[a][b];
	return 0;
} 

题目3

配置魔药

题目描述

在《Harry Potter and the Chamber of Secrets》中,Ron的魔杖因为坐他老爸的Flying Car撞到了打人柳,不幸被打断了,

从此之后,他的魔杖的魔力就大大减少,甚至没办法执行他施的魔咒,这为Ron带来了不少的烦恼。

这天上魔药课,Snape要他们每人配置一种魔药(不一定是一样的),Ron因为魔杖的问题,不能完成这个任务,他请Harry在魔药课上(自然是躲过了Snape的检查)帮他配置。

现在Harry面前有两个坩埚,有许多种药材要放进坩埚里,但坩埚的能力有限,无法同时配置所有的药材。

一个坩埚相同时间内只能加工一种药材,但是不一定每一种药材都要加进坩埚里。

加工每种药材都有必须在一个起始时间和结束时间内完成(起始时间所在的那一刻和结束时间所在的那一刻也算在完成时间内),每种药材都有一个加工后的药效。

现在要求的就是Harry可以得到最大的药效。

输入格式

输入文件的第一行有2个整数,一节魔药课的t(1≤t<≤500)和药材数n(1≤n≤100)。

输入文件第2行到n+1行中每行有3个数字,分别为加工第i种药材的起始时间t1、结束时间t2、(1≤t1≤t2≤t)和药效w(1≤w≤100)。

输出格式

只有一行,只输出一个正整数,即为最大药效。

样例数据

input

7 4
1 2 10
4 7 20
1 3 2
3 7 3

output

35

设第k种药的起始时间为a[k].st,结束时间为a[k].sw。

那么可得状态转移方程:
f[k][i][j]=max(f[k-1][a[k].st-1][j]); (i>=a[k].st)
f[k][i][j]=max(f[k-1][i][a[k].st-1]); (j>=a[k].st)
 

#include<bits/stdc++.h>
using namespace std;
struct Node
{
    int L,R,S;
}i[501];
int n,Time,ans,f[101][501][501];
bool Rule(Node t1,Node t2)
{
    if (t1.L==t2.L)
      return t1.R<t2.R;
    return t1.L<t2.L; 
}
int main() //双线程序列动态规划。
{
	freopen("medic.in","r",stdin);
	freopen("medic.out","w",stdout);
    scanf("%d%d",&Time,&n);
    for (int a=1;a<=n;a++)
      scanf("%d%d%d",&i[a].L,&i[a].R,&i[a].S);
    sort(i+1,i+n+1,Rule); //尽量按早的排序。
    for (int a=1;a<=n;a++) //前多少个。
      for (int b=0;b<a;b++) //坩埚1最后放的几号药品。
        for (int c=0;c<a;c++) //坩埚2最后放的几号药品。
        {
            f[a][b][c]=f[a-1][b][c]; //因为排过序了,所以没有别的顾虑。
            if (i[a].L>i[b].R) //不冲突,可以更新。
              f[a][a][c]=max(f[a][a][c],f[a-1][b][c]+i[a].S);
            if (i[a].L>i[c].R) //同理。
              f[a][b][a]=max(f[a][b][a],f[a-1][b][c]+i[a].S);
            ans=max(ans,max(f[a][b][a],f[a][a][c])); //顺便更新答案。
        }
    printf("%d",ans);
    return 0;
}

但是

可以想背包一样倒着推,就可以省掉一维状态,非常秀

#include<bits/stdc++.h>
using namespace std;
int t,n,f[510][510];

struct nod
{
	int s;
	int e;
	int w;
}y[110];

bool cmp(nod a,nod b)
{
	return a.e<b.e;
}

int main()
{
	freopen("medic.in","r",stdin);
	freopen("medic.out","w",stdout);
	cin>>t>>n;
	for(int i=1;i<=n;i++)
	cin>>y[i].s>>y[i].e>>y[i].w;
	sort(y+1,y+n+1,cmp);
	for(int i=1;i<=n;i++)
		for(int j=t;j>=0;j--)
			for(int k=t;k>=0;k--)
				{
					if(j>=y[i].e) f[j][k]=max(f[j][k],f[y[i].s-1][k]+y[i].w);
					if(k>=y[i].e) f[j][k]=max(f[j][k],f[j][y[i].s-1]+y[i].w);	
				}
	cout<<f[t][t];
	return 0;
}

题目4

交错匹配

题目描述

有两行自然数,UP[1..N],DOWN[1..M],如果UP[I]=DOWN[J]=K,那么上行的第I个位置的数就可以跟下行的第J个位置的数连一条线,称为一条K匹配,但是同一个位置的数最多只能连一条线。

另外,每个K匹配都必须且至多跟一个L匹配相交且K≠L 。现在要求一个最大的匹配数。

例如:以下两行数的最大匹配数为8

输入格式

第一行有两个正整数N和M。第二行N个UP的自然数,第三行M个DOWN的自然数。其中0<N、M<=200,UP、DOWN的数都不超过32767。

输出格式

一个整数:最大匹配数

样例数据

input1

12 11
1 2 3 3 2 4 1 5 1 3 5 10
3 1 2 3 2 4 12 1 5 5 3

output1

8

input2

4 4
1 1 3 3
1 1 3 3

output2

0

 这个题难就难在思路了

想到轮船问题了,那边是不允许相交,这里是必须且只能与一个匹配相交。

再仔细一想,两者什么关系都没有。(自己后来都想笑)

f[i][j]表示up取前i个数,down取前j个数时的最大匹配数,注意,不是i和j匹配。
仍然先写状态转移方程:
f[i][j]=max(f[i-1][j-1],f[i-1][j],f[i][j-1]);(这个还是比较好想的,但是不知道的条件太多了)

因为k!=l,即两个交叉的匹配的数字不能相同,故当up[i]==down[j]时不执行匹配。

假设存在kup,满足在doan的前j个数中,down[kup]==up[i]且kup距离j最近。

假设存在kdown,满足在up的前i个数中,up[kdown]==down[i]且kdown距离i最近。

则,当up[i]!=down[j]时: f[i][j]=max(f[i][j],f[kup-1][kdown-1]+2);

即up[i]与down[kup]匹配,down[j]与up[kdown]匹配。

所以我们要预处理求kup以及kdown。

#include<bits/stdc++.h>
using namespace std;
int up[222],down[222],Up[222][222],Down[222][222],f[222][222],m,n;
int main()
{
	freopen("cross.in","r",stdin);
	freopen("cross.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>up[i];
	}
	for(int j=1;j<=m;j++)
	{
		cin>>down[j];
	}
	for(int i=1;i<=n;i++) //down的前j个元素中,和up[i]相等的最靠近j下标
    {
    	for(int j=2;j<=m;j++)
    	{
    	 	if(down[j-1]==up[i]) Up[i][j]=j-1;
            else Up[i][j]=Up[i][j-1];
		}   
	}   
    for(int i=1;i<=m;i++)//up的前j个元素中,与 down[i]相等的最靠近j下标的kdown 
    {
    	for(int j=2;j<=n;j++)
    	{
    		if(up[j-1]==down[i]) Down[j][i]=j-1;
            else Down[j][i]=Down[j-1][i];
		}
	}    
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
        {
            f[i][j]=max(max(f[i-1][j],f[i][j-1]),f[i-1][j-1]);
            if(Down[i][j]!=0&&Up[i][j]!=0&&up[i]!=down[j])  
            {
            	 f[i][j]=max(f[i][j],f[Down[i][j]-1][Up[i][j]-1]+2);
			}   
        }
	}
        
    printf("%d",f[n][m]);
	return 0;
}

题目5

马兰过河卒

题目描述

棋盘上A点有一个过河卒,需要走到目标B点。卒行走的规则:可以向下、或者向右。

同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。

棋盘用坐标表示,A点(0, 0)、B点(n, m)(n, m为不超过15的整数),同样马的位置坐标是需要给出的。现在要求你计算出卒从A点能够到达B点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

输入格式

一行四个数据,分别表示B点坐标和马的坐标。

输出格式

一个数据,表示所有的路径条数。

样例数据

input

6 6 3 3

output

6

加法原理       左边的加上上边的

巧妙方法      1    地图中的马,终点都下移动一个,就不用处理边界

                    2   由于兵走位置比较特殊,所以

                 是成对角线往下延的,满足无后效性

 状态转移方程

f[1][1]

1

初始状态

f[i][j]

0

vis[i][j]=1

f[i][j]

f[i-1][j]+f[i][j-1]

map[i][j]=0,i>0,j>0

f[i][0]

f[i-1][0]

map[i][j]=0,i>0,j>0

f[0][j]

f[0][j-1]

map[i][j]=0,i>0,j>0

#include<bits/stdc++.h>
using namespace std;
int m,n,x,y;
int vis[30][30];
long long f[30][30];
int dx[10]={0,-2,-1,1,2,2,1,-1,-2};
int dy[10]={0,1,2,2,1,-1,-2,-2,-1};
int main()
{
	freopen("input.in","r",stdin);
	freopen("output.out","w",stdout);
	cin>>n>>m>>x>>y;
	f[1][1]=1;
	x++;
	y++;
	m++;
	n++;
	for(int i=0;i<=8;i++)
	{
		int xx=x+dx[i];
		int yy=y+dy[i];
		if(xx>0&&xx<=n&&yy>0&&xx<=m)
		{
			vis[xx][yy]=1;
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(vis[i][j]!=1&&!(i==1&&j==1))
			{
				f[i][j]=f[i-1][j]+f[i][j-1];
			}
		}
	}
	cout<<f[n][m];
}

题目6

摘花生 

略,之前打过

题目7

传纸条

Description

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标(1,1),小轩坐在矩阵的右下角,坐标(m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。

在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙。反之亦然。

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用0表示),可以用一个0-100的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度只和最大。现在,请你帮助小渊和小轩找到这样的两条路径。

Input Format

输入的第一行有2个用空格隔开的整数m和n,表示班里有m行n列(1<=m,n<=50)。

接下来的m行是一个m*n的矩阵,矩阵中第i行j列的整数表示坐在第i行j列的学生的好心程度。每行的n个整数之间用空格隔开。

Output Format

输出共一行,包含一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值。

Sample Input

3 3
0 3 9
2 8 5
5 7 0

Sample Output

34

常见状态设置肯定是

f[i][j][k][l]表示第一个纸条在(i,j),第二个纸条在(k,l)

有点危险,所以引入一个步数p

f[p][i][j]表示当前走了p步,第一个人走到i行,第二个人走到j行的最大价值

那列就是p-j+1了,非常细减了一维

所以状态转移

f[p][i][j]=max

记得f[1][1][1]=a[1][1]

f[p-1][i][j]

+a[i][p-i+1]

+a[j][p-j+1]

f[p-1][i-1][j]

f[p-1][i][j-1]

f[p-1][i-1][j-1]

#include<bits/stdc++.h>
using namespace std;
int m,n,a[111][111],f[111][111][111];
int main()
{
	freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);
	cin>>m>>n;
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>a[i][j];
		}
	}
	f[1][1][1]=a[1][1];
	for(int p=2;p<=m+n-1;p++)
	{
		for(int i=1;i<=m&&i<=p;i++)
		{
			for(int j=1;j<=m&&j<=p;j++)
			{
				if(i==1&&j==1)
				{
					continue;
				}
				f[p][i][j]=max(max(f[p-1][i][j],f[p-1][i-1][j]),max(f[p-1][i][j-1],f[p-1][i-1][j-1]))+a[i][p-i+1]+a[j][p-j+1];
				if(i==j) f[p][i][j]-=a[i][p-i+1];
			}
		}
	}
	cout<<f[n+m-1][m][m];
	return 0;
}

题目8

矩阵切割

题目描述

给你一个矩阵,其边长均为整数。你想把矩阵切割成总数最少的正方形,其边长也为整数。切割工作由一台切割机器完成,它能沿平行于矩形任一边的方向,从一边开始一直切割到另一边。对得到的矩形再分别进行切割。

输入格式

输入文件中包含两个正整数,代表矩形的边长,每边长均在1—100之间。

输出格式

输出文件包含一行,显示出你的程序得到的最理想的正方形数目。

样例数据

input

5 6

output

5

样例解释

我起初的思路是找到以每个点为右下角最大正方形的边长,像这样

瞪眼看了半晌也没思路,看来还是我格局小了

这个题有一点点像区间dp的思路

可以设置f[i][k] + f[i][j - k]表示第一种切矩形方法的最少正方形

                f[k][j] + f[i - k][j]表示第二种切矩形方法的最少正方形

这里面f[i][k1]+f[i][j-k1]表示将矩形横着切开以后分成的两块包含的最少的矩形,

同理可得f[k2][j]+f[i-k2][j]是竖着切开后两块的最少矩形

#include<bits/stdc++.h>
using namespace std;
int m,n,f[111][111];
int main()
{
	freopen("cuts.in","r",stdin);
	freopen("cuts.out","w",stdout);
	cin>>n>>m;
	memset(f,10,sizeof(f));
	for(int i=1;i<=min(n,m);i++)
	{
		f[i][i]=1;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			for(int k=1;k<i;k++)
			{
				f[i][j]=min(f[i][j],f[i-k][j]+f[k][j]);
			}
			for(int k=1;k<j;k++)
			{
				f[i][j]=min(f[i][j],f[i][j-k]+f[i][k]);
			}
		}
	}
	cout<<f[n][m];	
	return 0;
}

题目9

农田个数

题目描述

你的老家在农村。过年时,你回老家去拜年。你家有一片N×M农田,将其看成一个N×M的方格矩阵,有些方格是一片水域。你的农村伯伯听说你是学计算机的,给你出了一道题: 他问你:这片农田总共包含了多少个不存在水域的正方形农田。

两个正方形农田不同必须至少包含下面的两个条件中的一条:

  1. 边长不相等

  2. 左上角的方格不是同一方格

输入格式

输入数据第一行为两个由空格分开的正整数N、M(1<=m< n <=1000)

第2行到第N+1行每行有M个数字(0或1),描述了这一片农田。0表示这个方格为水域,否则为农田(注意:数字之间没有空格,而且每行不会出现空格)

输出格式

满足条件的正方形农田个数。

样例数据

input

3 3
110
110
000

output

5

这个题就是用f[i][j]来表示用该点右下角为顶点的不同正方形的个数

1 【1】【1】1【1】【2】1【1】【3】
1【2】【1】2【2】【2】2【2】【3】
1【3】【1】2【3】【2】3【3】【3】

观察上表,得

 每个格子都和左边,上边,左上有关,min再,这就简单了

状态转移:


				f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;

 代码:

#include<bits/stdc++.h>
using namespace std;
int m,n,f[1111][1111],ans;
string mapp[1111];
int main()
{
	freopen("count.in","r",stdin);
	freopen("count.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>mapp[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<m;j++)
		{
			if(mapp[i][j]=='1')
			{
				f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;
				ans+=f[i][j];
			}
		}
	}
	cout<<ans;
	return 0;
}

第10题

创意吃鱼

题目描述

可爱猫猫家里长方形大池子中有很多鱼,她开始思考:到底要以何种方法吃鱼呢(猫猫就是这么可爱,吃鱼也要想好吃法 ^_*)。

她发现,把大池子视为01矩阵(0表示对应位置无鱼,1表示对应位置有鱼)有助于决定吃鱼策略。

在代表池子的01矩阵中,有很多的正方形子矩阵,如果某个正方形子矩阵的某条对角线上都有鱼,且此正方形子矩阵的其他地方无鱼,

猫猫就可以从这个正方形子矩阵“对角线的一端”下口,只一吸,就能把对角线上的那一队鲜鱼吸入口中。

猫猫是个贪婪的家伙,所以她想一口吃掉尽量多的鱼。请你帮猫猫计算一下,她一口下去,最多可以吃掉多少条鱼?

输入格式

  第一行有两个整数n和m(n,m≥1),描述池塘规模。接下来的n行,每行有m个数字(非“0”即“1”)。每两个数字之间用空格隔开。

输出格式

只有一个整数——猫猫一口下去可以吃掉的鱼的数量,占一行,行末有回车。

样例数据

input

4 6
0 1 0 1 0 0
0 0 1 0 1 0
1 1 0 0 0 1
0 1 1 0 1 0

output

3

非常复杂的一个题,但思路还是转化常规;

设f[i][j]表示以i为横坐标,j为纵坐标的点为右下角的满足条件的子矩阵的对角线长度,

那么我们可以用f[i-1][j-1]即该坐标的斜上方的子矩阵的对角线长度+1得到f[i][j]。

但这个推法存在一个问题,无法保证其余的元素全为0。

所以我们要另外开两个数组,

一个用来装该坐标点左(右)边连续0的长度,

一个用来装该坐标点上边连续0的长度,

然后我们取这两个长度和f[i-1][j-1]这三者的最小值来+1作为f[i][j]。才能得到正确的答案。
 

dp[i][j]

min

c[i-1][j]

+1

r[i][j-1]

dp[i-1][j-i]

左,右,上中0的个数可以以预处理一下

min需要2次,因为对角线有2种(有点乱)

第一遍80分;

死因:没用scanf读1000*1000的表格

#include<bits/stdc++.h>
using namespace std;
int m,n,a[2600][2600],c[2600][2600],r[2600][2600],dp[2600][2600],ans;
int main()
{
	freopen("meal.in","r",stdin);
	freopen("meal.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(!a[i][j])
			{
				c[i][j]=c[i-1][j]+1;
				r[i][j]=r[i][j-1]+1;
			}
			else 
			{
				dp[i][j]=min(min(c[i-1][j],r[i][j-1]),dp[i-1][j-1])+1;
				ans=max(ans,dp[i][j]);
			}
		}
	}
	memset(c,0,sizeof(c));
	memset(r,0,sizeof(r));
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=1;j--)
		{
			if(!a[i][j])
			{
				c[i][j]=c[i-1][j]+1;
				r[i][j]=r[i][j+1]+1;
			}
			else 
			{
				dp[i][j]=min(min(c[i-1][j],r[i][j+1]),dp[i-1][j+1])+1;
			}
			ans=max(ans,dp[i][j]);
		}
	}
	cout<<ans<<endl;
	return 0;
}

题目11

方格取数(弱鸡版)

题目描述

设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示:

某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。

在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。

此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大。

输入格式

第一行为一个整数N,表示 N×N 的方格图。

接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。

行和列编号从 1 开始。

一行“0 0 0”表示结束。

输出格式

输出一个整数,表示两条路径上取得的最大的和。

样例

输入样例

8
2 3 13
2 6 6
3 5 7
4 4 14
5 2 21
5 6 4
6 3 15
7 2 14
0 0 0

输出样例

67

和传纸条一样,不解释

#include<bits/stdc++.h>
using namespace std;
int a,b,c,n,ss;
int m[20][20],f[40][20][20];
int main()
{
	freopen("number.in","r",stdin);
	freopen("number.out","w",stdout);
	cin>>n;
	for(int i=1;;i++)
	{
		cin>>a>>b>>c;
		if(a!=0&&b!=0&&c!=0)	m[a][b]=c;
		else break;
	}
	f[1][1][1]=m[1][1];
	for(int p=2;p<=2*n;p++)
	{
		for(int i=1;i<=n&&i<=p;i++)
		{
			for(int j=1;j<=n&&j<=p;j++)
			{
				if(i==1&&j==1) continue;
				f[p][i][j]=max(max(f[p-1][i][j],f[p-1][i-1][j]),max(f[p-1][i][j-1],f[p-1][i-1][j-1]))+m[i][p-i+1]+m[j][p-j+1];
				if(i==j) f[p][i][j]-=m[i][p-i+1];
			}
		}
	}
	cout<<f[n+n-1][n][n];
}

题目12

方格取数(大佬版)

【问题描述】
设有 n×m 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。

【输入格式】
第一行有两个整数 n,m。
接下来 n 行,每行 m 个整数,依次代表每个方格中的整数。

【输出格式】
一个整数,表示小熊能取到的整数之和的最大值。

【数据规模与约定】
对于20%的数据,n,m≤5。
对于40%的数据,n , m ≤ 50 。
对于70%的数据,n , m ≤ 300。 
对于100%的数据,1≤n,m≤10^3。方格中整数的绝对值不超过10^4。

毫无思路啊

因为感觉没有无后效性了,dp做不了,3维状态感觉还是不行,dfs25分。。

dfs:(感谢我头像这哥们)

#include<bits/stdc++.h>
using namespace std;
const int dx[4]={1,0,-1};
const int dy[4]={0,1,0};
int n,m,ans=-1e9,sum=0;
int a[1100][1100];
bool f[1100][1100];
void dfs(int x,int y)
{
	f[x][y]=1;
    if(x==n&&y==m)
	{
		ans=max(ans,sum);
		f[n][m]=0;
		return ;
	}
	for(int i=0;i<=2;i++)
	{
		int xx=x+dx[i],yy=y+dy[i];
//		if(!(xx>0&&xx<=n&&y>0&&yy<=m))  continue;
		if(xx>0&&xx<=n&&y>0&&yy<=m) 
		{
		    if(!f[xx][yy])
			{
				sum+=a[xx][yy];
				dfs(xx,yy);
				sum-=a[xx][yy];
			}  
		}
	}
	f[x][y]=0;
} 
void init()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m;j++)
	        scanf("%d",&a[i][j]);
}
void print()
{
	printf("%d",ans);
}
int main()
{
	freopen("number.in","r",stdin);
	freopen("number.out","w",stdout);
    init();
    sum=a[1][1];
    dfs(1,1);
    print();
	return 0;

正解

将数据矩阵顺时针旋转90度,

则每一步由只能向上、向下或向右走一格转变为

只能向左、向右或向下走一格,

加上题目给出的“不能重复经过已经走过的方格”的约束,

则走法便退化为了只能向下、向右走一格,

显然此时已经是经典的DP问题了,

满足DP“无后效性”的特点,便可按经典DP的自底向上方法或最后一步进行分析。


在代码实现上,为了达到旋转数据矩阵的要求,

在按题意读入数据后,按照先列后行的顺序处理便可。即以原矩阵的转置矩阵进行分析。

也就是m,n换一下

设f[i][j][0]表示由左方来到(i,j)的路径上的整数之和的最大值,

f[i][j][1]表示由右方来到(i,j)的路径上的整数之和的最大值:

● 由左方来到(i,j)的路径上的整数之和的最大值:f[i][j][0]=max(f[i][j][0],f[i][j−1][0]+a[i][j])

● 由右方来到(i,j)的路径上的整数之和的最大值:f[i][j][1]=max(f[i][j][1],f[i][j+1][1]+a[i][j])

● 由上方来到(i,j)的路径上的整数之和的最大值:

f[i][j][0]=max(f[i][j][0],max(f[i−1][j][0],f[i−1][j][1]))+a[i][j]

f[i][j][1]=max(f[i][j][1],max(f[i−1][j][0],f[i−1][j][1]))+a[i][j]

#include <bits/stdc++.h>
using namespace std;
int n,m;
long long f[1111][1111][2];
long long a[1111][1111];
int main() 
{
		freopen("number.in","r",stdin);
	freopen("number.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
			cin>>a[j][i];
	}
	memset(f,-10,sizeof(f));
	f[1][1][0]=f[1][1][1]=a[1][1];
	for(int i=1;i<=m;i++) 
	{
		for(int j=1;j<=n;j++) 
		{
			if(i>1) 
			{ 
				f[i][j][0]=max(f[i][j][0],max(f[i-1][j][0],f[i-1][j][1])+a[i][j]);
				f[i][j][1]=max(f[i][j][1],max(f[i-1][j][0],f[i-1][j][1])+a[i][j]);
			}
			if(j>1) f[i][j][0]=max(f[i][j][0],f[i][j-1][0]+a[i][j]); 
		}
		for(int j=n;j>0;j--) 
		{
			if(j<n) f[i][j][1]=max(f[i][j][1],f[i][j+1][1]+a[i][j]);
		}
	}
	cout<<max(f[m][n][0],f[m][n][1]);
	return 0;
}

题目13

滑雪

题目描述

顺治喜欢滑雪,这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待太监们来载你。顺治想知道载一个区域中最长的滑坡。

区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:

     1  2  3  4 5

    16 17 18 19 6

    15 24 25 20 7

    14 23 22 21 8

    13 12 11 10 9

顺治可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-...-3-2-1更长。事实上,这是最长的一条。

输入格式

输入的第一行表示区域的行数R和列数C(1 <= R,C <= 500)。下面是R行,每行有C个整数,代表高度h,0<=h<=10000。

输出格式

输出最长区域的长度。

input

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

output

25

首先,我们可以发现此题有 3 个输入的要点。

一个点的高度                             这个点的横坐标                      这个点的纵坐标
既然是二维的DP,那么就要使用二维的图标分析
先看下面的一张图:


i 表示行数,j 表示列数,

在这里,可以很容易的就看出来最长的路线是一条螺旋的曲线向下滑。

那么,我们是怎么想到这么一条曲线的呢?

部分人就会很自然的想到,首先将高度排序,

然后从小到大选一条最大的路线。

这是不行的,如果怎么做的话,就没有考虑到坐标的影响,

这也就是题目的一个限制的条件,只能向相邻的点滑动,

而且只能由高处往低处滑动:

 但是如果总是从最高点出发的话,那么这条路不一定是最大的。

把这道题转化一下,转化成其他的DP题。如果说把这二维的坐标全部转化为一维的数组

这个二维的图,如果说把它转化为一维的图的话,可以发现数组是这么排列的:
1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9

因为这数组原来全都是一维的,但是由于要将它改造成 5 * 5 的矩阵,

所以在上图中它执行了每五列换一次行。

根据最终的结果来看,它其实是这样的一个序列:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
也就是一个有坐标限制的最长上升子序列、

那么,我们可以开一个结构数组来存储一个点的信息,

它的信息包括它的 x ,y 坐标和它的高度,

结构数组是一维的,但是它又可以同时存储多个信息,所以它是最佳的选择,

而且它还可以用 sort 进行排序。

所以,最终的结论就是,首先把整个地图接进来,

然后把每个点的信息存入一个结构数组里面,再对它们的高度进行排序,排序完之后再仿照求最长

上升子序列的方法来求最大的滑行长度。

状态转移死也就别不难了,只可能来自上下左右4个方向,比较一下就行

f[i][j]=max()

f[i][j]

f[i-1][j]+1

f[i][j-1]+1

f[i+1][j]+1

f[i][j+1]+1

别忘了地图类的边界考虑

#include<bits/stdc++.h>
using namespace std;
struct node
{
	int x,y,h;
}mapp[360000];
int n,m; 
int len;
int ans=-1e9;
int a[666][666],f[666][666];
bool mysort(node x,node y)
{
	return x.h <y.h ;
}
int main()
{
	freopen("Shunzhi.in","r",stdin);
	freopen("Shunzhi.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			mapp[++len].x=i;
			mapp[len].y=j;
			int ddd;
			cin>>ddd;
			a[i][j]=ddd;
			mapp[len].h=ddd;
			f[i][j]=1;
		}
	}
	sort(mapp+1,mapp+1+m*n,mysort);
	for(int i=1;i<=m*n;i++)
	{
		int xx=mapp[i].x ,yy=mapp[i].y ;
		if(a[xx][yy]>a[xx-1][yy]&&xx-1>0)
		{
			f[xx][yy]=max(f[xx-1][yy]+1,f[xx][yy]);
		}
		if(a[xx][yy]>a[xx+1][yy]&&xx+1<=n)
		{
			f[xx][yy]=max(f[xx+1][yy]+1,f[xx][yy]);
		}
		if(a[xx][yy]>a[xx][yy-1]&&yy-1>0)
		{
			f[xx][yy]=max(f[xx][yy-1]+1,f[xx][yy]);
		}
		if(a[xx][yy]>a[xx][yy+1]&&yy+1<=m)
		{
			f[xx][yy]=max(f[xx][yy+1]+1,f[xx][yy]);
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			ans=max(ans,f[i][j]);
//			cout<<f[i][j]<<' ';
		}
//		cout<<endl;
	}
	cout<<ans;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值