动态规划入门3-滚动数组

Minimum Partition

题目

http://www.lintcode.com/zh-cn/problem/minimum-partition/

分析

mp(n, sum1):子集1的和为sum1时,对第n个数进行操作后的最小划分和。

当子集和为sum1时,对第n个数进行划分,它可能被划分到第一个子集中,也可能被划分到第二个子集中。我们需要处理的是,选择最小的划分。

如果将第n个数添加进子集1中,子集1的和为sum1+arr[n-1]。此时需要对前n-1个数进行选择划分,即为状态mp(n-1, sum1+arr[n-1])。

如果将第n个数加入子集2中,子集1的和不变,为sum1,此时状态为mp(n-1, sum1)。

边界情况为n等于0时,此时不需要对数进行选择,只需完成基本操作,即将子集和1与子集和2的绝对差返回。

mp(n,sum1)={abs(sum2sum1)min(mp(n1,sum1+arr[n1]),mp(n1,sum1))if n=0others(1) (1) m p ( n , s u m 1 ) = { a b s ( s u m − 2 ∗ s u m 1 ) if  n = 0 m i n ( m p ( n − 1 , s u m 1 + a r r [ n − 1 ] ) , m p ( n − 1 , s u m 1 ) ) others

编程

此题二维数组爆栈,先写出爆栈做法,再用滚动数组解决栈溢出问题。
自顶向下记忆法

#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;

class Solution {
public:
    /** * @param nums: the given array * @return: the minimum difference between their sums */
    int findMin(vector<int> &nums) {
        // write your code here
        int sum = 0;
        for(vector<int>::iterator it = nums.begin(); it != nums.end(); it++) {
            sum += *it;
        }
        int dp[(int)nums.size()+1][sum+1];
        memset(dp, -1, sizeof(int) * (nums.size()+1) * (sum+1));
        return mp(nums.size(), 0, sum, nums, &dp[0][0]);
    }

    int mp(int n, int sum1, int sum, vector<int> &nums, int * dp) {
        if(*(dp + n * (sum+1) + sum1) != -1)
            return *(dp + n * (sum+1) + sum1);
        if(n == 0) {
            return *(dp + n * (sum+1) + sum1) = abs(sum - 2*sum1);
        }
        return *(dp + n * (sum+1) + sum1) = min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp));
    }

    int min(int a, int b) {
        return a < b ? a : b;
    }
};

int main() {
    // int a[] = {987,523,979,847,734,706,452,903,702,332,713,181,991,843,879,505,718,694,18,303,795,521,696,388,866,908,350,528,445,780,864,295,257,337,704,648,495,949,39,33,606,553,618,191,854,405,715,413,472,185,216,489,212,199,162,462,929,191,429,726,902,9,579,403,370,435,871,160,197,884,619,716,182,7,906,974,679,531,852,158,861,174,445,701,871,557,942,798,921,389,450,485,901,179,515,401,117,451,731,828,685,20,50,673,891,232,30,385,511,338,375,118,81,392,296,546,903,59,580,620,268,422,597,876,333,766,158,295,443,204,434,357,632,592,543,341,434,58,525,683,338,165,332,51,152,191,378,63,10,475,951,469,622,811,296,415,282,547,994,358,134,195,888,75,195,805,908,673,867,346,935,318,603,507,45,209,54,641,515,867,881,880,290,781,452,808,775,998,731,908,451,592,608,87,1000,812,30,673,393,380,241,135,421,144,954,64,747,502,633};
    int a[] = {1, 6, 11, 5};
    vector<int> nums(a, a + sizeof(a)/sizeof(int));
    Solution * s = new Solution();
    printf("%d\n", s->findMin(nums));
}

使用滚动数组

#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;

class Solution {
public:
    /** * @param nums: the given array * @return: the minimum difference between their sums */
    int findMin(vector<int> &nums) {
        // write your code here
        int sum = 0;
        for(vector<int>::iterator it = nums.begin(); it != nums.end(); it++) {
            sum += *it;
        }
        int dp[2][sum+1];
        memset(dp, -1, sizeof(int) * 2 * (sum+1));
        return mp(nums.size(), 0, sum, nums, &dp[0][0]);
    }

