经典动态规划问题 背包问题 [POJ3211][POJ2063][HDU1114][POJ1276][POJ1014][POJ1742][ZOJ3631]

动态规划(dp)有主要有记忆化搜索和递推两种实现形式。


首先来看一个递归的问题——汉诺塔问题

题意:有三根柱子分别为A,B,C,现在在A柱子上套着n个盘子,每一个盘子都比在它上面的的盘子大,现在需要将n个盘子全部从A移到C,移动过程中要保证每个柱子上,底部的盘子总是比在它上面的盘子大,求移动多盘子的过程。

分析:

将n个盘子从A借助B移动到C,可分3步实现:

1.将上面的n-1个盘子从A借助C移动到B

2.将最底部的第n个盘子从A直接移到C

3.将n-1个盘子从B借助A移动到C

代码:

void hanoi(int n,char A,char B,char C)  
{  
    if(n==1)  
    {  
        moveAC(A,C);  
        return;  
    }  
    hanoi(n-1,A,C,B); 
    move(A,C);
    hanoi(n-1,B,A,C);
}  
动态规划问题往往也可以用递归(搜索)来求解,但在递归实现的过程中往往会大量地进行重复的调用,增加了时间复杂度,这时我们有两种策略:

1)增加标记数组,当某个搜索的状态已经调用过,我们就直接返回之前调用时得到的结果;

2)写出递推式,由搜索的最底层开始,借助底层已经得到的结果推出上面层的结果,最终得到搜索顶层的答案。


最长上升自序列问题(LIS)

题意:给出n个整数a1,a2...an组成序列,求最长的上升子序列(若i< j,则ai<aj)长度。如序列1,4,3,5,7,3,8,可选出最长上升子序列1,3,5,7,8。

分析:

时间复杂度为O(n^2)的做法:

dp[i]表示以ai为末尾的最长上升子序列长度,则有 当a[j] < a[i], dp[i] = max{dp[i], dp[j] + 1}

时间复杂度O(nlogn)的做法:

dp[i]表示长度为i的上升子序列末尾元素的最小值,则对于每个元素ai,二分查找数组dp,找到第一个dp[]值不小于ai的位置,将其值变为ai

例题:http://acm.nyist.net/JudgeOnline/problem.php?pid=17

代码:

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
#define INF 0x7fffffff
#define MOD 1000000007
using namespace std;
typedef long long ll;
char a[10005], dp[10005];
int main()
{
    int n;
    scanf("%d ", &n);
    while(n--)
    {
        scanf("%s", a);
        int len = strlen(a);
        for(int i = 0; i <= len; i++)
        {
            dp[i] = 'z' + 5;
        }
        for(int i = 0; i < len; i++)
        {
            *lower_bound(dp, dp + len, a[i]) = a[i];
        }
        printf("%d\n", lower_bound(dp, dp + len, 'z' + 5) - dp);
    }

    return 0;
}

最长公共子序列问题(LCS)

题意:给定两个字符串s1,s2...sn和t1,t1...tm,求最长公共子序列长度

如序列abcda和becd,最长公共子序列为bcd。

分析:

dp[i][j]表示 s1...si 和 t1...tj 的对应的最长公共子序列长度

则 当si=tj,dp[i][j] = max{dp[i - 1][j - 1] + 1, dp[i - 1][j], dp[i][j - 1]}

     否则,dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]} 


矩阵连成顺序问题

题意:矩阵A为p0*p1的矩阵,矩阵B为p1*p2的矩阵,则两个矩阵相乘运算量为p0 * p1 * p2,多个矩阵相乘满足结合律,即A * B * C = A * (B * C),改变运算顺序使得结果相同但运算量不同,给出n个矩阵组成的序列,求出将它们依次乘起来的最小运算量 。

分析:

dp[i][j]表示第i个矩阵到第j个矩阵相乘的最小运算量,则 dp[i][j] = min{dp[i][k] + dp[k + 1][j] + p(i-1) * pk * pj},其中k为 i 和 j 之间的矩阵

观察得 j - i 较大的dp[i][j]要由 j - i 较小的dp[i][j] 转移得到结果,所以需按照 j - i 递增的顺序递推。


划分数

题意:有n个无区别的物品,将它们划分成不超过m组,求出划分方法数。

分析:

dp[i][j] 表示将j个物品划分为小于等于i分的方法数,则有

dp[i][j] = dp[i][j - 1] + dp[i - 1][j]

其中,dp[i][j - i]为第j个组不为空的方法数,可理解为先在i组中各放一个,再将j - i个放在i组中


背包问题

0-1背包

题意:有n个重量和价值分别为wi,vi(0 <=  i < n)的物品,从这些物品中挑选出总重量不超过W的物品,使得到的总价值尽量大,求出总价值的最大值。

分析:

dp[i][j] 表示考虑了前i个物品,总重量不超过 j 时得到的总价值最大值,则

当 j < wi,dp[i][j] = dp[i - 1][j]

否则,dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - wi] + vi},这样理解,在容量最大为 j 时,如果一定要取第 i 个物品,那么前 i - 1个物品所占的最大容量为 j - wi


完全背包

题意:与0-1背包不同的是,给出的是n种重量和价值为wi,vi的物品,即相当于0-1背包中每个物品可以选无限多次

分析:

dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - k * wi] + vi},该式中还需从0开始枚举k直到 k * wi > j


0-1背包和完全背包都可以使用一维数组来实现,根据dp[i][j]列出表,我们可以观察到

0-1背包中,第i行第j列的更新只与第i - 1行的第j列之前的元素有关,所以,我们可以用一维数组不记录行数 i,按 j 递减的顺序更新,第j个元素的更新后不影响j前 j 个元素的更新

