动态规划例题

安稳与你 提交于 2020-02-11 02:15:59

这里记录一些动态规划的例题,不断积累,不断补充。

1、不等式数列

题目描述:

对1到n这n个整数进行全排列,在每一个排列中每两个相邻的整数之间插入>或<,使得不等式成立。现要求<的个数为k,即>的个数为n-k-1,求问满足要求的排列个数。例如,现在给定n=3,k=1,则满足要求的排列有1<3>2,2>1<3,2<3>1,3>1<2这四种情况,即结果为4。参数范围K < n <= 1000。

解题思路:

暴力搜索复杂度为n!,肯定是不行的。虽然可以进行一定程度的剪枝,比如在遍历每一个位置的数字时,判断当前<是否已经用完,但这只是杯水车薪。遇到这种问题,第一反应肯定是需要使用动态规划,当然难点就在于如何构造递推公式。针对本题目,我们设定dp[i][j]为在n=i,k=j的情况下的排列数,则递推公式为

dp[i][j] = dp[i - 1][j - 1] * (i - j) + dp[i - 1][j] * (j + 1)

怎么理解这个公式呢?其实就要考虑如何从i-1过渡到i。当n从i-1增加到i,新增的数字i要比之前的1到i-1都大,所以无论把i插入到哪个位置,它都会比左右两个数字a和b都大。假如a<b,将i插入到两数之间,不等式变为a<i>b,小于号<数量不变;如果a>b,不等式变为a<i>b,小于号<数量加1。所以为了求解dp[i][j],就要考虑dp[i-1][j]和dp[i-1][j-1]两种情况。对于dp[i-1][j],小于号数量已经足够,因此数字i只能插入到之前排列中前数小于后数的位置,即小于号插入的位置,再加上第一个数前面的位置,所以总数为j+1。对于dp[i-1][j-1],小于号还差一个,所以数字i应该插入到先前排列中大于号的位置,在加上最后一个数后面一个位置,所以总数为i-j-1+1,即i-j。

示例代码:

#include <iostream>
#include <vector>

using namespace std;

int main() {
    int numCount, K;
    cin >> numCount >> K;
    vector<vector<int>> dp(numCount + 1, vector<int>(K + 1));    //当k=0时,只有递增序这一种排列。
    for (int i = 1; i <= numCount; i++) {
        dp[i][0] = 1;
    }
    for (int i = 2; i <= numCount; i++) {
        for (int j = 1; j <= K; j++) {
            dp[i][j] = dp[i - 1][j] * (j + 1) + dp[i - 1][j - 1] * (i - j);
        }
    }

    cout << dp[numCount][K];
    return 0;
}

 2、Ugly Number

题目描述:

Write a program to find the nth super ugly number.

Super ugly numbers are positive numbers whose all prime factors are in the given prime list primes of size k. For example, [1, 2, 4, 7, 8, 13, 14, 16, 19, 26, 28, 32] is the sequence of the first 12 super ugly numbers given primes = [2, 7, 13, 19] of size 4.

Note:
(1) 1 is a super ugly number for any given primes.
(2) The given numbers in primes are in ascending order.
(3) 0 < k ≤ 100, 0 < n ≤ 106, 0 < primes[i] < 1000.
(4) The nth super ugly number is guaranteed to fit in a 32-bit signed integer.

题目来源:

LeetCode 313

解题思路:

题目要求找到第n个符合要求的正整数,一个很明显的规律就是,如果已知前i-1个ugly number,则第i个ugly number一定是由前面i-1个数其中一个再乘以某一个素数而得到。所以,我们只要从第一个开始按序求出所有ugly number,直到求出第n个即可。

为了求第i个ugly number,一个无脑的办法就是,将前面所有的i-1个数依次跟所有的素数求乘积,然后取出比前i-1个ugly number都大的数中最小的一个即可。这样做很显然浪费了很多计算,但浪费在哪里?对于每一个素数,我们没有必要跟所有的前i-1个ugly number都做乘积,我们的目的是求出最小的一个,所以对于每一个素数,我们也应该只求出一个最符合要求的乘积(比前i-1个ugly number大,且是最小的一个),然后对比所有素数求出来的乘积,找出最小一个即可。

