动态规划

动态规划算法的核心

理解一个算法就要理解一个算法的核心,动态规划算法的核心是下面的一张图片和一个小故事。

A * "1+1+1+1+1+1+1+1 =?" *

A : "上面等式的值是多少"
B : *计算* "8!"

A *在上面等式的左边写上 "1+" *
A : "此时等式的值为多少"
B : *quickly* "9!"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"

由上面的图片和小故事可以知道动态规划算法的核心就是记住已经解决过的子问题的解。

 

动态规划算法的两种形式

上面已经知道动态规划算法的核心是记住已经求过的解,记住求解的方式有两种:

  • ①自顶向下的备忘录法
  • ②自底向上。

为了说明动态规划的这两种方法,举一个最简单的例子:求斐波拉契数列Fibonacci 。先看一下这个问题:

Fibonacci (n) = 1;   n = 0

Fibonacci (n) = 1;   n = 1

Fibonacci (n) = Fibonacci(n-1) + Fibonacci(n-2)

以前学c语言的时候写过这个算法使用递归十分的简单。先使用递归版本来实现这个算法:

public int fib(int n)
{
    if(n<=0)
        return 0;
    if(n==1)
        return 1;
    return fib( n-1)+fib(n-2);
}
//输入6
//输出:8

 

先来分析一下递归算法的执行流程,假如输入6,那么执行的递归树如下:


上面的递归树中的每一个子节点都会执行一次,很多重复的节点被执行,fib(2)被重复执行了5次。由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。下面就看看动态规划的两种方法怎样来解决斐波拉契数列Fibonacci 数列问题。
①自顶向下的备忘录法

public static int Fibonacci(int n)
{
        if(n<=0)
            return n;
        int [ ]Memo=new int[n+1];        
        for(int i=0;i<=n;i++)
            Memo[i]=-1;
        return fib(n, Memo);
    }
    public static int fib(int n,int []Memo)
    {

        if(Memo[n]!=-1)
            return Memo[n];
    //如果已经求出了fib(n)的值直接返回,否则将求出的值保存在Memo备忘录中。               
        if(n<=2)
            Memo[n]=1;

        else Memo[n]=fib( n-1,Memo)+fib(n-2,Memo);  

        return Memo[n];
    }

备忘录法也是比较好理解的,创建了一个n+1大小的数组来保存求出的斐波拉契数列中的每一个值,在递归的时候如果发现前面fib(n)的值计算出来了就不再计算,如果未计算出来,则计算出来后保存在Memo数组中,下次在调用fib(n)的时候就不会重新递归了。比如上面的递归树中在计算fib(6)的时候先计算fib(5),调用fib(5)算出了fib(4)后,fib(6)再调用fib(4)就不会在递归fib(4)的子树了,因为fib(4)的值已经保存在Memo[4]中。

 

②自底向上的动态规划

备忘录法还是利用了递归,上面算法不管怎样,计算fib(6)的时候最后还是要计算出fib(1),fib(2),fib(3)……,那么何不先计算出fib(1),fib(2),fib(3)……,呢?

这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。

public static int fib(int n)
{
        if(n<=0)
            return n;
        int []Memo=new int[n+1];
        Memo[0]=0;
        Memo[1]=1;
        for(int i=2;i<=n;i++)
        {
            Memo[i]=Memo[i-1]+Memo[i-2];
        }       
        return Memo[n];
}

自底向上方法也是利用数组保存了先计算的值,为后面的调用服务。观察参与循环的只有 i,i-1 , i-2三项,因此该方法的空间可以进一步的压缩如下。

public static int fib(int n)
    {
        if(n<=1)
            return n;

        int Memo_i_2=0;
        int Memo_i_1=1;
        int Memo_i=1;
        for(int i=2;i<=n;i++)
        {
            Memo_i=Memo_i_2+Memo_i_1;
            Memo_i_2=Memo_i_1;
            Memo_i_1=Memo_i;
        }       
        return Memo_i;
    }

由于备忘录方式的动态规划方法使用了递归,递归时会产生额外的开销,使用自底向上的动态规划方法要比备忘录方法好。
你以为看懂了上面的例子就懂得了动态规划吗?  那就too young too simple了。动态规划远远不止如此简单,下面先给出一个例子看看能否独立完成。然后再对动态规划的其他特性进行分析。
动态规划小试牛刀

