题目
有若干块大理石,其大小及美观程度不一,为了比较客观的分割这些大理石,我们需要先给这些大理石一个评分,评分分为6个等级,分别用1~6的数字来表示。现希望将这些大理石分成两部分,使每部分的评分之和相同。
输入:
输入一行,包括6个数,分别是每个等级的大理石的数量。每种等级的大理石数量不超过20000.
输出:
如果这些大理石能否分割成评价等级之和相同的两部分,则输出true,否则输出false.
样例输入:
1 0 1 2 0 0
样例输出:
false
要求
1、写出求解样例输入时的求解过程。
2、写出算法分析过程,编写程序求解上述问题,并分析算法的时间复杂度。
参考链接
这是力扣关于01背包的讲解,看懂了就知道怎么做这道题了。
分析
本题是多重背包问题,只要会01背包就能做多重背包。所以,我们不用递归,用动态规划来做。只用填一张二维表,先不说为什么。
题目输入是“1 0 1 2 0 0”,也就是有1个等级为1的大理石,有1个等级为3的,有2个等级为4的。把它转化为大理石的数组:num=[1,3,4,4]。要分割为两半,则每一部分是(1+3+4+4)/2=6个等级,用这四个数能凑出6我们就能返回true。
现在构建二维表,我们用二维数组dp[][]表示。行是大理石数组num的元素,列是需要凑出的等级,表格则是判断true还是false。如果我们只凑6,为什么要把1到5都写出来呢,后面再说。
num | 1 | 2 | 3 | 4 | 5 | 6 |
1 | ||||||
3 | ||||||
4 | ||||||
4 |
现在开始一行一行地填表。
1、填第一行的时候,第一个空格代表背包里只有1,要凑出1,明显是可以的,dp[num[0]][1]填true。后面的2到6是凑不出来的,都是false。
2、填第二行的时候,注意,这里不是背包里只有3的意思,而是1和3。
3、填第三行的时候,代表背包里有1,3,4。
4、填第四行的时候,代表背包里有1,3,4,4
所以,为什么凑6需要把1-5都列出来,就是为了查背包里有没有其他大理石能和当前大理石一起凑到某个数。
第一行我们总是只能填一个T,就是行=列的时候,因为一个数只能凑它本身的数。在填第二行的时候,对于小于3的列,直接照抄上一行;对于等于3的列,直接填T;对于大于3的列,用列减3得到的差,去查上一行且列等于这个差的格子(也就是看背包里有没有等于这个差的数),如果是T,代表背包里有这个数,填T,如果是F代表背包里没有这个数,填F。
完整代码如下:(时间紧迫,不注释了,想着那个填表就知道咋写了)
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<string.h>
#define MAXN 120000
bool distribution(int* num, int numSize);
int main()
{
int num[MAXN]={0};
int n;
int cnt=0;
// 输入6个数
for(int i=1;i<=6;i++)
{
scanf("%d",&n);
// 多重背包数组转01背包数组,也就是每个大理石的等级组成的数组
for(int j=0;j<n;j++)
{
num[cnt]=i;
cnt++;
}
}
// 大理石数组,和它的长度传进去
if(distribution(num,cnt))
printf("true");
else
printf("false");
return 0;
}
// 填表
bool distribution(int* num, int numSize)
{
// 只有1块大理石凑啥啊还,裂成两半吧
if (numSize < 2) {
return false;
}
int sum = 0, maxNum = 0;
// 大理石的等级加起来
for (int i = 0; i < numSize; ++i) {
sum += num[i];
maxNum = fmax(maxNum, num[i]);
}
// 大理石的等级加起来是奇数凑啥啊还,走走走
if (sum & 1) {
return false;
}
// 大理石等级和的一半才是最终要凑出的数
int target = sum / 2;
// 啥,连最大那块大理石都比目标数大?凑啥啊还
if (maxNum > target) {
return false;
}
// 表来了,就是它,二维数组dp
int dp[numSize][target + 1];
// 初始化dp为全F
memset(dp, 0, sizeof(dp));
// 填第一行,行和列相等的就是T
dp[0][num[0]] = true;
// 从第二行开始填
for (int i = 1; i < numSize; i++) {
int row = num[i];
for (int j = 1; j <= target; j++) {
// 列的数大于行的数,用列减去当前行的差,去找上一行列数等于这个差的状态
if (j >= row) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - row];
// 列数小于行数,直接照抄上一行
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
// 返回表格右下角那个格,因为一旦上一行为T,后面所有行都为T
return dp[numSize - 1][target];
}
算法的时间复杂度为
这不是最优的,最优的是把dp变成一维数组。因为某个格子是T,则同列的后面每行都是T,所以dp不是二维的也可以。这种办法的时间复杂度变成 ,时间关系就不再讲了。