    int mp(int n, int sum1, int sum, vector<int> &nums, int * dp) {
        if(*(dp + (n%2) * (sum + 1) + sum1) != -1)
            return *(dp + (n%2) * (sum + 1) + sum1);
        if(n == 0) {
            return *(dp + (n%2) * (sum + 1) + sum1) = abs(sum - 2*sum1);
        }
        return *(dp + (n%2) * (sum + 1) + sum1) = min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp));
    }

    int min(int a, int b) {
        return a < b ? a : b;
    }
};
int main() {
    int a[] = {860,5,572,168,878,820,247,661,577,185,136,315,230,413,596,379,991,645,468,699,554,850,340,851,413,237,98,324,436,270,33,619,603,809,668,144,679,827,581,827,215,337,27,34,762,742,779,112,970,283,302,743,437,248,130,31,300,748,1000,644,848,52,1,199,79,515,678};
    // int a[] = {987,523,979,847,734,706,452,903,702,332,713,181,991,843,879,505,718,694,18,303,795,521,696,388,866,908,350,528,445,780,864,295,257,337,704,648,495,949,39,33,606,553,618,191,854,405,715,413,472,185,216,489,212,199,162,462,929,191,429,726,902,9,579,403,370,435,871,160,197,884,619,716,182,7,906,974,679,531,852,158,861,174,445,701,871,557,942,798,921,389,450,485,901,179,515,401,117,451,731,828,685,20,50,673,891,232,30,385,511,338,375,118,81,392,296,546,903,59,580,620,268,422,597,876,333,766,158,295,443,204,434,357,632,592,543,341,434,58,525,683,338,165,332,51,152,191,378,63,10,475,951,469,622,811,296,415,282,547,994,358,134,195,888,75,195,805,908,673,867,346,935,318,603,507,45,209,54,641,515,867,881,880,290,781,452,808,775,998,731,908,451,592,608,87,1000,812,30,673,393,380,241,135,421,144,954,64,747,502,633};
    // int a[] = {1, 6, 11, 5};
    vector<int> nums(a, a + sizeof(a)/sizeof(int));
    Solution * s = new Solution();
    printf("%d\n", s->findMin(nums));
}

在提交代码之后,对于上述输入,OJ上会出现期望:0,输出:20614的结果。但在本机上输出的是0。这是由于两编译器运行顺序不同造成的。

将代码min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp))这一行,min中参数对换顺序,就会出现本机输出20118,OJ运行通过。

因此,我们通过讲解滚动数组来说明为什么会出现这种情况。参考此文

在分析中,我们提到了状态转移方程,当前状态仅与前一个状态有关,因此,我们进行存储子问题时,可以按照相对位置存储,即当前状态下标为i%2,上一个状态的下标为(i-1)%2.由此,可以对存储子问题解的二维数组进行压缩,避免超出栈大小。

由此可知,若当前状态与上两个状态相关,则可用i%3。当与前k-1个状态相关,可用i%k,第一维的大小为k。

我们采用自底向上的制表法更直观的看待这一阐述。

自底向上制表法

#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;

class Solution {
public:
    /** * @param nums: the given array * @return: the minimum difference between their sums */
    int findMin(vector<int> &nums) {
        return _findMin(nums, nums.size());
    }

    int _findMin(vector<int> arr, int n) {
        int sum = 0;
        for (int i = 0; i < n; i++)
            sum += arr[i];
        bool dp[2][sum+1];
        memset(dp, 0, sizeof(bool) * 2 *(sum+1));
        dp[0][0] = true; dp[1][0] = true;

        for (int i=1; i<=n; i++) {
            for (int j=1; j<=sum; j++) {
                dp[i%2][j] = dp[(i-1)%2][j];
                if (arr[i-1] <= j)
                    dp[i%2][j] |= dp[(i-1)%2][j-arr[i-1]];
            }
        }
        int diff = INT_MAX;
        for (int j=sum/2; j>=0; j--) {
            if (dp[n%2][j] == true) {
                    diff = sum-2*j;
                    break;
            }
        }
        return diff;
    }
};
int main() {
    int a[] = {616,202,595,876,388,120,238,296};
    vector<int> nums(a, a + sizeof(a)/sizeof(int));
    Solution * s = new Solution();
    printf("%d\n", s->findMin(nums));
}

使用制表法,则在本机和OJ都运行正确。而采用记忆法,由于不同的编译环境对于方法参数的调用顺序不同等原因,造成不同的结果。

因而,如果在OJ过程中,出现本机结果与OJ结果不同,可能是由于编译环境的差异造成的。

本节介绍了滚动数组,解决了错误:本机与OJ结果不同,解决方法:将参数对换 或采用自底向上制表法,尽量避免由于编译环境不同造成的错误。

时间复杂度: O(nsum) O ( n ∗ s u m )
空间复杂度:视dp数组大小而定。

系列文:
动态规划入门1
动态规划入门2
动态规划入门3

点赞