例题:钢条切割




上面的例题来自于算法导论
关于题目的讲解就直接截图算法导论书上了这里就不展开讲。现在使用一下前面讲到三种方法来来实现一下。
①递归版本

public static int cut(int []p,int n)
    {
        if(n==0)
            return 0;
        int q=Integer.MIN_VALUE;
        for(int i=1;i<=n;i++)
        {
            q=Math.max(q, p[i-1]+cut(p, n-i));  
        }
        return q;
    }

递归很好理解,如果不懂可以看上面的讲解,递归的思路其实和回溯法是一样的,遍历所有解空间但这里和上面斐波拉契数列的不同之处在于,在每一层上都进行了一次最优解的选择,q=Math.max(q, p[i-1]+cut(p, n-i));这个段语句就是最优解选择,这里上一层的最优解与下一层的最优解相关。

 

②备忘录版本

public static int cutMemo(int []p)
    {
        int []r=new int[p.length+1];
        for(int i=0;i<=p.length;i++)
            r[i]=-1;                        
        return cut(p, p.length, r);
    }
    public static int cut(int []p,int n,int []r)
    {
        int q=-1;
        if(r[n]>=0)
            return r[n];
        if(n==0)
            q=0;
        else {
            for(int i=1;i<=n;i++)
                q=Math.max(q, cut(p, n-i,r)+p[i-1]);
        }
        r[n]=q;

        return q;
    }

有了上面求斐波拉契数列的基础,理解备忘录方法也就不难了。备忘录方法无非是在递归的时候记录下已经调用过的子函数的值。这道钢条切割问题的经典之处在于自底向上的动态规划问题的处理,理解了这个也就理解了动态规划的精髓。

 

③自底向上的动态规划

public static int buttom_up_cut(int []p)
    {
        int []r=new int[p.length+1];
        for(int i=1;i<=p.length;i++)
        {
            int q=-1;
            //①
            for(int j=1;j<=i;j++)
                q=Math.max(q, p[j-1]+r[i-j]);
            r[i]=q;
        }
        return r[p.length];
    }

自底向上的动态规划问题中最重要的是理解注释①处的循环,这里外面的循环是求r[1],r[2]……,里面的循环是求出r[1],r[2]……的最优解,也就是说r[i]中保存的是钢条长度为i时划分的最优解,这里面涉及到了最优子结构问题,也就是一个问题取最优解的时候,它的子问题也一定要取得最优解。下面是长度为4的钢条划分的结构图。我就偷懒截了个图。

 

动态规划原理

虽然已经用动态规划方法解决了上面两个问题,但是大家可能还跟我一样并不知道什么时候要用到动态规划。总结一下上面的斐波拉契数列和钢条切割问题,发现两个问题都涉及到了重叠子问题,和最优子结构。

①最优子结构

用动态规划求解最优化问题的第一步就是刻画最优解的结构,如果一个问题的解结构包含其子问题的最优解,就称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个很好的线索。使用动态规划算法时,用子问题的最优解来构造原问题的最优解。因此必须考查最优解中用到的所有子问题。

②重叠子问题

在斐波拉契数列和钢条切割结构图中,可以看到大量的重叠子问题,比如说在求fib(6)的时候,fib(2)被调用了5次,在求cut(4)的时候cut(0)被调用了4次。如果使用递归算法的时候会反复的求解相同的子问题,不停的调用函数,而不是生成新的子问题。如果递归算法反复求解相同的子问题,就称为具有重叠子问题(overlapping subproblems)性质。在动态规划算法中使用数组来保存子问题的解,这样子问题多次求解的时候可以直接查表不用调用函数递归。

 

动态规划的经典模型

线性模型

线性模型的是动态规划中最常用的模型,上文讲到的钢条切割问题就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。例1是一个经典的面试题,我们将它作为线性模型的敲门砖。

【例题1】在一个夜黑风高的晚上,有n(n <= 50)个小朋友在桥的这边,现在他们需要过桥,但是由于桥很窄,每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],时间为两个人过桥的总二者中时间长者。问所有小朋友过桥的总时间最短是多少。

