算法——钻石金字塔(动态规划+贪心)

钻石金字塔

一、实验题目

矿工从金字塔顶部一直走到金字塔底部,每次只能走左下或者右下方向,每个区域内有一定数量的钻石,求矿工所能开采到的钻石的最大数量

在这里插入图片描述

(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 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个小时,完全独立完成,感到有了很大收获。

发布了174 篇原创文章 · 获赞 18 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41173604/article/details/103356862
今日推荐