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

你可能感兴趣的:(POJ1837 01背包)