每次过桥的时候最多两个人,如果桥这边还有人,那么还得回来一个人(送手电筒),也就是说N个人过桥的次数为2*N-3(倒推,当桥这边只剩两个人时只需要一次,三个人的情况为来回一次后加上两个人的情况…)。有一个人需要来回跑,将手电筒送回来(也许不是同一个人,realy?!)这个回来的时间是没办法省去的,并且回来的次数也是确定的,为N-2,如果是我,我会选择让跑的最快的人来干这件事情,但是我错了…如果总是跑得最快的人跑回来的话,那么他在每次别人过桥的时候一定得跟过去,于是就变成就是很简单的问题了,花费的总时间:

  • T = minPTime * (N-2) + (totalSum-minPTime)

来看一组数据 四个人过桥花费的时间分别为 1 2 5 10 (四人过桥速度不同!),按照上面的公式答案是19,但是实际答案应该是17。

具体步骤是这样的:

  • 第一步:1和2过去,花费时间2,然后1回来(花费时间1);
  • 第二歩:3和4过去,花费时间10,然后2回来(花费时间2);
  • 第三部:1和2过去,花费时间2,总耗时17。

所以之前的贪心想法是不对的。我们先将所有人按花费时间递增进行排序,假设前i个人过河花费的最少时间为opt[i],那么考虑前i-1个人过河的情况,即河这边还有1个人,河那边有i-1个人,并且这时候手电筒肯定在对岸,所以opt[i] = opt[i-1] + a[1] + a[i] (让花费时间最少的人把手电筒送过来,然后和第i个人一起过河)如果河这边还有两个人,一个是第i号,另外一个无所谓,河那边有i-2个人,并且手电筒肯定在对岸,所以opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2] (让花费时间最少的人把电筒送过来,然后第i个人和另外一个人一起过河,由于花费时间最少的人在这边,所以下一次送手电筒过来的一定是花费次少的,送过来后花费最少的和花费次少的一起过河,解决问题)
所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }

 

区间模型

区间模型的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。

【例题2】给定一个长度为n(n <= 1000)的字符串A,求插入最少多少个字符使得它变成一个回文串
典型的区间模型,回文串拥有很明显的子结构特征,即当字符串X是一个回文串时,在X两边各添加一个字符’a’后,aXa仍然是一个回文串,我们用d[i][j]来表示A[i…j]这个子串变成回文串所需要添加的最少的字符数,那么对于A[i] == A[j]的情况,很明显有 d[i][j] = d[i+1][j-1] (这里需要明确一点,当i+1 > j-1时也是有意义的,它代表的是空串,空串也是一个回文串,所以这种情况下d[i+1][j-1] = 0);当A[i] != A[j]时,我们将它变成更小的子问题求解,我们有两种决策:

  • 1、在A[j]后面添加一个字符A[i];
  • 2、在A[i]前面添加一个字符A[j];

根据两种决策列出状态转移方程为:

  • d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1; (每次状态转移,区间长度增加1)

空间复杂度O(n^2),时间复杂度O(n^2), 下文会提到将空间复杂度降为O(n)的优化算法。

 

背包模型

背包问题是动态规划中一个最典型的问题之一。由于网上有非常详尽的背包讲解,这里只将常用部分抽出来。

【例题3】有N种物品(每种物品1件)和一个容量为V的背包。放入第 i 种物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。f[i][v]表示前i种物品恰好放入一个容量为v的背包可以获得的最大价值。决策为第i个物品在前i-1个物品放置完毕后,是选择放还是不放,状态转移方程为:

  • f[i][v] = max{ f[i-1][v], f[i-1][v – Ci] +Wi }

时间复杂度O(VN),空间复杂度O(VN) (空间复杂度可利用滚动数组进行优化达到O(V) )。
动态规划题集整理

1、最长单调子序列
Constructing Roads In JG Kingdom★★☆☆☆
Stock Exchange ★★☆☆☆

2、最大M子段和
Max Sum ★☆☆☆☆
最长公共子串 ★★☆☆☆

3、线性模型
Skiing ★☆☆☆☆

 

总结