假设素数集合primes的大小为k,我们维护一个k大小的数组next,next[i]表示我们前面所说的针对第i个素数最符合要求的那个乘积,每次求下一个ugly number时,从next数组中找出最小值即可,假设为minValue,下标为p。这时,我们需要更新next数组,注意到新的ugly number是目前next中最小的,所以我们只需要更新next数组中等于minValue的值。怎样更新?minValue一定等于primes[p]乘以前面某一个ugly number,假设是第d个,我们只要把它更新为primes[p]乘以第d+1个ugly number就可以了。最后还要注意,next宿主中是可能存在相同值的。

示例代码:

int nthSuperUglyNumber(int n, vector<int>& primes) {
        vector<int> index(primes.size());
        vector<int> next(primes.size(), 1);
        vector<int> ret;
        ret.reserve(n);
        
        while(ret.size() < n){
            int tmpmin = next[0];
            for(int i = 1; i < primes.size(); i++){
                tmpmin = tmpmin < next[i] ? tmpmin : next[i];
            }
            ret.push_back(tmpmin);
            for(int i = 0; i < primes.size(); i++){
                if(next[i] == tmpmin)next[i] = primes[i] * ret[index[i]++];
            }
        }
        
        return ret[n-1];
}

 3、Burst Balloons

题目描述:

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.

Find the maximum coins you can collect by bursting the balloons wisely.

Note: 
(1) You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
(2) 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

Example:

Given [3, 1, 5, 8]

Return 167

题目来源:

LeetCode 312

解题思路:

这道题的中心思想其实是分治法,只是使用了动态规划作为解题工具。我们把nums[-1]和nums[n]都当做气球,只是它们不能被打破,因此可以把它们当做边界,那么现在问题就是在已知左右边界的情况下,求边界内所有气球的一个最优解。接下来拆分子问题,就要找到子问题的左右边界。什么样的气球可以当做边界?注意最后一个被打破的气球,假设为nums[i],在其他气球都被打破之前,nums[i]都不会被打破,就相当于它也跟nums[-1]和nums[n]一样是不可被打破的,因此它就是子问题的边界。也就是说nums[-1]、nums[i]、nums[n]将原问题划分为左右两个子问题。由于nums[i]划分了界限,则左右两边的气球无论以什么样的顺序求乘积,都不会互相影响。在解决了两个子问题之后,接下来的归并步骤只需要将两边加起来,再加上打破nums[i]的得分即可。总结起来,得到如下公式:f(left,right) = f(left, i - 1) + f(i + 1, right) + nums[left - 1] * nums[i] * nums[right + 1].

接下来是对于子问题求最优解。我们依次将每一个气球当做最后一个被打破的,这样将子问题又划分为更小的子问题,找到使得得分最大的划分方法即作为最优解。这样看起来是一个递归的过程,而过程中存在大量的重复运算,因此显而易见要用到动态规划。

示例代码:

int maxCoins(vector<int>& nums) {
        vector<int> bullons(nums.size() + 2);
        bullons[0] = bullons[nums.size() + 1] = 1;
        for(int i = 0; i < nums.size(); i++){
            bullons[i + 1] = nums[i];
        }
        
        vector<vector<int>> dp(nums.size() + 2, vector<int>(nums.size() + 2));
        for(int count = 1; count <= nums.size(); count++){
            for(int begin = 1; begin <= nums.size() - count + 1; begin++){
                int end = begin + count - 1;
                for(int index = begin; index <= end; index++){
                    dp[begin][end] = max(dp[begin][end], dp[begin][index - 1] + dp[index + 1][end] + bullons[index] * bullons[begin - 1] * bullons[end + 1]);
                }
            }
        }
        
        return dp[1][nums.size()];
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!