首页 > 代码库 > poj 1742 Coins 多重背包变形

poj 1742 Coins 多重背包变形

传说中的男人八题,是男人就A这八题。有n种面额的硬币,面额个数分别为A_i、C_i,求最多能搭配出几种不超过m的金额?

这是一个多重部分和问题(多重背包问题),放在了《2.3 记录结果再利用的“动态规划” 优化递推关系式》。最基本的做法是:

dp[i][j] := 用前i种硬币能否凑成j

递推关系式:

dp[i][j] = (存在k使得dp[i - 1][j - k * A[i]]为真,0 <= k <= m 且下标合法)

然后三重循环ijk递推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <algorithm>
using namespace std;
 
bool dp[100 + 16][100000 + 16]; // dp[i][j] := 用前i种硬币能否凑成j
int A[100 + 16];
int C[100 + 16];
 
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
#ifndef ONLINE_JUDGE
    freopen("in.txt""r", stdin);
    freopen("out.txt""w", stdout);
#endif
    int n, m;
    while(cin >> n >> m && n > 0)
    {
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i < n; ++i)
        {
            cin >> A[i];
        }
        for (int i = 0; i < n; ++i)
        {
            cin >> C[i];
        }
        dp[0][0] = true;
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j <= m; ++j)
            {
                for (int k = 0; k <= C[i] && k * A[i] <= j; ++k)
                {
                    dp[i + 1][j] |= dp[i][j - k * A[i]];
                }
            }
        }
        int answer = count(dp[n] + 1, dp[n] + 1 + m , true); // 总额0不算在答案内
        cout << answer << endl;
    }
#ifndef ONLINE_JUDGE
    fclose(stdin);
    fclose(stdout);
    system("out.txt");
#endif
    return 0;
}
///////////////////////////End Sub//////////////////////////////////

这种代码不用提交也知道会TLE,因为这个朴素的算法的复杂度是O(m∑iCi),比如那第二个用例画成图的话会看到:

解释一下,dp数组和更新顺序为:

第二个用例:

1 0 0 0 0 0 

0 0 0 0 0 0 

0 0 0 0 0 0 

————–

因为可以用0个1加上dp[0][0]拼成0;所以,dp[1][0]被更新为真

因为可以用1个1加上dp[0][0]拼成1;所以,dp[1][1]被更新为真

因为可以用2个1加上dp[0][0]拼成2;所以,dp[1][2]被更新为真

1 0 0 0 0 0 

1 1 1 0 0 0 

0 0 0 0 0 0 

————–

因为可以用0个4加上dp[1][0]拼成0;所以,dp[2][0]被更新为真

因为可以用0个4加上dp[1][1]拼成1;所以,dp[2][1]被更新为真

因为可以用0个4加上dp[1][2]拼成2;所以,dp[2][2]被更新为真

因为可以用1个4加上dp[1][0]拼成4;所以,dp[2][4]被更新为真

因为可以用1个4加上dp[1][1]拼成5;所以,dp[2][5]被更新为真

1 0 0 0 0 0 

1 1 1 0 0 0 

1 1 1 0 1 1 

————–

4

顺便第一个用例:

1 0 0 0 0 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

————–

因为可以用0个1加上dp[0][0]拼成0;所以,dp[1][0]被更新为真

因为可以用1个1加上dp[0][0]拼成1;所以,dp[1][1]被更新为真

因为可以用2个1加上dp[0][0]拼成2;所以,dp[1][2]被更新为真

1 0 0 0 0 0 0 0 0 0 0 

1 1 1 0 0 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

————–

因为可以用0个2加上dp[1][0]拼成0;所以,dp[2][0]被更新为真

因为可以用0个2加上dp[1][1]拼成1;所以,dp[2][1]被更新为真

因为可以用0个2加上dp[1][2]拼成2;所以,dp[2][2]被更新为真

因为可以用1个2加上dp[1][1]拼成3;所以,dp[2][3]被更新为真

因为可以用1个2加上dp[1][2]拼成4;所以,dp[2][4]被更新为真

1 0 0 0 0 0 0 0 0 0 0 

1 1 1 0 0 0 0 0 0 0 0 

1 1 1 1 1 0 0 0 0 0 0 

0 0 0 0 0 0 0 0 0 0 0 

————–

因为可以用0个4加上dp[2][0]拼成0;所以,dp[3][0]被更新为真

因为可以用0个4加上dp[2][1]拼成1;所以,dp[3][1]被更新为真

因为可以用0个4加上dp[2][2]拼成2;所以,dp[3][2]被更新为真

