钻石金字塔
一、实验题目
矿工从金字塔顶部一直走到金字塔底部,每次只能走左下或者右下方向,每个区域内有一定数量的钻石,求矿工所能开采到的钻石的最大数量
(1)矿工并不事先知道金字塔的钻石分布,但是他可以估算前面两个方块内的钻石数,或者租用探测器来获得前x步内钻石的分布。
(2)又或者,假设他有一张残破的地图
这些情况下的信息量和矿工收益有怎么样的关系呢?
二、实验环境
操作系统:Windows 10 专业版
硬件设备:cpu i5-7200HQ 内存8GB
编码环境:c/c++ Clion
三、实验目的
1、加强对动态规划思想的理解和认识
2、提高问题分析能力,数据处理能力。
四、实验内容及其实验步骤
1、数据生成
使用随机数的方法,采用二维数组存储每个区域内的钻石数量。
使用二维数组存储金字塔,只使用到左上半部分。
每个区域内钻石的数量为0~1000(可自定义)
#include <cstdlib>
#include <ctime>
int map[maxn][maxn]; //地图的大小
void DataGenerate()
{
srand((unsigned)time(NULL));
int i,j;
cin>>n; // 输入金字塔的深度
for(i=1;i<=n;i++)
{
for(j=1;j<=n-i+1;j++)
{
map[i][j]=rand()%1000;
cout<<map[i][j]<<" ";
}
cout<<endl;
}
}
令 n = 10,随机生成数据如下
将数组旋转45度,即可得到钻石金字塔,假设矿工目前的位置为(x,y),那么矿工的下一步可以开采 (x+1,y) 或者 (x,y+1)
采用密集分布的方法。
#define K 0.1
void DataDense()
{
srand((unsigned)time(NULL));
cin>>n;
int num = n * K;
num = num > 1? num:1;
int dia,x,y,i,j;
while(num--)
{
x = rand()%n + 1;
y = rand()%(n-x-1) + 1;
dia = rand() % diamond;
for(i=x>2?x-2:1;i<x+2 && i<n;i++)
for(j=y>2?y-2:1;j<y+2&&j<n;j++)
{
if(map[i][j]<dia-abs(x+y-i-j))
map[i][j] = dia-abs(x+y-i-j);
}
}
/*
for(i=1;i<=n;i++)
{
for(j=1;j<=n-i+1;j++)
{
// map[i][j]=rand()%diamond;
cout<<map[i][j]<<" ";
}
cout<<endl;
}
*/
}
n=20
2、全局动态规划(上帝视角、探测距离为无穷大)
定义二维数组 ans,ans[x][y]表示矿工从出发点到达位置 (x,y)处的最优值,这个最优解是由下面的递推式得到
动态规划: ans[x][y] = max( ans[x-1][y] , ans[x][y-1] ) + map[x][y];
全遍历,动态规划,可以很轻松的得到每一个区域的最优值。
int ans_simple=0;
int ans[maxn][maxn]={0};
int dir[maxn][maxn]={0};
void simple() // God perspective add Dynamic programming
{
int i,j;
for(i=1;i<=n;i++)
for(j=1;j<=n-i+1;j++)
{
ans[i][j]=max(ans[i-1][j],ans[i][j-1]) + map[i][j];
/* if(ans[i-1][j]>ans[i][j-1])
dir[i][j] = 0;
else
dir[i][j] = 1;
*/
}
for(i=1;i<=n;i++)
{
if(ans[i][n-i+1]>ans_simple)
ans_simple=ans[i][n-i+1];
}
cout<<ans_simple<<endl;
}
使用数组存储金字塔从1开始存储,可以方便这一步骤的计算。
例如对于n=3规模
1 3 2
2 1
1
在数组中的存储是
0 0 0 0
0 1 3 2
0 2 1
0 1
ans[0][i]=0 ans[i][0]=0 就是起始条件。
如上代码只得到了最优值,而没有得到最优解。
使用一个数组,来存储它的前一个位置,即可得到最优解,最优解代码在注释中有体现。
之后将不在讨论最优解,方法类似,主要来讨论如何求得最优值。
3、探测范围 = 1
下面我们来分析探测范围=1的情况,矿工每次探测一步,即下一步是左下还是右下,如果选择一个方向继续探测,那么另一个方向的未来信息无法得知。
举个例子 n = 3
5 2 4
3 1
1
当前矿工的位置是(1,1),那么对矿工来说地图为
5 2 #
3 #
#
矿工采用贪婪算法,选择眼前能看到的得到的最大值的策略,下一步往下走,不往右走。
最终矿工可以获得的钻石数量为 5 + 3 + 1 = 9
该地图的最优值为 5 + 2 + 4 = 11
这是一个贪婪策略,这种算法并非能找到最优值,但是可以找到矿工眼中看到的最优值,总是寻找局部最优解。
算法思想及其伪代码
curr = map[1][1]; //开始位置
do{
探测右边和下边两个方向的钻石数量
哪个方向钻石数量多,就移动到该位置
}
while(矿工未到达金字塔底部)
算法如下
void greedy_probe(int x,int y,int curr)
{
if(x+y==n+1) //矿工到达了金字塔底部
{
if(curr > ans_probe_1)
ans_probe_1 = curr;
return;
}
map[x+1][y] > map[x][y+1]
? greedy_probe(x+1,y,curr+map[x+1[y]) :greedy_probe(x,y+1,curr+map[x][y+1]);
}
void probe_1()
{
//greedy
greedy_probe(1,1,map[1][1]);
cout<<ans_probe_1<<endl;
}
4、探测范围 = m
前面两种策略都是对该方法的铺垫,也可以说是m=∞和m=1的特殊情况。
下面我将上面两种算法结合起来,可以得到 自定义探测范围 的解。
从例子出发,给出例子
矿工从金字塔顶 (1,1) 出发,取m = 2,他可以看到的地图是
326 | 330 | 519 | # | # |
---|---|---|---|---|
301 | 142 | # | # | |
872 | # | # | ||
# | # | |||
# |
矿工根据当前自己知道的钻石分布情况,来选择下一步该如何走,是向右还是向下。
非常直接,矿工经过思考,它有4条路可以选择
301 -> 872
301 -> 142
330 -> 519
330 -> 142
于是矿工认为向下走,即下一步是301,可以获得更多的钻石数量,而不是选择走330,即使330>301,可以说,矿工的眼光相对更长远了。
于是当前地图为:
326 | 301 | 519 | # | # |
---|---|---|---|---|
301* | 142 | 155 | # | |
872 | 358 | # | ||
124 | # | |||
# |
标* 的位置,为矿工的当前位置,矿工此时依旧有4条路可以选择
872 -> 124
872 -> 358
142 -> 155
142 -> 358
矿工依旧聪明的选择了下一步为 872
对上述分析作总结
假设矿工的探测范围为m,矿工当前的位置为(x,y)
那么矿工可以看到的金字塔的信息为
(x,y) (x,y+1) (x,y+1) (x,y+2) … (x,y+m)
(x+1,y) (x+1,y+1) … (x+1,y+m-1)
…
(x+m,y)
至此,可以构造处矿工观察到的局部金字塔
矿工的探测范围是一个边长为m的小的金字塔
矿工的下一步选择只有向右或者向下
如果下一步向右,那么可以有一个m-1规模的金字塔,可以对这个小的金字塔使用全局动态规划,求解出下一步为右情况下,矿工可以期望得到的最多钻石数量。
同理,下一步向下,也可以求得期望得到的最多钻石的数量。
此时矿工需要觉得这一步该怎么走?
策略是:
如果下一步向右走,期望得到的钻石数量更多,就往右走,否则向下走
我们使用两次规模大小为m的全局动态规划,只能决定矿工应该走的下一步该如何选择?是向右还是向下。执行完这一步,那么矿工将面临新的局部金字塔
分析完毕,总结来说,就是 探测范围为m = 全局动态规划 + 探测范围为1,这也启示我们复杂的问题是往往都是简单问题的组合。
注意:矿工如果快要到达金字塔边界时,探测范围为m可能已经超出了范围,此时我们取边界值即可。
int dynamic(int x,int y,int steps) //矿工当前位置为(x,y),可以看到的规模是steps
{
fill(arr[0],arr[0]+maxn*maxn,0);
int i,j;
int res=0;
int right = n-x+1;
if( x + steps > right )
steps = right -x;
for(i=x;i<=x+steps;i++)
{
for(j=y;i+j<=x+y+steps;j++)
{
arr[i][j]=max(arr[i-1][j],arr[i][j-1]) + map[i][j];
}
}
for(i=x;i<=x+steps;i++)
if(arr[i][steps-i+y+x]>res)
{
res = arr[i][steps-i+x+y];
}
return res;
}
void probe_go(int x,int y,int &res,int m)
{
int a=0,b=0;
if( x+1 <= n && y <= n && x+y <= n+1) // right
{
a = dynamic(x+1,y,m-1);
}
if( x <= n && y+1 <= n && x+y <= n+1 ) // down
{
b = dynamic(x,y+1,m-1);
}
if(a==0&&b==0)
return;
if(a > b) //根据两个方向的期望值,来决定下一步向哪个方向走
{
res+=map[x+1][y];
probe_go(x+1,y,res,m);
}
else
{
res+=map[x][y+1];
probe_go(x,y+1,res,m);
}
}
void probe_m() // predict m steps for diamonds
{
int m;
int res=0;
cin>>m;
res = map[1][1];
probe_go(1,1,res,m);
cout<<res<<endl;
/*x y is the current position of the miner,
he can predict m steps so that he choose the best next step */
}
四、实验数据分析
设金字塔的规模为N,
全局动态规划很明显就是两层for循环,时间复杂度为O(N2)
探测范围为m,矿工总共需要移动N-1次(不包括出发点)到达金字塔底部,上面分析已经知道,矿工每移动一次,需要计算的规模时O(M2),因此总的时间复杂度为O(N×M2)
测试代码如下,计算10次求平均值
void probe_m() // predict m steps for diamonds
{
int m[]={1,2,5,10,20,40};
int res=0,r;
// cin>>m;
int i,j;
// res = map[1][1];
for(i=0;i<6;i++)
{
r = 0;
for(j=0;j<10;j++)
{
res=map[1][1];
probe_go(1,1,res,m[i]);
r+=res/10;
}
cout<< m[i] << ":" << r<<endl;
}
}
m\N | 100 | 200 | 500 | 1000 |
---|---|---|---|---|
1 | 64180|67830 | 136240 | 329183 | 647560 |
2 | 68080|70520 | 132590 | 337477 | 688859 |
5 | 68310|73830 | 143800 | 353247 | 713960 |
10 | 69590|71910 | 144780 | 357894 | 719028 |
20 | 73000|74390 | 141730 | 356911 | 713875 |
40 | 62620|75020 | 147430 | 363764 | 731622 |
N(最优解) | 73867|75026 | 147431 | 368527 | 1244154 |
当N=500,1000的时候,计算机计算已经非常吃力,我只随机的计算了一次作为结果。
可以从数据结果中粗略看到,当m逐渐增大时,计算出的结果往往会向最优解靠近(但不绝对)。计算时间和计算结果之间的关系也非常有意思。
当N远大于m时,贪婪算法难以得到理想的结果,见N=1000情况。
而N的其他三个值,虽然随着m的逐渐增大,计算的结果可能越接近最优值,但是付出的时间代价要多的多
例如 N = 100,m=5 取值73830,m=40 取值 75020
m=5 计算量 5*5*100
m=40 计算量 40*40*400
理论上,m=40是m=50花费时间的 (40/5)2=64倍,而计算精确度差了多少呢?
73830 / 75026 × 100% = 98.4%
75020 / 75026 × 100% = 99.99%
有力的体现了,贪心往往无法达到最优值,但是可以在时间复杂度低得多的情况下,取得近似最优值
五、实验总结
本次实验融合了动态规划和贪心策略的知识,加强了对两种算法思想的理解与认识,体会了算法策略的由浅入深的
突破策略,复杂的问题可以通过简单的问题的组合来解决。
实验耗费时间4个小时,完全独立完成,感到有了很大收获。