王道机试指南--第七章(动态规划)

搬宿舍

题目7.5链接搬宿舍
题目大意:从n物品中取出k对,使得每对重量之差平方的和最小。
思路:这道动态规划题目在做之前需要自己证明一下:每对物品取相邻物品(先排序),可以保证取得的物品重量之差的平方的和是最小的。所以题解转换为:先排序,在取相邻物品。
可以设状态dp[i][j]表示在前j件物品中选择i对物品是最小的代价。对于物品j有两种选择:与物品j-1配对或不配对。不配对,则dp[i][j] = dp[i][j-1];配对,则dp[i][j] = dp[i-1][j-2]+(w[j-1]-w[j-2])^2(注意w是重量,下标从0开始),二者取最小。
同时需要注意:只有j>=ix2,dp[i][j]才有意义;且当j==ix2时,所有的物品均要使用。
代码如下:

#include <iostream>
#include <algorithm>
using namespace std;

const int MAX = 2001;
const int M = 1<<15;
int dp[MAX][MAX];
int w[MAX];
int n, k;

void Init()
{
    for(int i=0; i<=MAX; i++)
    {
        dp[0][i] = 0;
    }
}
int main()
{
    while(cin >> n >> k)
    {
        //初始化
        Init();
        for(int i=0; i<n; i++)
        {
            cin >> w[i];
        }
        //排序
        sort(w, w+n);
        //初始化
        for(int i=2; i<=n; i+=2)
        {
            dp[i/2][i] = dp[(i-2)/2][i-2]+(w[i-1]-w[i-2])*(w[i-1]-w[i-2]);
        }
        //动态规划,注意下标
        for(int i=1; i<=k; i++)
        {
            for(int j=i*2+1; j<=n; j++)
            {
                dp[i][j] = min(dp[i][j-1], dp[i-1][j-2]+(w[j-1]-w[j-2])*(w[j-1]-w[j-2]));
            }

        }
        cout << dp[k][n] << endl;
    }
    return 0;
}

记忆化搜索代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

/*
    dp[i][j]表示在前i个物品中搬2*j个的最小疲劳度
*/

const int MAX = 2005;
const int INF = 1<<29;
int w[MAX], dp[MAX][MAX/2];

int DP(int i, int j)
{
    if(i<2*j) return INF;//无法搬出2*j的返回INF
    int &ans = dp[i][j];
    if(ans!=-1) return ans;
    ans = min(DP(i-1, j), DP(i-2, j-1)+(w[i]-w[i-1])*(w[i]-w[i-1]));
    return ans;
}
int main()
{
    int n, k;
    while(scanf("%d%d", &n, &k)!=EOF)
    {
        for(int i=1; i<=n; i++)
        {
            scanf("%d", &w[i]);
        }
        sort(w+1, w+n+1);
        memset(dp, -1, sizeof(dp));
        for(int i=0; i<=n; i++) dp[i][0] = 0;//初始化
        printf("%d\n", DP(n, k));
    }
    return 0;
}
Gready Tino

题目链接7.6Gready Tino
题目大意:从一个数列中选出两堆数,使得两堆数的总和相等,求满足该条件下,每一堆数的总和最大是多少。
思路:动态规划,重点在状态的构造选择。

  • 状态构造:设状态dp[i][j]代表前i个柑橘被选择分配后(三种情况:分配给第一堆;分配给第二堆;不选择),第一堆比第二堆重j时(当j为负时表示第二堆比第一堆重),两堆的最大总重量之和。
  • 终态:状态dp[n][0]表示将所有物品均分配好后,达到条件的状态。
  • 状态转移方程:总共有三种状态可以转移,1)移到第一个堆,则移动之前第一堆一定比第二个堆多j-w[i],2)移到第二个堆,则移动之前第二个堆一定比第二个堆多j+w[i],3)不移动,则之前的重量不变。
    dp[i][j] = max(dp[i-1][j-w[i]]+w[i], dp[i-1][j+w[i]]+w[i], dp[i-1][j]);
  • 初始化:dp[0][0]=0; dp[0][j] (j不等于0)为负无穷(表示该状态不应该被选择,非法状态)。
  • 额外注意:存在柑橘质量为0时,可以使得一定存在情况满足条件(即一堆放一个质量为0的柑橘)
#include <iostream>
#include <cstdio>
using namespace std;

const int OFFSET = 2000;//偏移量
const int INF = 1<<29;
int dp[101][4001];
int W[101];//重量