弄懂动态规划问题的基本原理和动态规划问题的几个常见的模型,对于解决大部分的问题已经足够了。希望能对大家有所帮助,转载请标明出处http://write.blog.csdn.net/mdeditor#!postId=75193592,创作实在不容易,这篇博客花了我将近一个星期的时间。
参考文献

1.算法导论

来源:CSDN
原文:https://blog.csdn.net/u013309870/article/details/75193592
 

 

典型问题(附代码)

01背包问题

背包九讲:http://www.cnblogs.com/jbelial/articles/2116074.html

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

  • f[i][v]=max{f[i-1][v], f[i-1][v-c[i]]+w[i]}。

将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

int main()
{
    //int m = 120;
    //int n = 5;
    //vector<int> w = { 0, 40, 50, 70, 40, 20 };
    //vector<int> v = { 0, 10, 25, 40, 20, 10 };

    int m, n;    //m重量,n数量
    while (cin >> m >> n)
    {
        vector<int> w(n + 1, 0);
        vector<int> v(n + 1, 0);
        for (int i = 1; i <= n; i++)
        {
            int tmp;
            cin >> tmp;
            w[i] = tmp;
        }
        for (int i = 1; i <= n; i++)
        {
            int tmp;
            cin >> tmp;
            v[i] = tmp;
        }
        vector< vector<int> > vec(n + 1, vector<int>(m + 1, 0));
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= m; j++)
            {
                if (w[i] > j)
                    vec[i][j] = vec[i - 1][j];
                else
                {
                    int tmp1 = v[i] + vec[i - 1][j - w[i]];
                    int tmp2 = vec[i - 1][j];
                    vec[i][j] = tmp1 > tmp2 ? tmp1 : tmp2;
                }
            }
        }
        double val = vec[n][m] * 0.1;
        cout << val << endl;
    }

    system("pause");
}

 

 

最长公共子序列(不连续) LCS  Longest Common Subsequence

找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。而最长公共子序列则并不要求连续。

cnblogs与belong,最长公共子序列为blog(cnblogs, belong),最长公共子串为lo(cnblogs, belong)

这两个问题都是用空间换空间,创建一个二维数组来记录之前的每个状态

参考:【动态规划】最长公共子序列与最长公共子串

C++实现最长公共子序列和最长公共子串

 

状态转移方程:

用i,j遍历两个子串x,y,如果两个元素相等就+1 ,不等就用上一个状态最大的元素

 

 1 public static int lcs(String str1, String str2) {
 2     int len1 = str1.length();
 3     int len2 = str2.length();
 4     int c[][] = new int[len1+1][len2+1];
 5     for (int i = 0; i <= len1; i++) {
 6         for( int j = 0; j <= len2; j++) {
 7             if(i == 0 || j == 0) {
 8                 c[i][j] = 0;
 9             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
10                 c[i][j] = c[i-1][j-1] + 1;
11             } else {
12                 c[i][j] = max(c[i - 1][j], c[i][j - 1]);
13             }
14         }
15     }
16     return c[len1][len2];
17 }

 

最长公共子串(连续)

状态转移方程:

区别就是因为是连续的,如果两个元素不等,那么就要=0了而不能用之前一个状态的最大元素

 

 1 public static int lcs(String str1, String str2) {
 2     int len1 = str1.length();
 3     int len2 = str2.length();
 4     int result = 0;     //记录最长公共子串长度
 5     int c[][] = new int[len1+1][len2+1];
 6     for (int i = 0; i <= len1; i++) {
 7         for( int j = 0; j <= len2; j++) {
 8             if(i == 0 || j == 0) {
 9                 c[i][j] = 0;
10             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
11                 c[i][j] = c[i-1][j-1] + 1;
12                 result = max(c[i][j], result);
13             } else {
14                 c[i][j] = 0;
15             }
16         }
17     }
18     return result;
19 } 

 

KMP

KMP算法

硬币找零问题

假设有几种硬币,如1 5 10 20 50 100,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。

解法:

用待找零的数值k描述子结构/状态,记作sum[k],其值为所需的最小硬币数。对于不同的硬币面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。对应于给定数目的找零total,需要求解sum[total]的值。

注意要从前往后算,从后往前算无法保存状态,需要递归,效率很低,就不是动态规划了

