zoukankan      html  css  js  c++  java
  • 动态规划(1)---从一个简单的例子开始

    1、从一个简单的例子开始

    爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。

    最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:

        选出任一 x,满足 0 < x < N 且 N % x == 0 。
        用 N - x 替换黑板上的数字 N 。

    如果玩家无法执行这些操作,就会输掉游戏。

    只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。

    示例输入:N=2,

    实例输出:true。

    解释:N为2时,2的约数只有1,爱丽丝选择1后,鲍勃的输入为1无法操作。

    解题思路:

      对于一个初入算法坑的萌新而言,看到这个问题的第一反应,也是人的自然反应,就是按照题目所描述的方式进行解题。很多人在学习算法的过程中会有此疑问,当我拿到一道题的时候,怎么知道要用哪个算法?诚然,当拿到一道题的时候,很多人,都是茫然的,大家的第一反应就是按照题目的叙述,进行暴力求解。我觉得暴力求解是一个不错的办法,至少很多优秀的思路就是在暴力求解的过程中慢慢改进而来的。当做过的题够多的时候,读到题干就知道需要用什么算法,就像高考的时候,看到题号就知道要考什么。

      回到本题,首先按照题干的描述,复现题干的要求,进行暴力求解。分析题干可以知道,如果输入的数字N=1,alias是先手局,则此时alias没有可以操作的数字,alias败,返回flase。由此可以得到以下结论:

      当轮到alias局时,若N=1,则alias败,返回flase,当时对手局时,N=1则 alias胜,返回true。

      在代码设计中,需要有以下几个参数:N----表征函数输入的值,isAlias----表征是否是alias的局,因为我们是要得出alias的胜利情况,因此函数返回值,需要此参数做参考。

      题干中明确指出,用N-x代替N,这是典型的递归思想,因此,函数采用递归式的设计。而函数的停止条件则是N=1,若此时isAlias=true,则返回Flase,否则返回true.

      依据上述结论,我们可以有以下代码

    boolean Gt(int N,boolean isAlias){
            /*分析可以知道,当轮到alias操作的时候,如果拿到的是1,则失败,若是对手局,对手拿到的是1则alias胜出*/
            if(isAlias && N==1)
                return false;
            if(!isAlias && N==1)
                return true;
            /*找出所有alias或者Bob能选的x*/
            List<Integer> vector = new LinkedList<>();
            for(int i=1;i<N;++i){
                if(N%i==0)
                    vector.add(i);
            }
            /*分析,x的值很多,但是不管选择什么x,一个N只会对应一个输出,也就是说对于所有的x,其得到的结果是一样的,因此,只需要计算一遍即可*/
            return Gt(N-vector.get(0),!isAlias);
        }

    经过进一步分析,对于一个N,无论alias先手选择哪个x,有且仅有唯一的一个输出,因此,无需找出所有的x,因为对于N>1,1都是N的约数,因此,有了以下代码:

    boolean Gt(int N,boolean isAlias){
            /*分析可以知道,当轮到alias操作的时候,如果拿到的是1,则失败,若是对手局,对手拿到的是1则alias胜出*/
            if(isAlias && N==1)
                return false;
            if(!isAlias && N==1)
                return true;
    
            /*分析,x的值很多,但是不管选择什么x,一个N只会对应一个输出,也就是说对于所有的x,其得到的结果是一样的,因此,只需要计算一遍即可*/
            return Gt(N-1,!isAlias);
        }

      重点到了,经过我们的剪枝,我们神奇的发现,f(n)和f(n-1)产生了关系(以上代码的最后一句),此时想一想,动态规划中,核心的不就是找出递推方程么?此时,我们误打误撞,知道了f(n)和f(n-1)存在某种联系,我们只要找出了是何种联系,就可以知道递推方程,那么,我们可以找出前N-1的解,推出N的解。因此,此时我们的主要工作从完整的模拟题干操作变成了求解递推方程。从以上的代码中,我们可以看出来,f(n, isalias) = f(n-1,!isalias),此方程蕴含着什么深刻的递推关系呢?左边的方程表示alias拿到的输入,方程的右边表征着bob拿到的输入。显然,这是一个零和博弈,或者说是一个不是你死就是我亡的博弈。

      那么回到问题的主体——alias,如果alias的输入为N,并且,alias可以选择共m个x,记为X,显然 对于每一个x属于X,(N-x)是bob的输入,那么可以得到:f(N) = !f(N-x)。这句话更为通俗的说法是:对于N的任意约数x,若有f(N-x) = flase,则会有f(N) = true,反之亦然。基于此结论或者说递推方程,我们可以给出动态规划的解法:

    boolean Gt(int N){
            boolean[] ans = new boolean[N+1];
            /*先求出前N-1的解,i=N时,会求解N的解*/
            for(int i=1;i<=N;++i) {
                for (int j = 1; j < i; ++j) {
                    /*对于任意x属于X(X的定义由题干得知)f(n) = !f(n-x)*/
                    if (i % j == 0 && !ans[i - j]) {
                        ans[i] = true;
                        break;
                    }
                }
            }
            return ans[N];
        }

      以上可以说得到了此题的动态规划解法,以上的动态规划思路也适用于一般的动态规划题目,即找到递推方程,或者说状态转移方程,然后依据状态转移方程书写代码,我们会发现,动态规划问题可以转用递归的方式求解,因为递归的方式和我们大脑思考的方式很相近,因此,当你觉得一个问题可以用递归来做的时候,你应该想一想,是否可以用动态规划求解。

      上述对于该问题的动态规划求解介绍完毕,但是做算法需要时刻问自己,问题还能否进一步优化。就本题而言,我们知道,对于任意的x属于X,其结果是一致的,因此,在上述代码中,我们用了break来提前结束,但是转念一想,1是N>1的约数,因此,为何不直接使用1呢,上述问题就转换成了:

    boolean Gt(int N){
            boolean[] ans = new boolean[N+1];
            /*先求出前N-1的解*/
            for(int i=1;i<=N;++i) {
                if(!ans[i-1])
                    ans[i] = true;
            }
            return ans[N];
        }

      翻译成人话就是

      

    ans[N] = !ans[N-1];

      我们知道 N=1时是false;递推得到,N=2时,是true,N=3时是false,N=4时是true。。。。发现凡是N为奇数,那么返回值就是false,N为偶数时,返回值就是true。因此有如下超级简单的代码

    boolean Gt(int N){
            return (N&0x01) == 0;
        }

      我觉得没有谁能从题干中一眼扫过去,就能得到上述最简的算法,优秀的算法一定是慢慢的改进而来的,永远不要放过一闪而过的灵感,也永远不要满足于现有算法。脚踏实地,一步一步的迭代、调优,才会得到真正优秀的算法。

      

      

     

  • 相关阅读:
    Html 播放 mp4格式视频提示 没有发现支持的视频格式和mime类型
    如何防止网站短信验证码被攻击
    JS和C#.NET获取客户端IP
    H5案例分享:移动端touch事件判断滑屏手势的方向
    防止asp.net连续点击按钮重复提交
    JS正则表达式验证手机号和邮箱
    sql server查询数据库的大小和各数据表的大小
    大型分布式网站架构技术总结
    一个高逼格开发者必须理解的大型分布式网站的几点概念
    C# 在程序中控制IIS服务或应用程序池关闭重启
  • 原文地址:https://www.cnblogs.com/establish/p/11599042.html
Copyright © 2011-2022 走看看