【动态规划】记忆搜索(C++)

  前几天还在踟蹰我应该注重培养做项目的能力还是修炼算法以及数据结构,然后发现这个场景有点似曾相识。曾几何时的一个月里,我有三件比较重要的事情要解决,在那个月刚开始的时候我一直在想我应该从那件事情开始着手,甚至在脑海里给这三件事情的重要性排个序,思绪争执不下,烦躁之极,不如玩玩游戏散散心吧,或许等下就知道怎么做了......结果一个月下来,没有一件事的结果能使我十分满意。做项目的编程能力固然需要学习,但技术更替这么快,亘古不变的是那些算法的思维和设计的数据结构,既然决定要学,就踏踏实实从算法方面入手吧。

  前些日子了解了滚动数组的求解,这几天学习了计划搜索。什么是记忆搜索,就是你在当前step所做的决定影响到了你接下来的每个决定,你必须考虑下一步的结果,使最后的价值最大化。以下通过两个例子来详细分析一下。(鄙人觉得,学习算法的初步就是学习例题,刚开始的时候不要急于从例题当中提取那些比较深度的思维,只管学习,当火候到了,自然就举一反三,当然这其中的兴趣成分十分重要。就像那个谁名人来的说过:我读诗,并不刻意去理解这句诗是什么意思,我只是喜欢她,我只是不断去读她,直到有一天,我们相互理解)

1.Coins in a line : http://www.lintcode.com/en/problem/coins-in-a-line/

2.Coins in a line II : http://www.lintcode.com/en/problem/coins-in-a-line-ii/

对于第一题,大概题意是这样的:给你一排硬币,两个足够聪明的人交替从头开始取这些硬币(两个人必须足够聪明,假设是爱因斯坦和特斯拉吧^ ^),每次可以取一或两个,当取走最后哪个硬币的人获胜,给你n个硬币,问先手取的人能否获胜。

按照解动态规划的步骤来

  1、状态(State):当目前有n个硬币,我要怎么取才能保证我能取得最后一枚硬币,我们往前推一步,n个硬币当中,我可以取一枚或者两枚,当你这一步取完的时候,对手也会执行同样的动作(足够聪明的对手也会考虑怎么取才能取到最后一枚),我们用布尔类型dp[x]来表示当目前剩下x枚硬币且轮到你来取硬币的时候能不能赢。这个时候我们分两种情况讨论:

①我取一枚,对手可以取一枚或者两枚,所以到下一轮到我取硬币的时候,标示你能否获胜的状态就是dp[x-2]或dp[x-3](对手取一枚或者两枚)。

②我取两枚,对手可以取一枚或者两枚,到下一轮到我取硬币的时候,标志你能否获胜的状态就是dp[x-3]或dp[x-4]。

那也就是说,如果dp[x-2]和dp[x-3]都是true(获胜)的话,那我在x这一步肯定取一枚,这样就能保证在dp[x]的时候为true。为什么dp[x-2]和dp[x-3]要同时为true才行,因为你取一枚之后,对手取完再轮到你的时候决定你是取dp[x-2]和dp[x-3]的情况是由对手决定的,如果其中有一个为false,那对手肯定不会让另一个true发生。反之也成立,如果dp[x-3]和dp[x-4]同时为true的话,那我在x这一步的时候肯定取两枚,这样对手就无论取一枚或者两枚都无法阻止我获胜啦嘿。

                            

如示意图中,假设当前剩下四枚硬币,如果我取一枚,接下来对手面对三枚,怎么取都能让下一步的我取到最后一枚硬币,但如果我取两枚,那对手足够聪明,肯定会两枚都取而获胜,所以我在面对四枚硬币的时候肯定取一枚。此处就相当于确定,如果是我面对四枚硬币,那我肯定能赢,所以往上推,我只需要考虑能不能构造剩下四枚硬币的结果就可以了。

  2、方程(Function):状态确定了,方程也随之能够确定下来。不难得出,当剩下n枚硬币,此时先手的人能否获胜为dp[n], 有

  dp[n] = MemorySearch(n-2) && MemorySearch(n-3) || MemorySearch(n-3) && MemorySearch(n-4); 

  3、初态(Initialization):轮到我取硬币,如果已经没有了(n=0),那说明对手赢了,如果剩下一枚或者两枚,那我能赢,剩下三枚,对手赢,剩下四枚,我赢。因此有

   if(0 == n || 3 == n) dp[n] = false; if(1 == n|| 2 == n||4 == n) dp[n] =true 

  4、结果(Result): dp[n] (面对n枚硬币先手)

 代码实现如下:传入n枚硬币作为参数,返回是否能够获得胜利。

bool firstWillWin(int n) {
        int dp[n+1];
        bool flag[n+1] = {false};
        return MemorySearch(dp,n,flag);
    }
    bool MemorySearch(int *dp,int n,bool *flag){
        if(true == flag[n])
            return dp[n];
        flag[n] = true;
        if(0 == n||3 == n) dp[n] = false;
        else if(1 == n||2==n||4==n||5==n) dp[n]= true;
        else{
            dp[n] = ( MemorySearch(dp,n-2,flag) && MemorySearch(dp,n-3,flag) )|| ( MemorySearch(dp,n-3,flag)&&MemorySearch(dp,n-4,flag) );
        }
        return dp[n];
    }