#include <iostream>
using namespace std;

#define MaxNum  pow(2,31) - 1

int main()
{
    int n;
    while (cin >> n)
    {
        vector<int> c(n + 1, 0);
        for (int i = 1; i <= n; i++)
        {
            if (i == 1 || i == 5 || i == 10 || i == 20 || i == 50 || i == 100)
            {
                c[i] = 1;
                continue;
            }
            int curMin = MaxNum;
            if (i - 1 > 0)
                curMin = c[i - 1] < curMin ? c[i - 1] : curMin;
            if (i - 5 > 0)
                curMin = c[i - 5] < curMin ? c[i - 5] : curMin;
            if (i - 10 > 0)
                curMin = c[i - 10] < curMin ? c[i - 10] : curMin;
            if (i - 20 > 0)
                curMin = c[i - 20] < curMin ? c[i - 20] : curMin;
            if (i - 50 > 0)
                curMin = c[i - 50] < curMin ? c[i - 50] : curMin;
            if (i - 100 > 0)
                curMin = c[i - 100] < curMin ? c[i - 100] : curMin;
            c[i] = curMin + 1;
        }
        cout << c[n] << endl;
    }
    

    system("pause");
    return 0;
}

 

 

类似硬币的问题找平方个数最小

题目:

给一个正整数 n, 找到若干个完全平方数(比如1, 4, 9, ... )使得他们的和等于 n。你需要让平方数的个数最少。
给出 n = 12, 返回 3 因为 12 = 4 + 4 + 4。
给出 n = 13, 返回 2 因为 13 = 4 + 9。

#include<iostream>  
using namespace std;

int findMin(int n)
{
    int *result = new int(n + 1);
    result[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        int minNum = i;
        for (int j = 1;; j++)
        {
            if (i >= j * j)
            {
                int tmp = result[i - j*j] + 1;
                minNum = tmp < minNum ? tmp : minNum;
            }
            else
                break;
        }
        result[i] = minNum;
    }
    return result[n];
}

int main()
{
    int n;
    while (cin >> n)
        cout << findMin(n) << endl;

}

 

 

最长回文字串

参考:最长回文子串