完全背包中,第i行第j列更新后的结果可用于第i行第j列后面的元素更新,所以我们用一维数组同样不记录行数,但按 j 递增的顺序更新

下面是我以前写过的题和题解
0-1背包
例题:POJ 3211 http://poj.org/problem?id=3211
完全背包
例题:POJ 2063 http://poj.org/problem?id=2063

上面介绍的0-1背包的解法时间复杂度是O(nW),有时会出现W很大而总价值可能的最大值相对较小的情况,这时候我们用dp[i]记录总价值为i时最小的总重量,求出dp[i] 小于等于W的最大的 i 值即可。解决背包问题的首先要确定要根据背包的大小确定好容量。

多重背包
题意:同样给出N种物品,已知每种物品的重量和价值,同时每种物品最多可选个数为n1,n2..nN
分析:
可以按照完全背包二维数组实现的方法,从0开始枚举k直到k=ni,但在很多题目中k值的范围会很大,这样,算法的复杂度就会是O(N*W*ni的最大值),所以我经常要对ni进行二进制转化,然后按照0-1背包的方法处理
二进制转化原理:
在1 2 4 8中不重复地任取一些数,相加即可表示出1-15中的任意一个数
如1 0 1 0对应去1和4相加可以得到5
如果现在我们有某种物品15个
我们可把它们分成:1个这种物品为1个物品,2个这种物品为1个物品...8个这种物品为一个物品(4种)
我们在上面分得的4个物品中任取一些,即可表示出我要取这种物品的个数(1-15)
然后对于这种物品取的个数,我只需枚举4次了(原来枚举15次)

下面是我以前写过的题和题解:
例题:POJ 1276 http://poj.org/problem?id=1276
例题:POJ 1014 http://poj.org/problem?id=1014

多重背包可行性问题
题意:有n种大小不同的数字ai,每种mi个,问能否从这些数字中取出 若干个使它们的和恰好为K。其中
分析:
乍一看,这个题可以用看成多重背包问题直接求解,但时间复杂度就大了,实际上我们可以在O(nK)时间内计算出结果
例题:POJ 1742 http://poj.org/problem?id=1742
题意:有n种价值为ai的硬币个ci个,问有多少种方案使得硬币价值总和为m
代码:
#include <stdio.h>
#include <string.h>
int a[105], c[105], dp[100005];

int main()
{
    #ifdef LOCAL
    freopen("data.in", "r", stdin);
    #endif

    int n, m, ans;
    while(scanf("%d%d", &n, &m) != EOF)
    {
        if(!n && !m) break;
        for(int i = 1; i <= n; i++)
            scanf("%d", &a[i]);
        for(int i = 1; i <= n; i++)
            scanf("%d", &c[i]);

        memset(dp, -1, sizeof(dp));
        dp[0] = 0;
        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= m; j++)
            {
                if(dp[j] >= 0) dp[j] = c[i]; //用前i-1种物品可以组成j,则要组成j剩余第i种物品c[i]个
                else if(j < a[i] || dp[j - a[i]] <= 0) //用第i种物品不能组成j
                    dp[j] = -1;
                else dp[j] = dp[j - a[i]] - 1; //用前i种物品组成j,最多剩余第i种物品个数
            }
        ans = 0;
        for(int i = 1; i <= m; i++)
        {
            if(dp[i] >= 0) ans++;
        }
        printf("%d\n", ans);
    }

    return 0;
}

最后讲一下超大背包问题,其实这不算dp问题了,需要使用位运算和折半枚举的技巧
题意:同0-1背包问题,只是n的数量比较小,W的数量很大
代码:这个代码我是照着《挑战》打的
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> P;
int n;
ll w[50], v[50], m;
long long int INF = (1 << 30) - 1;
P ps[1 << 20];

void solve()
{
    int n2 = n / 2;
    ll res = 0;
    for(int i = 0; i < 1 << n2; i++) //先枚举在左边的物品中取物品的情况,总情况数为2^n2
    {//枚举1-2^n2,每个数字i对应一种取物品的情况
        ll sw = 0, sv = 0;
        for(int j = 0; j < n2; j++)
        { //用位运算判断i这种情况时,有没有去第j个物品
            if(i >> j & 1)
            {
                sw += w[j]; sv += v[j];
            }
        }
        ps[i] = make_pair(sw, sv); //加入每种取物品的情况对应总重和总价值
    }
    sort(ps, ps + (1 << n2));
    int mm = 1;
    for(int i = 1; i < 1 << n2; i++)
    { //去掉多余情况
        if(ps[mm - 1].second < ps[i].second)
        {
            ps[mm++] = ps[i];
        }
    }
    for(int i = 0; i < 1 << (n - n2); i++)
    { //枚举在右边的物品中取物品的情况
        ll sw = 0, sv = 0;
        for(int j = 0; j < n - n2; j++)
        {
            if(i >> j & 1)
            {
                sw += w[n2 + j]; sv += v[n2 + j];
            }
        }
        if(sw <= m)
        {//每得到在右边取物品的情况,用二分查找寻找在左边取物品的情况
            ll tv = (lower_bound(ps, ps + mm, make_pair(m - sw, INF)) - 1) -> second; //使得两部分总重相加小于等于且总价值尽量大
            res = max(res, sv + tv);
        }
    }
    printf("%lld\n", res);
}

int main()
{
    while(scanf("%d%lld", &n, &m) != EOF)
    {
        for(int i = 0; i < n; i++)
        {
            scanf("%lld", &w[i]);
            v[i] = w[i];
        }
        solve();
    }
    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值