• 动态规划笔试题


    1、最长公共子序列、最长公共子串

    最长公共子序列(Longest-Common-Subsequence,LCS)

    dp[i][j]:dp[i][j]表示长度分别为i和j的序列X和序列Y构成的LCS的长度 dp[i][j] = 0,如果i=0 或 j=0 dp[i][j] = dp[i-1][j-1] + 1,如果 X[i-1] = Y[i-1] dp[i][j] = max{ dp[i-1][j], dp[i][j-1] },如果 X[i-1] != Y[i-1] LCS长度为 dp[Xlen][Ylen]

    View Code
    int dp[100][100];  // 存储LCS长度, 下标i,j表示序列X,Y长度 
    void LCS_dp(char * X, char * Y) 
    {
        int i, j;     
        int xlen = strlen(X);     
        int ylen = strlen(Y);       
        // dp[0-xlen][0] & dp[0][0-ylen] 都已初始化0     
        for(i = 1; i <= xlen; ++i)     
        {        
            for(j = 1; j <= ylen; ++j)     
            {             
                if(X[i-1] == Y[j-1]) 
                {                 
                    dp[i][j] = dp[i-1][j-1] + 1;             
                }
                else if(dp[i][j-1] > dp[i-1][j])             
                {                 
                    dp[i][j] = dp[i][j-1];             
                }
                else             
                {
                    dp[i][j] = dp[i-1][j];
                }         
            }     
        }     
        printf("len of LCS is: %d\n", dp[xlen][ylen]); 
        i = xlen; 
        j = ylen; 
        int k = dp[i][j]; 
        char lcs[100] = {'\0'}; 
        while(i && j) 
        {     
            if(X[i-1] == Y[j-1] && dp[i][j] == dp[i-1][j-1] + 1)     
            {         
                lcs[--k] = X[i-1];         
                --i; --j;     
            }
            else if(X[i-1] != Y[j-1] && dp[i-1][j] > dp[i][j-1])     
            {        
                --i;     
            }
            else     
            {         
                --j;     
            } 
        } 
        printf("%s\n",lcs);
    }

    最长公共子串(Longest-Common-Substring,LCS)

    dp[i][j]:表示X[0-i]与Y[0-j]的最长公共子串长度 dp[i][j] = dp[i-1][j-1] + 1,如果 X[i] == Y[j] dp[i][j] = 0,如果 X[i] != Y[j] 初始化:i==0或者j==0,如果X[i] == Y[j],dp[i][j] = 1;否则dp[i][j] = 0。

    最长公共子串的长度为max(dp[i][j])。

    View Code
    // 最长公共子串 DP
    int dp[100][100];
    void LCS_dp(char * X, char * Y) 
    {
        int xlen = strlen(X);
        int ylen = strlen(Y);
        int maxlen = 0;
        int maxindex = 0;     
        for(int i = 0; i < xlen; ++i)
        {
            for(int j = 0; j < ylen; ++j)
            {
                if(X[i] == Y[j])
                {
                    if(i && j)
                    {
                        dp[i][j] = dp[i-1][j-1] + 1;
                    }
                    if(i == 0 || j == 0)
                    {
                        dp[i][j] = 1;
                    }
                    if(dp[i][j] > maxlen)
                    {
                        maxlen = dp[i][j];
                        maxindex = i + 1 - maxlen;
                    }
                }
            }
        }
        if(maxlen == 0)
        {
            printf("NULL LCS\n");
            return;
        }
        printf("The len of LCS is %d\n",maxlen);
        int i = maxindex;
        while(maxlen--)
        {
            printf("%c",X[i++]);
        }
        printf("\n"); 
    }

    2、数组中最长递增子序列:如在序列1,-1,2,-3,4,-5,6,-7中,最长递增序列为1,2,4,6。

    时间复杂度O(N^2)的算法: LIS[i]:表示数组前i个元素中(包括第i个),最长递增子序列的长度 LIS[i] = max{ 1, LIS[k]+1 }, 0 <= k < i, a[i]>a[k]

    View Code
    int LIS(int a[], int length)
    {
        int *LIS = new int[length];
        for(int i = 0; i < length; ++i)
        {
            LIS[i] = 1; //初始化默认长度
            for(int j = 0; j < i; ++j) //前面最长的序列
                if(a[i] > a[j] && LIS[j]+1 > LIS[i])
                    LIS[i] = LIS[j]+1;  
        }
        int max_lis = LIS[0];
        for(int i = 1; i < length; ++i)
            if(LIS[i] > max_lis)
                max_lis = LIS[i];
        return max_lis;  //取LIS的最大值
    }

    时间复杂度O(NlogN)的算法: 辅助数组b[],用k表示数组b[]目前的长度,算法完成后k的值即为LIS的长度。 初始化:b[0] = a[0],k = 1 从前到后扫描数组a[],对于当前的数a[i],比较a[i]和b[k-1]: 如果a[i]>b[k-1],即a[i]大于b[]最后一个元素,b[]的长度增加1,b[k++]=a[i]; 如果a[i]<b[k-1],在b[1]...b[k]中二分查找第一个大于a[i]的数b[j],修改b[j]=a[i]。 LIS的长度为k

    View Code
    //修改的二分搜索算法,若要查找的数w在长为len的数组b中存在则返回下标
    //若不存在,则返回b数组中的第一个大于w的那个元素的下标
    int BiSearch(int *b, int len, int w)
    {
        int left = 0, right = len-1;
        int middle;
        while(left <= right)
        {
            middle = (left+right)/2;
            if(b[middle] > w)
                right = middle - 1;
            else if(b[middle] < w)
                left = middle + 1;
            else
                return middle;
        }
    
        //返回b数组中的刚刚大于w的那个元素的下标
        return (b[middle]>w) ? middle : middle+1;
    }
    
    int LIS(int *array, int n)
    {
        int *B = new int[n];
        int len = 1;
        B[0] = array[0];
    
        for(int i=1; i<n; ++i)
        {
            if(array[i] > B[len-1])
            {
                B[len] = array[i];
                ++len;
            }
            else
            {
                int pos = BiSearch(B, len, array[i]);
                B[pos] = array[i];
            }
        }
        delete []B;
        return len;
    }

    3、计算字符串的相似度(编辑距离)

    为了判断字符串的相似程度,定义了一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为: 1.修改一个字符。2.增加一个字符。3.删除一个字符。

    比如,对于“abcdefg”和“abcdef”两个字符串来说,可以通过增加/减少一个“g“的方式来达到目的。上面的两种方案,都仅需要一次操作。把这个操作所需要的次数定义为两个字符串的距离,给定任意两个字符串,写出一个算法来计算出它们的距离。

    设 L(i,j)为使两个字符串和Ai和Bj相等的最小操作次数。 当ai==bj时 显然 L(i,j) = L(i-1,j-1) 当ai!=bj时 L(i,j) = min( L(i-1,j-1), L(i-1,j), L(i,j-1) ) + 1

    View Code
    int minValue(int a, int b, int c)
    {
        int t = a <= b ? a:b;
        return t <= c ? t:c;
    }
    
    int calculateStringDistance(string strA, string strB)
    {
        int lenA = (int)strA.length()+1;
        int lenB = (int)strB.length()+1;
    
        int **c = new int*[lenA];
        for(int i = 0; i < lenA; i++)
            c[i] = new int[lenB];
    
        for(int i = 0; i < lenA; i++) c[i][0] = i;
        for(int j = 0; j < lenB; j++) c[0][j] = j;
        c[0][0] = 0;
        for(int i = 1; i < lenA; i++)
        {
            for(int j = 1; j < lenB; j++)
            {
                if(strB[j-1] == strA[i-1])
                    c[i][j] = c[i-1][j-1];
                else
                    c[i][j] = minValue(c[i][j-1], c[i-1][j], c[i-1][j-1]) + 1;
            }
        }
    
        int ret =  c[lenA-1][lenB-1];
    
        for(int i = 0; i < lenA; i++)
            delete [] c[i];
        delete []c;
    
        return ret;
    }

    4、8*8的棋盘上面放着64个不同价值的礼物,每个小的棋盘上面放置一个礼物(礼物的价值大于0),一个人初始位置在棋盘的左上角,每次他只能向下或向右移动一步,并拿走对应棋盘上的礼物,结束位置在棋盘的右下角,请设计一个算法使其能够获得最大价值的礼物。

    动态规划算法:   dp[i][j] 表示到棋盘位置(i,j)上可以得到的最大礼物值   dp[i][j] = max( dp[i][j-1] , dp[i-1][j] ) + value[i][j]  (0<i,j<n) 

    View Code
    int GetMaxValue(int **dp, int **value)
    {
        int i, j, n = 8;  
        dp[0][0] = value[0][0];  
        for(i = 1; i < n; i++)  
        {  
            dp[i][0] = dp[i-1][0] + value[i][0];  
        }  
        for(j = 1; j < n; j++)  
        {  
            dp[0][j] = dp[0][j-1] + value[0][j];  
        }  
    
        for(i = 1; i < n; i++)  
        {  
            for(j = 1; j < n; j++)  
            {  
                dp[i][j] = max(dp[i][j-1] , dp[i-1][j]) + value[i][j];  
            }  
        }  
        return dp[n-1][n-1];
    }

    5、给定一个整数数组,求这个数组中子序列和最大的最短子序列,如数组a[]={1,2,2,-3,-5,5}子序列和最大为5,最短的为a[5]。

    动态规划   sum[i] = max(sum[i-1]+a[i], a[i]) (sum[0]=a[0],1<=i<=n)  len[i] = max(len[i-1]+1, 0) (len[0]=0,1<=i<=n)

    View Code
    void max_sub(int a[], int size)
    {
        int *sum = new int[size];
        int *len = new int[size];
        int temp_sum = 0;
    
        sum[0] = a[0];
        len[0] = 0;
        for(int i = 1; i < size; i++)
        {
            temp_sum = sum[i-1] + a[i];
            if(temp_sum > a[i])
            {
                sum[i] = temp_sum;
                len[i] = len[i-1]+1;
            }
            else
            {
                sum[i] = a[i];
                len[i] = 0;
            }
        }
        int index = 0;
        for(int i = 1; i < size; i++)
        {
            if(sum[i] > sum[index])
                index = i;
            else if(sum[i] == sum[index] && len[i] < len[index])
                index = i;
        }
        printf("Max sub sum is %d, from %d to %d",sum[index],index-len[index],index);
    
        delete []sum;
        delete []len;
    }

    6、子数组的最大和

    状态方程: Start[i] = max{A[i], Start[i-1]+A[i]} All[i] = max{Start[i], All[i-1]}

    View Code
    int MaxSum(int *A, int n)
    {
        int * All = new int[n];
        int * Start = new int[n];
    
        All[0] = Start[0] = A[0];
        for(int i=1; i<n; ++i)
        {
            Start[i] = max(A[i], A[i]+Start[i-1]);
            All[i] = max(Start[i], All[i-1]);
        }
        int max = All[n-1];
        delete []All;
        delete []Start;
        return max;
    }

    因为Start[i-1]只在计算Start[i]时使用,而且All[i-1]也只在计算All[i]时使用,所以可以只用两个变量就够了,节省空间。

    View Code
    int MaxSum(int *A, int n)
    {
        int All = A[0];
        int Start = A[0];
        for(int i=1; i<n; ++i)
        {
            Start = max(A[i], A[i]+Start);
            All = max(Start, All);
        }
        return All;
    }

    7、在数组中,数字减去它右边的数字得到一个数对之差。求所有数对之差的最大值。例如在数组{2, 4, 1, 16, 7, 5, 11, 9}中,数对之差的最大值是11,是16减去5的结果。

    思路:假设f[i]表示数组中前i+1个数的解,前i+1个数的最大值为m[i]。则状态转移方程: f[i] = max(f[i-1], m[i-1] - a[i]), m[i] = max(m[i-1],a[i])。问题的解为f[n-1]。

    View Code
    int MaxDiff_Solution1(int *pArray, int nLen)  
    {  
        if(pArray == NULL || nLen <= 1)  
            return 0;  
        int *f = new int[nLen];  
        int *m = new int[nLen];  
      
        f[0] = 0;          //1个数的情况   
        m[0] = pArray[0];  
        for(int i = 1; i < nLen; i++)  
        {  
            f[i] = max(f[i-1], m[i-1] - pArray[i]);  
            m[i] = max(m[i-1], pArray[i]);  
        }  
        return f[nLen - 1];  
    }

    上述代码用了两个辅助数组,其实只需要两个变量,前i个数的情况只与前i-1个数的情况有关。在“子数组的最大和问题”中,也使用过类似的技术。

    View Code
    int MaxDiff_Solution2(int *pArray, int nLen)  
    {
        if(pArray == NULL || nLen <= 1)  
            return 0;  
        int f = 0;  
        int m = pArray[0];  
        for(int i = 1; i < nLen; i++)  
        {  
            f = max(f, m - pArray[i]);  
            m = max(m, pArray[i]);  
        }  
        return f;  
    }

    8、从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的。

    双端 LIS 问题,用 DP 的思想可解,目标规划函数 max{ b[i] + c[i] - 1 }, 其中 b[i] 为从左到右,0--i 个数之间满足递增的数字个数;c[i] 为从右到左,n-1--i个数之间满足递增的数字个数。最后结果为 n-max 。

    View Code
    /* 
    a[] holds the original numbers
    b[i] holds the number of increasing numbers from a[0] to a[i]
    c[i] holds the number of increasing numbers from a[n-1] to a[i]
    */
    int double_lis(int a[], int n)
    {
        int *b = new int[n];
        int *c = new int[n];
    
        // update array b from left to right
        for(int i = 0; i < n; ++i)
        {
            b[i] = 1;
            for(int j = 0; j < i; ++j)
                if(a[i] > a[j] && b[j]+1 > b[i])
                    b[i] = b[j] + 1;
        }
    
        // update array c from right to left
        for (int i = n-1; i >= 0; --i)
        {
            c[i] = 1;
            for(int j = n-1; j > i; --j)
                if(a[i] > a[j] && c[j]+1 > c[i])
                    c[i] = c[j] + 1;
        }
    
        int max = 0;
        for (int i = 0; i < n; ++i )
        {
            if (b[i]+c[i] > max)
                max = b[i] + c[i];
        }
    
    
        max = max-1; //delete the repeated one
        delete []b;
        delete []c;
    
        return n-max;
    }

    9、从给定的N个正数中选取若干个数之和最接近M

    解法:转换成01背包问题求解,从正整数中选取若干个数放在容量为M的背包中。

    View Code
    #include <stdio.h>
    
    const int MAX = 10010;
    int f[MAX];
    int g[MAX][MAX];
    
    int main()
    {
        //从数组value中选中若干个数之和最接近V
        int value[] = {2,9,5,7,4,11,10};
        int V = 33;   //子集和
        int N = sizeof(value)/sizeof(value[0]);
    
        for(int i = 0; i <= V; ++i)  //初始化:没要求和一定是V
        {
            f[i] = 0;
        }
        for(int i = 0; i < N; ++i)
        {
            for(int v = V; v >= value[i]; --v)
            {
                if(f[v] < f[v-value[i]] + value[i] ) //选value[i]
                {
                    f[v] = f[v-value[i]] + value[i];
                    g[i][v] = 1;
                }
                else         //不选value[i]
                {
                    f[v] = f[v]; 
                    g[i][v] = 0;
                }
            }
        }
        printf("%d\n",f[V]);
    
        int i = N; //输出解
        int v = V;
        while(i-- > 0)
        {
            if(g[i][v] == 1)
            {
                printf("%d, ",value[i]);
                v -= value[i];
            }
        }
        printf("\n");
        return 0;
    }

    从给定的N个正数中选取若干个数之和为M

    View Code
    #include <iostream>
    #include <list>
    using namespace std;
    
    void find_seq(int sum, int index, int * value, list<int> & seq)
    {
        if(sum <= 0 || index < 0) return;
        if(sum == value[index])
        {
            printf("%d ", value[index]);
            for(list<int>::iterator iter = seq.begin(); iter != seq.end(); ++iter)
            {
                printf("%d ", *iter);
            }
            printf("\n");
        }
        seq.push_back(value[index]); 
        find_seq(sum-value[index], index-1, value, seq); //放value[index]
        seq.pop_back();
        find_seq(sum, index-1, value, seq); //不放value[index]
    }
    
    int main()
    {
        int M;
        list<int> seq;
        int value[] = {2,9,5,7,4,11,10};
        int N = sizeof(value)/sizeof(value[0]);
        for(int i = 0; i < N; ++i)
        {
            printf("%d ",value[i]);
        }
        printf("\n");
        scanf("%d", &M);
        printf("可能的序列:\n");
        find_seq(M, N-1, value, seq);
    
        return 0;
    }

    10、将一个较大的钱,不超过1000的人民币,兑换成数量不限的100、50、10、5、2、1的组合,请问共有多少种组合呢?

    解法:01背包中的完全背包问题(即每个物品的数量无限制) dp[i][j]:表示大小为j的价值用最大为money[i]可表示的种类数

    View Code
    #define NUM 7
    int money[NUM] = {1, 2, 5, 10, 20, 50, 100};  
    // 动态规划解法(完全背包)   
    int NumOfCoins(int value)  
    {  
        int dp[7][1010];  
        for(int i = 0; i <= value; ++i)
            dp[0][i] = 1;  
      
        for(int i = 1; i < NUM; ++i)
        {
            for(int j = 0; j <= value; ++j)  
            {
                if(j >= money[i])  
                    dp[i][j] = dp[i][j-money[i]] + dp[i-1][j];  
                else  
                    dp[i][j] = dp[i-1][j];  
            }  
        }  
        return dp[6][value];
    }

    11、捞鱼问题:20个桶,每个桶中有10条鱼,用网从每个桶中抓鱼,每次可以抓住的条数随机,每个桶只能抓一次,问一共抓到180条的排列有多少种。

    分析:看看这个问题的对偶问题,抓取了180条鱼之后,20个桶中剩下了20条鱼,不同的抓取的方法就对应着这些鱼在20个桶中不同的分布,于是问题转化为将20条鱼分到20个桶中有多少中不同的分类方法(这个问题当然也等价于180条鱼分到20个桶中有多少种不同的方法)。

    dp[i][j]:前i个桶放j条鱼的方法共分为11种情况:前i-1个桶放j-k(0<=k<=10)条鱼的方法总和。我们可以得到状态方程:f(i,j) = sum{ f(i-1,j-k), 0<=k<=10}

    View Code
    /*捞鱼:将20条鱼放在20个桶中,每个桶最多可以放10条,求得所有的排列方法
    /*自底向上DP f(i,j) = sum{ f(i-1,j-k), 0<=k<=10 }
    /*该方法中测试 20个桶 180条鱼,与递归速度做对比
    */
    void CatchFish()
    {
        int dp[21][200]; // 前i个桶放j条鱼的方法数
        int bucketN = 20;
        int fishN = 20;
        memset(dp,0,sizeof(dp));
        for(int i = 0; i <= 10; ++i)  // 初始化合法状态
        {
            dp[1][i] = 1;
        }
        for(int i = 2; i <= bucketN; ++i)  // 从第二个桶开始
        {
            for(int j = 0; j <= fishN; ++j)
            {
                for(int k = 0; k <= 10 && j-k >= 0; ++k)
                {
                    dp[i][j] += dp[i-1][j-k];
                }
            }
        }
        printf("%d\n",dp[bucketN][fishN]);
    }

    12、n个骰子的点数:把n个骰子扔在地上,所有骰子朝上一面的点数之和为S。输入n,打印出S的所有可能的出现的值。

    F(k,n) 表示k个骰子点数和为n的种数,k表示骰子个数,n表示k个骰子的点数和 对于 k>0, k<=n<=6*k F(k,n) = F(k-1,n-6) + F(k-1,n-5) + F(k-1,n-4) + F(k-1,n-3) + F(k-1,n-2) + F(k-1,n-1)  对于 n<k or n>6*k F(k,n) = 0 当k=1时, F(1,1)=F(1,2)=F(1,3)=F(1,4)=F(1,5)=F(1,6)=1

    View Code
    void SumOfDices()
    {
        int dp[21][6*20+1]; // k个骰子,和为n的种类数,不超过20个骰子
        int number = 3;  // 骰子数
        int face = 6;   // 面数,6面
        memset(dp,0,sizeof(dp));
        for(int i = 1; i <= 6; ++i)  // 初始化1个骰子的情况
        {
            dp[1][i] = 1;
        }
        for(int i = 2; i <= number; ++i)  // 从第二个骰子开始
        {
            for(int j = i; j <= face * i; ++j) // i个骰子的点数从i到i*6
            {
                for(int k = 1; k <= face && j-k >= 0; ++k)
                {
                    dp[i][j] += dp[i-1][j-k];
                }
            }
        }
        for(int i = 0; i <= number * face; ++i)
        {
            printf("Sum = %d, Number is %d\n",i,dp[number][i]);
        }
    }

    13、给定三个字符串A,B,C;判断C能否由AB中的字符组成,同时这个组合后的字符顺序必须是A,B中原来的顺序,不能逆序;例如:A:mnl,B:xyz;如果C为mnxylz,就符合题意;如果C为mxnzly,就不符合题意,原因是z与y顺序不是B中顺序。

    DP求解:定义dp[i][j]表示A中前i个字符与B中前j个字符是否能组成C中的前(i+j)个字符,如果能标记true,如果不能标记false; 有了这个定义,我们就可以找出状态转移方程了,初始状态dp[0][0] = 1: dp[i][j] = 1 如果 dp[i-1][j] == 1 && C[i+j-1] == A[i-1] dp[i][j] = 1 如果 dp[i][j-1] == 1 && C[i+j-1] == B[j-1]

    View Code
    #include <iostream>
    using namespace std;
    
    char A[201]; 
    char B[201];
    char C[401];    
    int dp[201][201];   // dp[i][j] 表示A前i个字符与B前j个字符是否能构成C前i+j个字符
    
    int main() 
    {
        memset(dp,0,sizeof dp); 
        scanf("%s %s %s", A, B, C); 
        int lenA = strlen(A); 
        int lenB = strlen(B);
        dp[0][0] = 1;       
        for(int i = 0; i <= lenA; ++i) 
        {
            for(int j = 0; j <= lenB; ++j) 
            {
                if(i > 0 && (dp[i-1][j] == 1) && (C[i+j-1] == A[i-1])) 
                { 
                    dp[i][j] = 1;
                }
                if(j > 0 && (dp[i][j-1] == 1) && (C[i+j-1] == B[j-1]))
                { 
                    dp[i][j] = 1; 
                } 
            }
        }
        printf("%s\n",dp[lenA][lenB] ? "yes" : "no");
        return 0; 
    }

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

  • 相关阅读:
    【bzoj4897】[Thu Summer Camp2016]成绩单 区间dp
    【bzoj3533】[Sdoi2014]向量集 线段树+STL-vector维护凸包
    【bzoj2121】字符串游戏 区间dp
    【bzoj2741】[FOTILE模拟赛]L 可持久化Trie树+分块
    【bzoj1067】[SCOI2007]降雨量 倍增RMQ
    【bzoj5197】[CERC2017]Gambling Guide 期望dp+堆优化Dijkstra
    【codeforces914G】Sum the Fibonacci FWT+FST(快速子集变换)
    【bzoj4305】数列的GCD 组合数学+容斥原理
    【bzoj5180】[Baltic2016]Cities 斯坦纳树
    【bzoj5178】[Jsoi2011]棒棒糖 主席树
  • 原文地址:https://www.cnblogs.com/liangzh/p/3097510.html
Copyright © 2020-2023  润新知