回文字符串的子串也是回文,比如P[i,j](表示以i开始以j结束的子串)是回文字符串,那么P[i+1,j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。这样需要额外的空间O(N^2),算法复杂度也是O(N^2)。

首先定义状态方程和转移方程:

P[i,j]=0表示子串[i,j]不是回文串。P[i,j]=1表示子串[i,j]是回文串。

P[i+1][j-1]&&s.at(i)==s.at(j)

初始化是准备两个元素是回文的情况aa,bb

 1 string findLongestPalindrome(string &s)
 2 {
 3     const int length=s.size();
 4     int maxlength=0;
 5     int start;
 6     bool P[50][50]={false};
 7     for(int i=0;i<length;i++)//初始化准备
 8     {
 9         P[i][i]=true;
10         if(i<length-1&&s.at(i)==s.at(i+1))
11         {
12             P[i][i+1]=true;
13             start=i;
14             maxlength=2;
15         }
16     }
17     for(int len=3;len<length;len++)//子串长度
18         for(int i=0;i<=length-len;i++)//子串起始地址
19         {
20             int j=i+len-1;//子串结束地址
21             if(P[i+1][j-1]&&s.at(i)==s.at(j))
22             {
23                 P[i][j]=true;
24                 maxlength=len;
25                 start=i;
26             }
27         }
28     if(maxlength>=2)
29         return s.substr(start,maxlength);
30     return NULL;
31 }

 

最长递增序列

问题:设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。

第一种方法,排序,然后用LCS来解决:设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。

第二种:时间复杂度O(N^2)的算法:

LIS[i]:表示数组前i个元素中(包括第i个),最长递增子序列的长度

LIS[i] = max{ LIS[i] , LIS[k]+1 }, 0 <= k < i, a[i]>a[k]

LIS数组的值表示前i个元素的最长子序列。i从第一个元素到最后一个元素遍历一遍,j从第一个元素到第i个元素遍历,如果第i个元素大于j,并且LIS[J] + 1比LIS[I]还大就更新,相当于把j加入到这个递增序列了

 1 int LIS(int a[], int length)
 2 {
 3     int *LIS = new int[length];
 4     for(int i = 0; i < length; ++i)
 5     {
 6         LIS[i] = 1; //初始化默认长度
 7         for(int j = 0; j < i; ++j) //前面最长的序列
 8             if(a[i] > a[j] && LIS[j]+1 > LIS[i])
 9                 LIS[i] = LIS[j]+1;  
10     }
11     int max_lis = LIS[0];
12     for(int i = 1; i < length; ++i)
13         if(LIS[i] > max_lis)
14             max_lis = LIS[i];
15     return max_lis;  //取LIS的最大值
16 }

 

字符串相似度/编辑距离(edit distance)

N皇后问题   https://blog.csdn.net/Scrat_Kong/article/details/85340720

其他问题

1.某幢大楼有100层。你手里有两颗一模一样的玻璃珠。当你拿着玻璃珠在某一层往下扔的时候,一定会有两个结果,玻璃珠碎了或者没碎。这幢大楼有个临界楼层。低于它的楼层,往下扔玻璃珠,玻璃珠不会碎,等于或高于它的楼层,扔下玻璃珠,玻璃珠一定会碎。玻璃珠碎了就不能再扔。现在让你设计一种方式,使得在该方式下,最坏的情况扔的次数比其他任何方式最坏的次数都少。也就是设计一种最有效的方式。

例如:有这样一种方式,第一次选择在60层扔,若碎了,说明临界点在60层及以下楼层,这时只有一颗珠子,剩下的只能是从第一层,一层一层往上实验,最坏的情况,要实验59次,加上之前的第一次,一共60次。若没碎,则只要从61层往上试即可,最多只要试40次,加上之前一共需41次。两种情况取最多的那种。故这种方式最坏的情况要试60次。仔细分析一下。如果不碎,我还有两颗珠子,第二颗珠子会从N+1层开始试吗?很显然不会,此时大楼还剩100-N层,问题就转化为100-N的问题了。

那该如何设计方式呢?

根据题意很容易写出状态转移方程:N层楼如果从n层投下玻璃珠,最坏的尝试次数是:clip_image002[6]

那么所有层投下的最坏尝试次数的最小值即为问题的解:clip_image002[8]。其中F(1)=1.

 1 /*
 2 *
 3 *功能:100楼层抛珠问题
 4 */
 5 #include<iostream>
 6 using namespace std;
 7 
 8 int max(int a, int b)
 9 {
10     return (a > b)? a : b;
11 }
12 
13 int dp[101];
14 //N<=100;
15 int floorThr(int N)
16 {
17     for (int i = 2; i <= N; i++)
18     {
19         dp[i] = i;
20         for (int j = 1; j<i; j++)
21         {
22             int tmp = max(j, 1 + dp[i - j]);    //j的遍历相当于把每层都试一遍
23             if (tmp<dp[i])
24                 dp[i] = tmp;
25         }
26     }
27     return dp[N];
28 }
29 
30 int main()
31 {
32     dp[0] = 0;
33     dp[1] = 1;
34     int dis = floorThr(100);
35     cout << dis << endl;
36     system("Pause");
37 }

 

输出为14,说明在合适的楼层抛玻璃珠,最差情况下只需14次可找到临界层。

答案是先从14楼开始抛第一次;如果没碎,再从27楼抛第二次;如果还没碎,再从39楼抛第三次;如果还没碎,再从50楼抛第四次;如此,每次间隔的楼层少一层。这样,任何一次抛棋子碎时,都能确保最多抛14次可以找出临界楼层。

N*N方格内的走法问题

 

 1 #include<iostream>
 2 #include<vector>
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     int n;
 8     while (cin >> n)
 9     {
10         vector<vector<int>> dp(n+1, vector<int>(n+1, 1));
11         for (int i = 1; i <= n;i++)
12         {
13             for (int j = 1; j <= n;j++)
14             {
15                 dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
16             }
17         }
18         cout << dp[n][n] << endl;
19     }
20 }

 

 

其他问题参考:

http://www.cnblogs.com/wuyuegb2312/p/3281264.html#q1a1

http://www.cnblogs.com/luxiaoxun/archive/2012/11/15/2771605.html

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值