以上是根据普通记忆搜索算法的思路简单明了容易理解,但此题还可以用数组状态推移的方法解。确定初态之后,以后的每个f[x]的状态都可以由f[x-2],f[x-3],f[x-4]得出,因此有

bool firstWillWin(int n) {
        bool f[n];
        if(1 == n ||2==n||4 == n||5 == n) return true;
        if(3 == n || 0==n) return false;
        f[0] = true;
        f[1] = true;
        f[2] = false;
        f[3] = true;
        f[4] = true;
        for(int i=5;i<n;i++)
            f[i] = (f[i-2] && f[i-3]) || (f[i-3] && f[i-4]);
        return f[n-1];
}

此处f[x]表示面对的硬币数为x+1枚。最后的结果为f[n-1](即n枚硬币的情况)。

到此,解决了给你一堆硬币轮流取,到底会不会赢的情况。接下来考虑一下一道拓展的题目,看第二个问题:Coins in a line II

题意大概是:给你一堆不同价值的硬币,取硬币的规则跟前者一样,取完硬币的时候获得价值总值高的一方胜利,问先手取的人能否获胜。

在这种情况下,就不是取到最后一个硬币就能够赢得,每一步我取一枚或者两枚都好,只要硬币取完拥有的价值量最高。那好,当硬币少于或者等于3枚的时候先手,那没有疑问,取最大值价值量最高(1枚的时候取1枚,2枚或3枚的时候取2枚),当然这个时候轮到对手的时候也会这么取,但当有四枚硬币的时候怎么取。举个栗子,当前为【1,2,4,8】的时候,我取一个,价值为【1】,剩下【2,4,8】,经过前面的推导,剩下三枚取两枚,结果被取走{2,4},我取最后一个【8】,这个时候价值总量为【9】。如果我取两个,价值为【3(1+2)】,剩下【4,8】,必须被对手取走,此时价值总量为【3】,比前一种情况的价值量低,所以在剩下四枚硬币而且此时我先手的话,肯定能根据这种方法来判断我到底取一枚还是两枚使最后获得的价值量最高。用dp[x]来表示当前剩下x枚硬币的时候取到最后能取得的最大价值量,当前我取一个,下一轮我面对的价值量就是f[x-2]或f[x-3],因为对手足够聪明,所以他肯定会根据f[x-2]和f[x-3]的价值量来决定当他面对x-1个硬币的时候是取一枚还是两枚,此时的话 dp[x] = min(dp[x-2],dp[x-3]) + values[x] ,此为面对x枚硬币取一枚的情况。第二种情况我取两枚,下一轮我面对的价值量是f[x-3]或f[x-4],同样的道理,但最后 dp[x] = min(f[x-3],f[x-4]) + values[x] + values[x+1] ,所以我到底取一枚还是两枚,价值量是为 dp[x] = max( min(dp[x-2],dp[x-3])+values[x],min(dp[x-3],dp[x-4])+values[x]+values[x+1]) ,往上推同样的道理,dp[x]的最大值取决于dp[x-2],dp[x-3],dp[x-4]的值,最后可以得出dp[n]就是面对n枚硬币的时候先手可以取得最大的价值量,此时只要判断dp[n] > sum/2即可胜出,sum为所有硬币的总共价值量。

代码如下:

bool firstWillWin(vector<int> &values) {
        int sum = 0;
        int dp[values.size()+1];
        bool flag[values.size()+1];
        for(int i=0;i<values.size()+1;i++) flag[i] = false;
        for(vector<int>::iterator ite = values.begin();ite!=values.end();ite++)
            sum += *ite;
        return sum/2<MemorySearch(dp,values.size(),flag,values);
    }
int MemorySearch(int* dp,int n,bool *flag,vector<int> values){
        if(flag[n])
            return dp[n];
        flag[n] = true;
        int count = values.size();
        if(0 == n) dp[n] = 0;
        else if(1 == n) dp[n] = values[count-n];
        else if(2 == n||3 == n) dp[n] = values[count-n] + values[count-n+1];
        else{
            dp[n] = max( min(MemorySearch(dp,n-2,flag,values),MemorySearch(dp,n-3,flag,values))+ values[count-n],
                             min(MemorySearch(dp,n-3,flag,values),MemorySearch(dp,n-4,flag,values))+values[count-n]+values[count-n+1] );
        }
        return dp[n];
}

除了这个方法,还有类似第一个题目第二种解法的方法,此处留给读者自己实现。

以上为个人对记忆搜索算法的求解过程的理解,每一句代码均经过本人测试可用,如有问题,希望大家提出斧正。

 

 

尊重知识产权,转载引用请通知作者并注明出处!

posted @ 2017-03-10 14:03  林、Zephyr  阅读(473)  评论(0编辑  收藏  举报