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
#include
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
#include
#include
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
#include
#include
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