POJ1837 01背包

POJ1837

题目

  大意是有一个“特殊”的天平,天平在不同位置分布着C(\(2\le C\le 20\))个挂钩,挂钩的位置坐标从-15到+15(-代表左臂,+代表右臂)。你有G(\(2\le G\le 20\))个砝码,砝码质量从1到25。问给定C个挂钩的位置坐标,G个砝码的质量,你有多少种悬挂方式使得天平平衡。

Sample Input

2 4	
-2 3 
3 4 5 8

Sample Output

2

  有2种悬挂方式如下:

\[\begin{equation} (-2) * (3 + 4 + 5) + (3) * (8) = 0\quad or \quad (-2) * (4 + 8) + (3) * (3 + 5) = 0 \end{equation} \]

算法思路

  借鉴01背包的思想,本题中的砝码可以类比01背包中的物品,本题中将砝码挂到天平的挂钩上类比01背包中将物品放入背包中。01背包的状态定义:opt[i][j]表示前i件物品放入容量为j的背包中可以获得的最大价值,本题类似,opt[i][j]种,i可以表示前i个砝码,那么本题中j代表什么?这一点说不好想还真是不好想,如果动态规划比较熟可能比较容易想出来。仔细观察本题,题目要求有多少种悬挂方式使天平“平衡”,平衡即如式(1)所示,即所有砝码质量乘对应挂钩的位置坐标的和等于零,而我们可以让j表示这个“和”,那么opt[i][j]即表示前i个砝码悬挂到天平上,导致“和”为j可能的悬挂方式有多少种。进而可以推导出递推公式:

\[\begin{equation} opt[i][j+hooks[k] * weights[i]] = opt[i][j+hooks[k] * weights[i]] + opt[i - 1][j] \end{equation} \]

\(hooks[k]\)表示第k个挂钩的坐标,\(weight[i]\)表示第i个砝码的质量,式(2)简单地说,当你把砝码i悬挂在挂钩k上时,“和”会由j变为\(j+hooks[k] * weights[i]\),则前i个砝码导致“和”为\(j+hooks[k] * weights[i]\)的悬挂方式增多\(opt[i - 1][j]\)种。注意标红的“增多”二字,表明可能还有其它悬挂方式达到\(j+hooks[k] * weights[i]\)这个值,所以应该用\(opt[i - 1][j]\)加上原先的值。而当你准备要放第i个砝码时,你可以悬挂在任意一个挂钩上,所以\(1\le k\le C\)

  另外,由题目的输入限制,可以确定j的范围为:\(-15\times20\times25=-7500\le j\le 7500=15\times20\times25\),j的范围包含负数,但是opt数组下标索引不允许出现负数,所以把j的范围向右平移7500,则范围变为\(0\le j \le 15000\)。题目初始化时,opt[0][7500]为1,表示未放砝码时,“和”为7500的悬挂方式有1种。最终输出结果为opt[g][7500],表示放了g个砝码,“和”仍为7500,即仍然保持未放砝码的状态,即平衡状态。

代码

  若有0ms的实现方法,感谢评论告知。

朴素实现

Result: 1460kB, 47ms.

#include <stdio.h>
#include <iostream>

int c, g;
int hooks[20 + 5], weights[20 + 5];
int opt[20 + 5][15000 + 5];//最大右偏20*25*15=7500,最大左偏-7500。由于数组下标不能索引负数,整体向右偏移7500,得0-15000
int main() {
	scanf("%d %d", &c, &g);
	for (int i = 1; i <= c; i++)
		scanf("%d", &hooks[i]);
	for (int i = 1; i <= g; i++)
		scanf("%d", &weights[i]);
	opt[0][7500] = 1;//初始时,0件砝码是平衡的。
	for (int i = 1; i <= g; i++)
		for(int j = 0; j <= 15000; j++)//遍历所有可能出现的“和”
			if(opt[i - 1][j])
				for (int k = 1; k <= c; k++)
					opt[i][j + hooks[k] * weights[i]] += opt[i - 1][j];
	printf("%d\n", opt[g][7500]);
}