int main()
{
    int T, cas = 0;
    scanf("%d", &T);
    while(T--)
    {
        int n;
        scanf("%d", &n);
        bool zero = false;//统计是否存在重量为0的柑橘
        int cnt = 0;//计数器,记录有多少个重量非0的柑橘
        for(int i=1; i<=n; i++)//输入n个柑橘的重量
        {
            scanf("%d", &W[++cnt]);
            if(W[cnt]==0)
            {
                cnt--;//去除这个重量为0的柑橘
                zero = true;//记录存在重量为0的柑橘
            }
        }

        n = cnt;
        //初始化所有dp[0][i]为负无穷,即不符合条件
        for(int i=-2000; i<=2000; i++)
        {
            dp[0][i+OFFSET] = -INF;
        }
        dp[0][0+OFFSET] = 0;
        for(int i=1; i<=n; i++)
        {
            for(int j=-2000; j<=2000; j++)
            {
                int tmp1 = -INF, tmp2 = -INF;//

                //保证两堆的重量差值不超过2000
                if(j+W[i]<=2000 && dp[i-1][j+W[i]+OFFSET]!=-INF)//可以放在第二堆
                {
                    tmp1 = dp[i-1][j+W[i]+OFFSET] + W[i];
                }
                if(j-W[i]>=-2000 && dp[i-1][j-W[i]+OFFSET]!=-INF)//可以放在第一堆
                {
                     tmp2 = dp[i-1][j-W[i]+OFFSET] + W[i];
                }

                //选出两堆重量最大的情况
                if(tmp1<tmp2) tmp1 = tmp2;
                if(tmp1<dp[i-1][j+OFFSET]) tmp1 = dp[i-1][j+OFFSET];
                dp[i][j+OFFSET] = tmp1;
            }
        }

        printf("Case %d: ", ++cas);
        if(dp[n][0+OFFSET]==0)
        {
            if(zero) printf("0\n");
            else     printf("-1\n");
        }
        else
            printf("%d\n", dp[n][0+OFFSET]/2);
    }
    return 0;
}
背包问题

《王道》上的三道背包题包括了0-1背包问题,完全背包问题和多重背包问题,但均是以0-1背包为基础,转换为0-1背包或以其为基础来解决,具有很大的参考价值。

采药(0-1背包)