因为可以用0个4加上dp[2][3]拼成3;所以,dp[3][3]被更新为真

因为可以用0个4加上dp[2][4]拼成4;所以,dp[3][4]被更新为真

因为可以用1个4加上dp[2][1]拼成5;所以,dp[3][5]被更新为真

因为可以用1个4加上dp[2][2]拼成6;所以,dp[3][6]被更新为真

因为可以用1个4加上dp[2][3]拼成7;所以,dp[3][7]被更新为真

因为可以用1个4加上dp[2][4]拼成8;所以,dp[3][8]被更新为真

1 0 0 0 0 0 0 0 0 0 0 

1 1 1 0 0 0 0 0 0 0 0 

1 1 1 1 1 0 0 0 0 0 0 

1 1 1 1 1 1 1 1 1 0 0 

————–

8


这个算法每次只记录一个bool值,损失了不少信息。在这个问题中,不光能够求出是否能得到某个金额,同时还能把得出了此金额时A_i还剩下多少个算出来,这样直接省掉了k那重循环。

优化dp定义:

1
2
3
4
dp[i][j] := 用前i种硬币凑成j时第i种硬币最多能剩余多少个(-1表示配不出来)
            如果dp[i - 1][j] >= 0(前i-1个数可以凑出j,那么第i个数根本用不着)直接为C[i]
dp[i][j] =  如果j < A[i]或者dp[i][j - a[i]] <=0 (面额太大或者在配更小的数的时候就用光了)-1
            其他(将第i个数用掉一个) dp[i][j-a[i]] - 1

最后统计一下dp数组第n行>=0的个数就知道答案了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <algorithm>
using namespace std;
 
int dp[100 + 16][100000 + 16]; // dp[i][j] := 用前i种硬币凑成j时第i种硬币最多能剩余多少个
int A[100 + 16];
int C[100 + 16];
 
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
#ifndef ONLINE_JUDGE
    freopen("in.txt""r", stdin);
    freopen("out.txt""w", stdout);
#endif
    int n, m;
    while(cin >> n >> m && n > 0)
    {
        memset(dp, -1, sizeof(dp));
        dp[0][0] = 0;
        for (int i = 0; i < n; ++i)
        {
            cin >> A[i];
        }
        for (int i = 0; i < n; ++i)
        {
            cin >> C[i];
        }
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j <= m; ++j)
            {
                if (dp[i][j] >= 0)
                {
                    dp[i + 1][j] = C[i];
                }
                else if (j < A[i]                        // 用一个就超出,不能用
                        || dp[i + 1][j - A[i]] <= 0)      // 连凑比j小的数的时候都用完了,此时更加用完了
                {
                    dp[i + 1][j] = -1;
                }
                else
                {
                    dp[i + 1][j] = dp[i + 1][j - A[i]] - 1;       // 用上了一个第i个硬币
                }
            }
        }
        int answer = count_if(dp[n] + 1, dp[n] + 1 + m , bind2nd(greater_equal<int>(), 0)); // 总额0不算在答案内
        cout << answer << endl;
    }
#ifndef ONLINE_JUDGE
    fclose(stdin);
    fclose(stdout);
    system("out.txt");
#endif
    return 0;
}
///////////////////////////End Sub//////////////////////////////////

还是拿第二个用例画个图:

第二个用例:

0 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 

————–

dp[0][0]不为负,dp[1][0]更新为硬币0的个数。

dp[0][1]其他,dp[1][1]更新为dp[1][0]

dp[0][2]其他,dp[1][2]更新为dp[1][1]

dp[1][2]用完了,dp[1][3]更新为硬币0的个数。

dp[1][3]用完了,dp[1][4]更新为硬币0的个数。

dp[1][4]用完了,dp[1][5]更新为硬币0的个数。

0 -1 -1 -1 -1 -1 

2 1 0 -1 -1 -1 

-1 -1 -1 -1 -1 -1 

————–

dp[1][0]不为负,dp[2][0]更新为硬币1的个数。

dp[1][1]不为负,dp[2][1]更新为硬币1的个数。

dp[1][2]不为负,dp[2][2]更新为硬币1的个数。

余额太大,dp[2][3]更新为硬币1的个数。

dp[1][4]其他,dp[2][4]更新为dp[2][0]

dp[1][5]其他,dp[2][5]更新为dp[2][1]

0 -1 -1 -1 -1 -1 

2 1 0 -1 -1 -1 

1 1 1 -1 0 0 

————–

4

第一个用例:

0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

————–

dp[0][0]不为负,dp[1][0]更新为硬币0的个数。

dp[0][1]其他,dp[1][1]更新为dp[1][0]