优化一下j的循环次数

Result: 936kB, 16ms

#include <stdio.h>
#include <iostream>
#include <limits.h>

int c, g;
int hooks[20 + 5], weights[20 + 5];
int opt[20 + 5][15000 + 5];//最大右偏20*25*15=7500,最大左偏-7500。由于数组下标不能索引负数,整体向右偏移7500,得0-15000
int main() {
	scanf("%d %d", &c, &g);
	scanf("%d", &hooks[1]);
	int min_hook = hooks[1], max_hook = min_hook;
	for (int i = 2; i <= c; i++) {//找出最小的位置坐标和最大的位置坐标
		scanf("%d", &hooks[i]);
		if (hooks[i] < min_hook)
			min_hook = hooks[i];
		else if (hooks[i] > max_hook)
			max_hook = hooks[i];
	}
	int sum_weight = 0;
	for (int i = 1; i <= g; i++) {
		scanf("%d", &weights[i]);
		sum_weight += weights[i];//砝码质量和
	}
		
	opt[0][7500] = 1;//初始时,0件砝码是平衡的。
	for (int i = 1; i <= g; i++)
		for (int j = 7500 + sum_weight * min_hook; j <= 7500 + sum_weight * max_hook; j++)//j的遍历范围从所有砝码放在min_hook到所有砝码放在max_hook
			if (opt[i - 1][j])
				for (int k = 1; k <= c; k++)
					opt[i][j + hooks[k] * weights[i]] += opt[i - 1][j];
	printf("%d\n", opt[g][7500]);
}

优化一下空间复杂度

  从式(2)的状态转移方程可以看出,求opt[i]这一行的值只需要opt[i-1]这一行的值就可以了,而更之前的如opt[i-2]、opt[i-3]...这些行的值是不需要的。不过跟普通背包问题有点不一样,普通背包问题,有一个递增更改的关系(更改opt[i][j]只需要opt[i][k]的值就可以,其中k小于j),所以倒序遍历j,使得只需要一行即可\(^{[1]}\),而这个题不存在这种关系,所以需要两行。

Result: 732kB, 16ms

  在朴素实现的基础上更改的,空间小了一半。时间上的变化不重要了,前后两次提交,一次是32ms,一次是16ms,空间两次都是固定732kB。

#include <stdio.h>
#include <string.h>
#include <iostream>

int c, g;
int hooks[20 + 5], weights[20 + 5];
int opt[2][15000 + 5];//最大右偏20*25*15=7500,最大左偏-7500。由于数组下标不能索引负数,整体向右偏移7500,得0-15000
int main() {
	scanf("%d %d", &c, &g);
	for (int i = 1; i <= c; i++)
		scanf("%d", &hooks[i]);
	for (int i = 1; i <= g; i++)
		scanf("%d", &weights[i]);
	opt[0][7500] = 1;//初始时,0件砝码是平衡的。
	for (int i = 1; i <= g; i++) {
		int index = i % 2, index_minus_1 = index ^ 1;//i为奇数,对opt[1]进行更改,i为偶数,对opt[0]进行更改
		memset(opt[index], 0, sizeof(opt[index]));
		for (int j = 0; j <= 15000; j++) {
			if (opt[index_minus_1][j])
				for (int k = 1; k <= c; k++)
					opt[index][j + hooks[k] * weights[i]] += opt[index_minus_1][j];
		}
	}
	printf("%d\n", opt[g % 2][7500]);//g为奇数,输出opt[1][7500],g为偶数,输出opt[0][7500]
}

参考:

[1] 背包问题九讲 2.0

posted @ 2019-12-25 15:47  wtyuan  阅读(211)  评论(0编辑  收藏  举报