动态规划求解收集硬币(‘超’详细介绍)
问题引入:
在m*n的方格中放有一些硬币,每格的硬币数目最多为一个,从方格左上方开始收集尽可能多的硬币并把它们带到右下方的单元格。每一步只可以从当前的位置向右移动一格或向下移动一格。当遇到一个有硬币的单元格时,就会将这枚硬币收集起来。问怎样移动才能收集到最多的硬币?
我们来举个例子分析分析:
如果硬币分布情况如图:
简化分布后(有硬币为 ‘1’,无硬币 ‘0’):
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | 0 | 1 | 0 | 1 | 0 | 0 |
3 | 0 | 0 | 0 | 1 | 0 | 1 |
4 | 0 | 0 | 1 | 0 | 0 | 1 |
5 | 1 | 0 | 0 | 0 | 1 | 0 |
思路及分析:
我们假设 F(i,j)为走到(a,b)所能收集到的最大硬币数。单元格(a,b)可以经由上方(a-1,b)和左侧单元格(a,b-1)到达。单元格(a-1,b)对应的最大硬币数为 F(a-1,b),单元格(a,b-1)对应的最大硬币为 F(a,b-1)。
关键方程:
F(i,j)=Max{ F(i-1),F(i,j-1)} + arr[ i ][ j ]
注:
(1) Max 为自定义函数,用来选取较大的值。
(2) arr[ i ][ j ] 表示第 i 行,第 j 列是否有硬币,有则为 ‘1’ ,否则为 ‘0’。
利用上述公式,我们可以逐行或则逐列对 m*n 的表进行填充。
F(i,j)的情况如下:
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 1 | 1 |
2 | 0 | 1 | 1 | 2 | 2 | 2 |
3 | 0 | 1 | 1 | 3 | 3 | 4 |
4 | 0 | 1 | 2 | 3 | 3 | 5 |
5 | 1 | 1 | 2 | 3 | 4 | 5 |
注:填写规则:
以图中所标记的 ‘3’ 为例,在只能往右和往下走的情况下,选取 ‘3’ 上面和左面相邻的方格中最大的一个,如图中选取上面的 ‘2’ ,由于在第 3 行,第 4 列的位置有一个硬币,所以上面的 ‘2’ 需要加 ‘1’ ( arr[ 3 ][ 4 ] = 1 ),以此类推,课填充所有表格。
结合以上分析写出关键代码:
void FindMax() //m 代表行, n 代表列
{
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
F[i][j]=Max(F[i-1][j],F[i][j-1])+arr[i][j];
//计算出第i行,第j列的表单值。
}
}
printf("\n最多搜集 %d 个硬币。\n",F[m][n]);
//注意,输出并不是F(i,j)!!!而是表单上位于第m行,第n列的值!!!
}
回溯法的思路及分析:
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。
{个人理解:知道结果,倒推路径。(该题从终点倒推回起点,并记录路径。)}
通过回溯法我们可以有以下的判断:
(1):F(i-1,j)>F(i,j-1)时,到达单元格(i,j)的最优路径肯定来自上面的单元格。
(2):F(i-1,j)< F(i,j-1)时,到达单元格(i,j)的最优路径肯定来自左面的单元格。
(3):F(i-1,j)= F(i,j-1)时,到达单元格(i,j)的最优路径可以来自上面或左面的任一单元格。
结合以上分析写出回溯的关键代码:
void HuiSu()
{
//H[][]用来保存收集路径。H[][]为 ‘1’时表示经过该单元格,为 ‘0’ 时表示未经过。
int a=m;
int b=n;
//将行和列的值分别赋给a和b,用来定位回溯的起点。
H[1][1]=1;
H[m][n]=1;
//由于这两处都没有硬币,但却是路径的起点和终点,所以赋值为 ‘1’。
while(a>=1&&b>=1) //在回到起点时退出回溯。
{
if(F[a-1][b]>=F[a][b-1])
//判断第a行,第b列的上面的单元格是否大于等于左面的单元格。
{
H[a-1][b]=1;
a--;
//如果条件成立,则行数减一。向上退 ‘1’。
}
else
{
H[a][b-1]=1;
b--;
//如果条件不成立,则列数减一。向左退 ‘1’。
}
}
for(int i=1;i<=m;i++)
//回溯完成后对保存路径的H[][]进行遍历,并输出路径。
for(int j=1;j<=n;j++)
{
if(H[i][j]==1)
{
if(i==m&&j==n) //如果遍历到终点,则取消输出 ‘-->’。
printf("(%d,%d)",m,n);
else
printf("(%d,%d)-->",i,j);
}
}
}
完整代码:
#include<stdio.h>
#define MAX 10
int m,n; //方格的行和列
int arr[MAX][MAX]; //储存原始方格元素
int F[MAX][MAX]; //保存收集到的最大硬币数
int H[MAX][MAX];//进行回溯操作时,保存路线坐标
void HuiSu();
void FindMax();
int Max(int a,int b);
/*
方格为:
0 0 0 0 1 0
0 1 0 1 0 0
0 0 0 1 0 1
0 0 1 0 0 1
1 0 0 0 1 0
*/
void main()
{
printf("请输入方格的行数和列数:(空格隔开)\n");
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
{
printf("请输入第%d行的%d个数:(只能出现0或1)\n",i,n);
for(int j=1;j<=n;j++)
{
scanf("%d",&arr[i][j]);
}
}
FindMax();
printf("收集的路线为:\n");
HuiSu();
}
int Max(int a,int b)
{
return a>=b? a:b;
}
void FindMax()
{
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
F[i][j]=Max(F[i-1][j],F[i][j-1])+arr[i][j];
}
}
printf("\n最多搜集 %d 个硬币。\n",F[m][n]);
}
void HuiSu()
{
int a=m;
int b=n;
H[1][1]=1;
H[m][n]=1;
while(a>=1&&b>=1)
{
if(F[a-1][b]>=F[a][b-1])
{
H[a-1][b]=1;
a--;
}
else
{
H[a][b-1]=1;
b--;
}
}
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
if(H[i][j]==1)
{
if(i==m&&j==n)
printf("(%d,%d)\n",m,n);
else
printf("(%d,%d)-->",i,j);
}
}
}
输入及结果示例:
请输入方格的行数和列数:(空格隔开)
5 6
请输入第1行的6个数:(只能出现0或1)
0 0 0 0 1 0
请输入第2行的6个数:(只能出现0或1)
0 1 0 1 0 0
请输入第3行的6个数:(只能出现0或1)
0 0 0 1 0 1
请输入第4行的6个数:(只能出现0或1)
0 0 1 0 0 1
请输入第5行的6个数:(只能出现0或1)
1 0 0 0 1 0
最多搜集 5 个硬币。
收集的路线为:
(1,1)-->(1,2)-->(2,2)-->(2,3)-->(2,4)-->(3,4)-->(3,5)-->(3,6)-->(4,6)-->(5,6)