楼教主男人八题(第一题)

本文介绍了编程竞赛中的知名人物楼天城及其在POJ举办的编程挑战,重点讲解了一个关于使用不同面值硬币组合支付价格的问题,该问题转化为多重背包问题。通过实例解析了多重背包的解题思路,包括时间复杂度为O(n*m)的直接模板和优化后的O(n)解决方案,并提供了完整的AC代码。文章适合对算法和编程竞赛感兴趣的读者。
摘要由CSDN通过智能技术生成

楼天城,1986 年出生,高中毕业于杭州十四中。 2004 年保送进清华大学计算机系。2008年进入姚期智院士领导的清华大学理论计算机中心攻读博士。 2017年初,创办小马智行(pony.ai)。楼天城是中国公认的大学生计算机编程第一人,经常以一人单挑一个队,在CEOI、ACM 界无人不晓其大名,人称“楼教主”。

2004年,楼教主在 POJ(http://poj.org/) 举办了第一场"是男人就过八题"的主题比赛。2018年,举办了第二场。

绝大多数参赛选手只过了一道题,赛后纷纷表示做男人真难。

这是2004年的题目,已经十七年了,最多的一道通过了 6790 次,最少的仅有 582 次。

接下来让我们按照通过人数从高到低,依次体验下。今天先来看下 1742 Coins 这道题。

题目链接

http://poj.org/problem?id=1742

题目描述

People in Silverland use coins. They have coins of value A 1 , A 2 , A 3 . . . A n A_1,A_2,A_3 ... A_n A1,A2,A3...An Silverland dollar. One day Tony opened his money-box and found there were some coins. He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m. But he didn’t know the exact price of the watch.

You are to write a program which reads n , m , A 1 , A 2 , A 3 . . . A n n,m,A_1,A_2,A_3 ... A_n n,m,A1,A2,A3...An and C 1 , C 2 , C 3 . . . C n C_1,C_2,C_3 ... C_n C1,C2,C3...Cn corresponding to the number of Tony’s coins of value A 1 , A 2 , A 3 . . . A n A_1,A_2,A_3...A_n A1,A2,A3...An then calculate how many prices(form 1 to m) Tony can pay use these coins.

输入

The input contains several test cases. The first line of each test case contains two integers n( 1 ≤ n ≤ 100 1\le n \le 100 1n100),m(m\le 100000).

The second line contains 2n integers, denoting A 1 , A 2 , A 3 . . . A n , C 1 , C 2 , C 3 . . . C n A_1,A_2,A_3 ... A_n, C_1,C_2,C_3 ... C_n A1,A2,A3...An,C1,C2,C3...Cn ( 1 ≤ A i ≤ 100000 , 1 ≤ C i ≤ 1000 1\le A_i\le 100000,1\le C_i\le 1000 1Ai100000,1Ci1000).

The last test case is followed by two zeros.

输出

For each test case output the answer on a single line.

样例输入

3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0

样例输出

8
4

样例解释

样例共输入了两组数据,第一组:

3 10
1 2 4 2 1 1

总共有三种硬币:

  • 2 个面值 1 的。
  • 1 个面值 2 的。
  • 1 个面值 4 的。

拢共可以拼出8种总面值:

  • 总面值 1 : 1
  • 总面值 2 : 1+1; 2
  • 总面值 3 : 1+2
  • 总面值 4 : 1+1+2; 4
  • 总面值 5 : 1+4;
  • 总面值 6 : 1+1+4; 2+4
  • 总面值 7 : 1+2+4
  • 总面值 8 : 1+1+2+4

第二组:

2 5
1 4 2 1

总共可以拼出五种:

  • 总面值 1 :1
  • 总面值 2 :1+1
  • 总面值 4 :4
  • 总面值 5 :1+4
  • 总面值 6 :1+1+4

但是,不超过 5的只有四种。所以答案为 4。

解题思路

知识点多重背包

时间复杂度 O ( n ∗ m ) O(n*m) O(nm)

该场比赛的签到题。直接套上多重背包模板即可。下面简单介绍下多重背包。

多重背包的题目场景大都可抽象为:现有一个体积为 V V V 的背包,以及若干种物品,每种物品的重量为 w i w_i wi,体积为 v i v_i vi,数量为 c i c_i ci。问,该背包最多能放入多少重量的物品。

还有些较为简单的场景,会省略物品的重量属性,即物品只有体积和数量,问有多少种总体积不同的放置方案。

该题就是第二种场景:

  • 手表的价格上限m,可以视作背包的体积 V V V
  • 硬币的面值可以视作物品的体积 v i v_i vi
  • 硬币的数量就是物品的数量 c i c_i ci

那么问题n种硬币能凑出多少种不同的面值,等价的变成了n种物品能凑出多少种不同的体积

我们不妨把拼凑出体积 i 的方案表示为集合 S i S_i Si。很显然, S 0 S_0 S0 是一个空集。接下来,以 S 0 S_0 S0 为基础,构造 i ∈ ( 0 , V   ] i∈(0,V\ ] i(0,V ] S i S_i Si

不妨举个具体的例子: V = 10 V=10 V=10,且有两种物品:

  • 第一种:1 个体积为 4 的物品。
  • 第二种:2 个体积为 2 的物品。

首先,向 S 0 S_0 S0 放入一个体积为 4 的物品,得到:
S 4 = S 0 + { 4 } = { 4 } S_4 = S_0 + \{4\} = \{4\} S4=S0+{4}={4}

现在我们有了三个集合:

  • S 0 = { } S_0 = \{\} S0={}
  • S 4 = { 4 } S_4 = \{4\} S4={4}

继续处理第二种物品,以上述三个集合为基础,放入第一个 2:

  • S 2 = S 0 + { 2 } = { 2 } S_2 = S_0 + \{2\} = \{2\} S2=S0+{2}={2}
  • S 6 = S 4 + { 2 } = { 2 , 4 } S_6 = S_4 + \{2\} = \{2,4\} S6=S4+{2}={2,4}

接着,向 S 2 , S 6 S_2, S_6 S2,S6 中放入第二个 2。

  • S 4 = S 2 + { 2 } = { 2 , 2 } S_4 = S_2 + \{2\} = \{2,2\} S4=S2+{2}={2,2}
  • S 8 = S 6 + { 2 } = { 2 , 2 , 4 } S_8 = S_6 + \{2\} = \{2,2,4\} S8=S6+{2}={2,2,4}

为何不用再尝试一次 S 0 , S 4 S_0, S_4 S0,S4呢?因为两个体积为 2 的物品无差别,这样重复尝试无意义~

现在拼出了五种总体积,甚至有些体积有多种拼凑方案:

  • S 0 = { } S_0 = \{\} S0={}
  • S 2 = { 2 } S_2 = \{2\} S2={2}
  • S 4 = { 2 , 2 } = { 4 } S_4 = \{2,2\} = \{4\} S4={2,2}={4}
  • S 6 = { 2 , 4 } S_6 = \{2,4\} S6={2,4}
  • S 8 = { 2 , 2 , 4 } S_8 = \{2,2,4\} S8={2,2,4}

用代码实现一下上述构造过程:

// 只关心Si是否存在,所以用 bool 数组表示。
// true 为存在,false为不存在。
// 初始时,只有S0存在
bool S[V+1] = {false};
S[0] = true;
// 枚举物品种类
for (int i = 0; i < goods.size(); i++) {
  const auto &g = goods[i];
  // 第i种物品的体积为 vol,数量为 num
  for (int j = 0; j < g.num; j++) {
    for (int k = V; k >= g.vol; k--) {
      // Sk 为true可分为两种情形:
      // 1. 在此之前已经拼凑出 k 了。
      // 2. 已拼出了 S[k-vol],放入一个 vol 即得到 S[k]。
      S[k] = S[k] || S[k-g.vol];
    }
  }
}

上述代码的时间复杂度为 O ( V ∗ ∑ i = 0 n n u m i ) O(V * \sum_{i=0}^{n} num_i) O(Vi=0nnumi),这其实是把多重背包当错01背包处理。

接下来,给出 O ( V ∗ ∑ i = 0 n 1 ) O(V * \sum_{i=0}^{n} 1) O(Vi=0n1) ,即 O ( V ∗ n ) O(V*n) O(Vn) 的思路。在优化之前,先来看看完全背包的写法:

// 只关心Si是否存在,所以用 bool 数组表示。
// true 为存在,false为不存在。
// 初始时,只有S0存在
bool S[V+1] = {false};
S[0] = true;
// 枚举物品种类
for (int i = 0; i < goods.size(); i++) {
  const auto &g = goods[i];
  // 完全背包不会限制物品数量
  for (int k = g.vol; k <= V; k++) {
    S[k] = S[k] || S[k-g.vol];
  }
}

在完全背包的基础上,增加数组 u s e d used used 完成对物品数量的限制。 u s e d i used_i usedi 表示 S i S_i Si 中当前物品的数量,当 S i S_i Si 有多种方案时,应选取 u s e d i used_i usedi 最小的方案。

// 只关心Si是否存在,所以用 bool 数组表示。
// true 为存在,false为不存在。
// 初始时,只有S0存在
bool S[V+1] = {false};
S[0] = true;
// 枚举物品种类
for (int i = 0; i < goods.size(); i++) {
  const auto &g = goods[i];
  memset(used, 0, sizeof(used));
  for (int k = g.vol; k <= V; k++) {
    if (!S[k] && S[k-g.vol] && used[k-g.vol] < g.num) {
      S[k] = true;
      used[k] = used[k-g.vol] + 1;
    }
  }
}

下面试完整的 Accepted 的代码。

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

using namespace std;

int val[100];
int cnt[100];
bool pack[100001];
int  used[100001];

int main() {
  int n, m;
  while(scanf("%d %d", &n, &m), n != 0 && m != 0) {
    for (int i = 0; i < n; i++) {
      scanf("%d", val+i);
    }

    for (int i = 0; i < n; i++) {
      scanf("%d", cnt+i);
    }

    memset(pack, 0, sizeof(bool)*(m+1));
    pack[0] = true;

    for (int i = 0; i < n; i++) {
      memset(used, 0, sizeof(int)*(m+1));
      for (int j = val[i]; j <= m; j++) {
        int pre = j - val[i];
        if (pack[pre] && pack[j] == false && used[pre] < cnt[i]) {
          pack[j] = true;
          used[j] = used[pre] + 1;
        }
      }
    }

    int anw = 0;
    for (int i = 1; i <= m; i++) {
      anw += pack[i];
    }
    printf("%d\n", anw);
  }
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值