dp[0][2]其他,dp[1][2]更新为dp[1][1]

dp[1][2]用完了,dp[1][3]更新为硬币0的个数。

dp[1][3]用完了,dp[1][4]更新为硬币0的个数。

dp[1][4]用完了,dp[1][5]更新为硬币0的个数。

dp[1][5]用完了,dp[1][6]更新为硬币0的个数。

dp[1][6]用完了,dp[1][7]更新为硬币0的个数。

dp[1][7]用完了,dp[1][8]更新为硬币0的个数。

dp[1][8]用完了,dp[1][9]更新为硬币0的个数。

dp[1][9]用完了,dp[1][10]更新为硬币0的个数。

0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

2 1 0 -1 -1 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

————–

dp[1][0]不为负,dp[2][0]更新为硬币1的个数。

dp[1][1]不为负,dp[2][1]更新为硬币1的个数。

dp[1][2]不为负,dp[2][2]更新为硬币1的个数。

dp[1][3]其他,dp[2][3]更新为dp[2][1]

dp[1][4]其他,dp[2][4]更新为dp[2][2]

dp[2][3]用完了,dp[2][5]更新为硬币1的个数。

dp[2][4]用完了,dp[2][6]更新为硬币1的个数。

dp[2][5]用完了,dp[2][7]更新为硬币1的个数。

dp[2][6]用完了,dp[2][8]更新为硬币1的个数。

dp[2][7]用完了,dp[2][9]更新为硬币1的个数。

dp[2][8]用完了,dp[2][10]更新为硬币1的个数。

0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

2 1 0 -1 -1 -1 -1 -1 -1 -1 -1 

1 1 1 0 0 -1 -1 -1 -1 -1 -1 

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

————–

dp[2][0]不为负,dp[3][0]更新为硬币2的个数。

dp[2][1]不为负,dp[3][1]更新为硬币2的个数。

dp[2][2]不为负,dp[3][2]更新为硬币2的个数。

dp[2][3]不为负,dp[3][3]更新为硬币2的个数。

dp[2][4]不为负,dp[3][4]更新为硬币2的个数。

dp[2][5]其他,dp[3][5]更新为dp[3][1]

dp[2][6]其他,dp[3][6]更新为dp[3][2]

dp[2][7]其他,dp[3][7]更新为dp[3][3]

dp[2][8]其他,dp[3][8]更新为dp[3][4]

dp[3][5]用完了,dp[3][9]更新为硬币2的个数。

dp[3][6]用完了,dp[3][10]更新为硬币2的个数。

0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 

2 1 0 -1 -1 -1 -1 -1 -1 -1 -1 

1 1 1 0 0 -1 -1 -1 -1 -1 -1 

1 1 1 1 1 0 0 0 0 -1 -1 

————–

8


本以为这次照着书上的思路来的应该没问题了吧,数组再利用就懒得做了。于是提交,结果MLE

于是打起精神来重复利用数组,注意到上图中的箭头都是垂直的,也就是说可以定义

dp[j] := 在第i次循环时之前表示用前i-1种硬币凑成j时第i种硬币最多能剩余多少个(-1表示配不出来),循环之后就表示第i次的状态

于是就省了一维数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
 
int dp[100000 + 16]; // dp[i][j] := 用前i种硬币凑成j时第i种硬币最多能剩余多少个
int A[100 + 16];
int C[100 + 16];
 
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
#ifndef ONLINE_JUDGE
    freopen("in.txt""r", stdin);
    freopen("out.txt""w", stdout);
#endif
    int n, m;
    while(cin >> n >> m && n > 0)
    {
        memset(dp, -1, sizeof(dp));
        dp[0] = 0;
        for (int i = 0; i < n; ++i)
        {
            cin >> A[i];
        }
        for (int i = 0; i < n; ++i)
        {
            cin >> C[i];
        }
        for (int i = 0; i < n; ++i)
        {
            for (int j = 0; j <= m; ++j)
            {
                if (dp[j] >= 0)
                {
                    dp[j] = C[i];
                }
                else if (j < A[i]                        // 用一个就超出,不能用
                        || dp[j - A[i]] <= 0)       // 连凑比j小的数的时候都用完了,此时更加用完了
                {
                    dp[j] = -1;
                }
                else
                {
                    dp[j] = dp[j - A[i]] - 1;     // 用上了一个第i个硬币
                }
            }
        }
        int answer = count_if(dp + 1, dp + 1 + m , bind2nd(greater_equal<int>(), 0)); // 总额0不算在答案内
        cout << answer << endl;
    }