题目链接7.7采药
题目大意:给定规定时间,有多种草药(每一种只有一个,且有不同的采药时间和价值),问在规定时间内怎样采到的草药价值最高。
思路:将问题抽象为背包问题。时间等价于背包容量,即如何将东西装入背包且获得价值最大。
方法一:二维数组

  • 构造状态:d[i][j]表示在总体积不超过j的条件下,前i个物品能达到的最大价值。
  • 终态:d[n][W]。W表示背包容量,n表示物品总数量。
  • 状态转移方程:物品有两种可能:装入背包,不装入背包。
    如果该物品可以装入背包(即容量足够大,需要比较装入该物品后的价值与不装入该物品(可能装入了之前的多个物品)的价值大小。
    d [ i ] [ j ] = { 0 i = 0 o r j = 0 m a x ( d [ i − 1 ] [ j ] , d [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) j − w [ i ] &gt; = 0 d [ i − 1 ] [ j ] j − w [ i ] &lt; 0 d[i][j] = \begin{cases} 0 &amp; &amp; i=0 or j=0 \\ max(d[i-1][j], d[i-1][j-w[i]]+v[i]) &amp; &amp; j-w[i]&gt;=0\\ d[i-1][j] &amp; &amp;j-w[i]&lt;0 \end{cases} d[i][j]=0max(d[i1][j],d[i1][jw[i]]+v[i])d[i1][j]i=0orj=0jw[i]>=0jw[i]<0
  • 初始化:d[0][0] = 0
    代码:
#include <iostream>
#include <cstring>
using namespace std;

const int MAX = 101;
struct Node
{
    int w, v;
};
Node u[MAX];
int dp[MAX][MAX*10];

void Init()
{
    memset(dp, 0, sizeof(dp));//也可以只对dp[0][0]赋值为0
}

int main()
{
    int T, M;
    while(cin >> T >> M)
    {
        for(int i=1; i<=M; i++)
        {
            cin >> u[i].w >> u[i].v;
        }
        Init();
        for(int i=1; i<=M; i++)
        {
            for(int j=1; j<=T; j++)
            {
                if(j>=u[i].w)
                {
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-u[i].w]+u[i].v);
                }
                else
                {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        cout << dp[M][T] << endl;
    }
    return 0;
}

方法二:一维数组

  • 构造状态:d[j]表示在总体积不超过j的条件下,物品能达到的最大价值。
  • 终态:d[W]。W表示背包容量。
  • 状态转移方程:物品有两种可能:装入背包,不装入背包。
    如果该物品可以装入背包(即容量足够大,需要比较装入该物品后的价值与不装入该物品(可能装入了之前的多个物品)的价值大小。
    d [ j ] = { 0 i = 0 o r j = 0 m a x ( d [ j ] , d [ j − w [ i ] ] + v [ i ] ) j − w [ i ] &gt; = 0 ; i &gt; = 1 i &lt; = n d [ j ] j − w [ i ] &lt; 0 ; i &gt; = 1 i &lt; = n d[j] = \begin{cases} 0 &amp; &amp; i=0 or j=0 \\ max(d[j], d[j-w[i]]+v[i]) &amp; &amp; j-w[i]&gt;=0; i&gt;=1&amp;i&lt;=n\\ d[j] &amp; &amp; j-w[i]&lt;0;i&gt;=1&amp;i&lt;=n \end{cases} d[j]=0max(d[j],d[jw[i]]+v[i])d[j]i=0orj=0jw[i]>=0;i>=1jw[i]<0;i>=1i<=ni<=n
  • 初始化:d[j] = 0 (j>=0 && j<=n)
  • 注意事项:因为是0-背包问题,所以每一个种类只允许出现一次,则需要使其j的遍历逆序。因为在更新d[j]时需要保证其依赖数值d[j-w[i]]没有包含第i个物品,如果是正序遍历,则先
#include <iostream>
#include <cstring>
using namespace std;

const int MAX = 101;
struct Node
{
    int w, v;
};
Node u[MAX];
int dp[MAX*10];

void Init()
{
    memset(dp, 0, sizeof(dp));//也可以只对dp[0]赋值为0
}

int main()
{
    int T, M;
    while(cin >> T >> M)
    {
        for(int i=1; i<=M; i++)
        {
            cin >> u[i].w >> u[i].v;
        }
        Init();
        for(int i=1; i<=M; i++)
        {
            for(int j=T; j>=u[i].w; j--)//从后向前
            {
                dp[j] = max(dp[j], dp[j-u[i].w]+u[i].v);
            }
        }
        cout << dp[T] << endl;
    }
    return 0;
}
Piggy-Bank(完全背包)

题目7.8链接Piggy-Bank
题目大意:有一个存储罐,告知其空时的重量和当前的重量,并给定一些钱币的价值和相应的的重量,求存储罐中最少有多少现金。
思路:
基本与0-1背包相同,只是遍历方向相反。

  • 构造状态:d[j]表示当重量为j时,最少有的价值。
  • 终态:d[W]。W为存储罐中装的钱币的总重量。
  • 状态转移方程:
    d [ j ] = { 0 i = 0 o r j = 0 m i n ( d [ j ] , d [ j − w [ i ] ] + v [ i ] ) j − w [ i ] &gt; = 0 ; i &gt; = 1 i &lt; = n d [ j ] j − w [ i ] &lt; 0 ; i &gt; = 1 i &lt; = n d[j] = \begin{cases} 0 &amp; &amp; i=0 or j=0 \\ min(d[j], d[j-w[i]]+v[i]) &amp; &amp; j-w[i]&gt;=0; i&gt;=1&amp;i&lt;=n\\ d[j] &amp; &amp; j-w[i]&lt;0;i&gt;=1&amp;i&lt;=n \end{cases} d[j]=0min(d[j],d[jw[i]]+v[i])d[j]i=0orj=0jw[i]>=0;i>=1jw[i]<0;i>=1i<=ni<=n
  • 初始化:d[j] = INF (j>0 && j<=n) 即表示状态不可用, d[0] = 0
  • 注意事项:对于完全背包问题,每一个种类的数量都是无穷的,因此就不存在0-1背包中的需要保证d[j-w[i]]中没有包含第i个种类的情况。所以这个可以正序遍历。而且状态转移方程与0-1背包完全一样。
    代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int MAX = 600;
const int INF = 1<<28;
struct Node
{
    int w, v;
};
Node u[MAX];
int dp[10001];

void Init(int n)
{
    dp[0] = 0;
    for(int i=1; i<=n; i++)
        dp[i] = INF;
}

int main()
{
    int T, E, F, n, W;
    scanf("%d", &T);
    while(T--)
    {
        //cin >> E >> F;
        scanf("%d%d", &E, &F);
        W = F-E;
        scanf("%d", &n);
        for(int i=1; i<=n; i++)
        {
            scanf("%d%d", &u[i].v, &u[i].w);
            //cin >> u[i].v >> u[i].w;
        }

        //注意初始化时要根据参数大小选择初始化范围,否则超时
        Init(W);

        //相当于先按种类来放
        for(int i=1; i<=n; i++)
        {
            for(int j=u[i].w; j<=W; j++)
            {
                  dp[j] = min(dp[j], dp[j-u[i].w]+u[i].v);
            }
        }
        if(dp[W]>=INF)
        {
            printf("This is impossible.\n");
        }
        else
        {
            printf("The minimum amount of money in the piggy-bank is %d.\n", dp[W]);
        }
    }
    return 0;
}
珍惜现在,感恩生活(多重背包)

题目7.9链接珍惜现在,感恩生活
题目大意:你有n元钱,市场上有m中大米,每一种大米均有其每袋的价格,每袋的重量以及对应种类大米的袋数。问你最多可以买多重的大米。
思路:
多重背包可以直接看成0-1背包来解决,即将每一种的每袋大米均看成不同种类,但可能使得种类太多,增大了复杂度。
通过将每一种大米拆若干组,分成1,2,4,8…k-2 ^c +1袋 (其中,c为使k-2^c+1大于0的最大整数)。可以简化其复杂度,并且这些组拼接起来可以组合成任意的数量。
w[i]代表第i种米的重量,v[i]代表第i种米的价钱。

  • 构造状态:d[j]代表拥有j元钱可以购买的最大重量的大米的重量值。
  • 终态:d[n]。n为拥有的钱。
  • 状态转化方程: d [ j ] = { 0 i = 0 o r j = 0 m a x ( d [ j ] , d [ j − w [ i ] ] + v [ i ] ) j − w [ i ] &gt; = 0 ; i &gt; = 1 i &lt; = n d [ j ] j − w [ i ] &lt; 0 ; i &gt; = 1 i &lt; = n d[j] = \begin{cases} 0 &amp; &amp; i=0 or j=0 \\ max(d[j], d[j-w[i]]+v[i]) &amp; &amp; j-w[i]&gt;=0; i&gt;=1&amp;i&lt;=n\\ d[j] &amp; &amp; j-w[i]&lt;0;i&gt;=1&amp;i&lt;=n \end{cases} d[j]=0max(d[j],d[jw[i]]+v[i])d[j]i=0orj=0jw[i]>=0;i>=1jw[i]<0;i>=1i<=ni<=n
  • 初始化:d[j] = 0, j>=0 &&j<n

代码:

#include <iostream>
#include <cstring>
using namespace std;

const int MAX = 101;
int dp[MAX];
struct Node
{
    int w, v;
};
Node u[MAX*20];

void Init()
{
    memset(dp, 0, sizeof(dp));
}

int main()
{
    int T, n, m, w, v, k;
    cin >> T;
    while(T--)
    {
        cin >> n >> m;
        int cnt = 0;//拆分后的总数量
        for(int i=1; i<=m; i++)
        {
            cin >> v >> w >> k;
            int c = 1;
            while(k-c>0)
            {
                k -= c;
                u[++cnt].w = c*w;
                u[cnt].v = c*v;
                c *= 2;
            }
            u[++cnt].w = w*k;
            u[cnt].v = v*k;
        }
        Init();
        for(int i=1; i<=cnt; i++)
        {
            for(int j=n; j>=u[i].v; j--)
            {
                dp[j] = max(dp[j], dp[j-u[i].v]+u[i].w);
            }
        }
        cout << dp[n] << endl;
    }
    return 0;
}

不优化
三重循环,限制每一种大米的袋数。

//HDU 2191 多重背包 
#include<stdio.h>
int val[110],wei[110],count[110],dp[110];//价格,重量,袋数。动态背包 
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n,m;// 钱数,种类 
		scanf("%d%d",&n,&m);
		for(int i=0;i<m;i++)
		{
			scanf("%d%d%d",val+i,wei+i,count+i);
		}
		for(int i=0;i<=n;i++) dp[i]=0;//初始为0 
		
		for(int i=0;i<m;i++) //第 i 种大米 
			for(int k=1;k<=count[i];k++) //大米 i 放入的次数 
				for(int j=n;j>=val[i];j--) // 动态规划 
					dp[j]=max(dp[j],dp[j-val[i]]+wei[i]);
		printf("%d\n",dp[n]); //数组dp存的是重量 
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值