#ifndef ONLINE_JUDGE
    fclose(stdin);
    fclose(stdout);
    system("out.txt");
#endif
    return 0;
}
///////////////////////////End Sub//////////////////////////////////
OHTER
多重背包的题目:才开始不知道怎么来统计最后可以给出多少种价格,因为以前的形式都是给出c[i],w[i],b[i]的这里没有给出,后来自己yy了一下,利用多粗冲背包向01背包转换的第一种方法后最后从1到V里所有出现的不同的金钱数量不就是最终可以给出的各种价格吗。01背包转换的第一种方法o(n^3)肯定会TLE代码如下:



#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int max_s = 107;
int f[100007],w[max_s],b[max_s];
int main()
{
    int n,V,i,j,k;
    while(scanf("%d%d",&n,&V))
    {
        if(!n&&!V) break;
        for(i=0;i<n;i++)
        scanf("%d",&w[i]);
        for(i=0;i<n;i++)
        scanf("%d",&b[i]);
        memset(f,0,sizeof(f));
        f[0]=0;
        for(i=0;i<n;i++)
        {
            for(j=1;j<=b[i];j++)
            {
                for(k=V;k>=w[i];k--)
                f[k]=max(f[k],f[k-w[i]]+w[i]);
            }
        }
        /*for(i=V;i>=0;i--)
        printf("%d ",f[i]);
         printf("\n");*/
        int ans=1;
        for(i=V-1;i>=0;i--)
        {
            if(f[i]!=f[i+1]&&f[i])
            ans++;
        }
        cout<<ans<<endl;
    }
    return 0;
}

最后想到了向01背包转化的第二种办法,可是自己写的还是会超时,搜了一下解题报考,发现有人能过用这个o(n*Σlog b[i])的方法,优化了自己的代码,可是还是不过贡献了N次wa最后对折人家的代码检查,只是改了一下int f[]--->到bool f[] 2594s险过。。。bool占用一个字节,应该是处理的时候比int快,,,



#include <cstdio>
using namespace std;
const int max_s = 107;
bool f[100007];
int w[max_s],b[max_s];
int V;
void zb(int c)
{
    for(int i=V;i>=c;--i)
    f[i]|=f[i-c];//直接或运算用0,1表示此价格是否出现过
}
void cb(int c)
{
    for(int i=c;i<=V;++i)
    f[i]|=f[i-c];
}
int main()
{
    int n,i,k,ans;
    while(scanf("%d%d",&n,&V),n+V)
    {
        for(i=0;i<n;++i)
        scanf("%d",&w[i]);
        for(i=0;i<n;++i)
        scanf("%d",&b[i]);
        for(i=f[0]=1;i<=V;++i) f[i]=0;
        for(i=0;i<n;++i)
        {
            if(w[i]*b[i]>=V)
            cb(w[i]);
            else
            {
                k=1;
                while(k<b[i])
                {
                    zb(k*w[i]);
                    b[i]-=k;
                    k<<=1;
                }
                zb(b[i]*w[i]);
            }
        }
        ans=0;
        for(i=1;i<=V;++i)
        ans+=f[i];
        printf("%d\n",ans);
    }
    return 0;
}

最后是楼教主的o(n*v)的方法。。ORZ啊。。

1284s A


复制代码
#include <cstdio>
#include <iostream>
using namespace std;
const int max_s = 107;
int f[100007],w[max_s],b[max_s];
//f[i]来表示当前i价格是否出现过,
int sum[100007];//当价格达到i时,最多使用这一种硬币的次数
int main()
{
    int n,V,i,j;
    while(scanf("%d%d",&n,&V),n+V)
    {
        int ans=0;
        for(i=0;i<n;i++)
        scanf("%d",&w[i]);
        for(i=0;i<n;i++)
        scanf("%d",&b[i]);
        for(i=f[0]=1;i<=V;i++) f[i]=0;
        for(i=0;i<n;i++)
        {
            for(j=0;j<=V;j++) sum[j]=0;//关键是用sum来限定了次数
            for(j=w[i];j<=V;j++)//从w[i]到V循环检查看是否能够出现前边没有出现的价格
            {
                if(!f[j]&&f[j-w[i]]&&sum[j-w[i]]<b[i])
                //如果j价格没有出现过,且j-w[i]出现过,并且使用i硬币的次数没有超出给定的数量
                {
                    f[j]=1;//标记为已出现过
                    sum[j]=sum[j-w[i]]+1;//使用次数+1
                    ans++;//满足条件++
                }
            }
        }
        cout<<ans<<endl;
    }
}

poj 1742 Coins 多重背包变形