糖果

糖果

糖果店的老板一共有 M 种口味的糖果出售。

为了方便描述,我们将 M 种口味编号 1M

小明希望能品尝到所有口味的糖果。

遗憾的是老板并不单独出售糖果,而是 K 颗一包整包出售。

幸好糖果包装上注明了其中 K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。

给定 N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。

输入格式

第一行包含三个整数 N,M,K

接下来 N 行每行 K 这整数 T1,T2,,TK,代表一包糖果的口味。

输出格式

一个整数表示答案。

如果小明无法品尝所有口味,输出 1

数据范围

1N100,
1M,K20,
1TiM

输入样例:

6 5 3
1 1 2
1 2 3
1 1 3
2 3 5
5 4 2
5 1 2

输出样例:

2

 

解题思路

dfs

  这是一个重复覆盖问题,一个经典问题。与之相对的还有一个精确覆盖问题。

  我们把每一种糖看成一列,一共有1m列,把每一包糖看成一行,一共有1n行。

  根据样例,我们画出这个矩阵。其中如果某包糖果有重复的某种糖,我们只记一次。矩阵中的1代表这一包糖含有这种糖。

  因此问题变成,我们至少选多少行,使得每一列都至少存在一个1

  这就是重复覆盖问题。精确覆盖问题就是至少选多少行,使得每列都恰好只有一个1

  方法是用dfs。搜索顺序是,每次找一个还未被覆盖的列,然后枚举所有可以覆盖这一列的行,每次枚举都选一行,然后递归到下层继续搜索。

  直接这样搜的话会超时,所以需要进行优化。

  其中一个优化是迭代加深。就是从小到大枚举答案。我们每次搜索都限制可以搜索到的行数数量。由于我们的答案是选择行数的最小值,因此从1m来枚举每次可以搜索到的行数数量。比如我们看看选一行可不可以覆盖所有的列,如果不可以,就看看选两行可不可以覆盖所有的列......以此类推,直到找到某个值的行数可以覆盖所有的列。

  另外一个优化是,由于我们每次搜索都可以选择任意一个未被覆盖的列,那么我们应该在这些未被覆盖的列中,选择包含可选择行数最少的那一列。比如在样例中,第一次搜索应该选择搜第4列,因为第4列可选择的行数最少。这是因为这样每次都可以搜索分支少的情况,可以提高搜索的效率。

  还有一个优化是可行性剪支。每次搜索时,我们都根据当前的状态(已选择的列)估计判断一下至少还需要选择多少行可以覆盖所有列。如果至少选择的行数超过还可以选择的行数,那么就return,不再继续搜下去。估计的方法是,如果某一列没有选,那么我们把所有可以覆盖这一列的行全部选上,但只算一行的数量。然而事实是加上这个剪支后运行的时间反而变久。

  最后还可以对每一包糖果进行状态压缩,用二进制来表示每包糖果包含哪种糖。如果第i位为1表示这包糖含有第i种糖。同时每次搜索的当前状态也进行状态压缩,如果如果第i位为1表示第i列已经被覆盖了,0的话表示第i列还没被覆盖。

  更新:现在数据加强了,还需要增加一个优化。就是对于每一列,需要把重复的行删除,不然会进行重复的搜索。

  比如这组数据:

复制代码
100 20 20
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
10 10 1...
Data
复制代码

  AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110, M = 1 << 20;
 7 
 8 int n, m, k;
 9 vector<int> col[N];
10 int lg2[M];
11 
12 int lowbit(int x) {
13     return x & -x;
14 }
15 
16 int get(int state) {
17     int ret = 0;
18     for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
19         ret++;  // 不论选多少行,都记为一行
20         int c = lg2[lowbit(i)];
21 
22         // 更新状态,更新已选择的列
23         for (auto &it : col[c]) {
24             i &= (1 << m) - 1 - it;
25         }
26     }
27 
28     return ret;
29 }
30 
31 bool dfs(int dep, int state) {
32     // 可选择的行数为0,并且或者至少选择的行数超过可选择的行数,则放回当前状态是否以选择所有的列
33     if (dep == 0 || get(state) > dep) return state == (1 << m) - 1;
34 
35     // 找到包含可选择行数最少的那一列
36     int c = -1;
37 
38     // 这里进行按位取反,原本1表示这列以选,取反后1表示这列未选
39     for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
40         int t = lg2[lowbit(i)];
41         if (c == -1 || col[c].size() > col[t].size()) c = t;
42     }
43 
44     // 搜索每一个可选的行
45     for (auto &it : col[c]) {
46         if (dfs(dep - 1, state | it)) return true;
47     }
48 
49     return false;
50 }
51 
52 int main() {
53     scanf("%d %d %d", &n, &m, &k);
54 
55     // 预处理log(2^i)
56     for (int i = 0; i < m; i++) {
57         lg2[1 << i] = i;
58     }
59 
60     for (int i = 0; i < n; i++) {
61         int state = 0;
62         for (int j = 0; j < k; j++) {
63             int x;
64             scanf("%d", &x);
65             state |= 1 << x - 1;    // 映射到0~m-1列,第x-1位变为1,表示这行可以覆盖第x-1列
66         }
67 
68         for (int j = 0; j < m; j++) {
69             if (state >> j & 1) col[j].push_back(state);    // 如果第j位为1,说明第j列可以被这一行覆盖,把这一整行放入
70         }
71     }
72     
73     // 优化,每一列删除重复的行
74     for (int i = 0; i < m; i++) {
75         sort(col[i].begin(), col[i].end());
76         col[i].erase(unique(col[i].begin(), col[i].end()), col[i].end());
77     }
78 
79     int dep = 1;    // 迭代加深,从1开始枚举
80     while (dep <= m && !dfs(dep, 0)) {  // 最多可以搜m行,超过m行说明不可以覆盖所有列。如果还没有覆盖所有列,继续搜索
81         dep++;
82     }
83     printf("%d", dep > m ? -1 : dep);
84 
85     return 0;
86 }
复制代码

  可以把h函数去掉,运行时间会更短(至少这题是这样)。还可以把迭代加深的枚举改为二分。

  AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110, M = 1 << 20;
 7 
 8 int n, m, k;
 9 vector<int> col[N];
10 int lg2[M];
11 
12 int lowbit(int x) {
13     return x & -x;
14 }
15 
16 bool dfs(int dep, int state) {
17     if (state == (1 << m) - 1) return true;
18     if (dep == 0) return false;
19 
20     int c = -1;
21     for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
22         int t = lg2[lowbit(i)];
23         if (c == -1 || col[c].size() > col[t].size()) c = t;
24     }
25 
26     for (auto &it : col[c]) {
27         if (dfs(dep - 1, state | it)) return true;
28     }
29 
30     return false;
31 }
32 
33 int main() {
34     scanf("%d %d %d", &n, &m, &k);
35 
36     for (int i = 0; i < m; i++) {
37         lg2[1 << i] = i;
38     }
39 
40     for (int i = 0; i < n; i++) {
41         int state = 0;
42         for (int j = 0; j < k; j++) {
43             int x;
44             scanf("%d", &x);
45             state |= 1 << x - 1;
46         }
47 
48         for (int j = 0; j < m; j++) {
49             if (state >> j & 1) col[j].push_back(state);
50         }
51     }
52     
53     for (int i = 0; i < m; i++) {
54         sort(col[i].begin(), col[i].end());
55         col[i].erase(unique(col[i].begin(), col[i].end()), col[i].end());
56     }
57 
58     int left = 1, right = m + 1;
59     while (left < right) {
60         int mid = left + right >> 1;
61         if (dfs(mid, 0)) right = mid;
62         else left = mid + 1;
63     }
64     printf("%d", left > m ? -1 : left);
65 
66     return 0;
67 }
复制代码

  另外再给出一种dfs的实现方式,这种方式的效率没有前面的效率高,但可以过。

复制代码
 1 #include <cstdio>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110, M = 1 << 20;
 7 
 8 int n, m, k;
 9 vector<int> col[N];
10 int lg2[M];
11 int ans = N;
12 
13 int lowbit(int x) {
14     return x & -x;
15 }
16 
17 void dfs(int cnt, int state) {
18     if (cnt >= ans) return; // 优化剪支
19     if (state == (1 << m) - 1) {
20         ans = cnt;
21         return;
22     }
23 
24     int c = -1;
25     for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
26         int t = lg2[lowbit(i)];
27         if (c == -1 || col[c].size() > col[t].size()) c = t;
28     }
29 
30     if (col[c].empty()) ans = -1;   // 如果某列未选择,并且不存在可以覆盖这一列的行,说明不存在覆盖所有列的方案
31     for (auto &it : col[c]) {
32         dfs(cnt + 1, state | it);
33     }
34 }
35 
36 int main() {
37     scanf("%d %d %d", &n, &m, &k);
38 
39     for (int i = 0; i < m; i++) {
40         lg2[1 << i] = i;
41     }
42 
43     for (int i = 0; i < n; i++) {
44         int state = 0;
45         for (int j = 0; j < k; j++) {
46             int x;
47             scanf("%d", &x);
48             state |= 1 << x - 1;
49         }
50 
51         for (int j = 0; j < m; j++) {
52             if (state >> j & 1) col[j].push_back(state);
53         }
54     }
55     
56     for (int i = 0; i < m; i++) {
57         sort(col[i].begin(), col[i].end());
58         col[i].erase(unique(col[i].begin(), col[i].end()), col[i].end());
59     }
60 
61     dfs(0, 0);
62     printf("%d", ans);
63 
64     return 0;
65 }
复制代码

 

状态dp

  这题还可以用状压dp来做。思想其实和01背包的思路是一样的,对于前n包糖果,每包糖果可以选或不选,然后用一个维度来记录状态(表示已选择的糖的种类),进行转移。

  其中si表示第i包糖果的状态,即第i包糖果含有糖的种类。f(i1,j(j & si))中的j(j & si)表示:从状态j(j & si)更新到j。那么如何得到转移前的状态呢,就是把si中含有的糖果从j中去掉,对应二进制表示的状态就是jsi中的某一位如果同时为1,则j的这一位变成0,否则不改变,比如j=11011, s=01010,那么j(j & si)=10001

  还有个问题就是,11011不一定是从10001转移过来的,也可能是从11001或者11011转移过来的。但又发现如果不考虑这个问题而直接按照这种做法来做,提交的代码是可以AC的。这是因为在计算状态时,是把所有的状态(1<<m)1都计算过一遍的,只要是si中出现过1的位,对应的状态都会被计算到。比如si=1010,那么状态10000010以及1010都会被视为有效状态而被计算。还有一种理解就是,如果把si中所有1的位在j中变为0,那么意味着去掉的糖果种类数会更多,即对应的上一个状态的糖果种类更少,说明更加好凑(糖果种类越少,那么所选的糖果包数更少)。

  还要注意的是要把f数组的第一维优化掉,否则就会爆内存。

  时间复杂度为O(n×2m),AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 110, M = 1 << 20;
 7 
 8 int a[N], f[M];
 9 
10 int main() {
11     int n, m, k;
12     scanf("%d %d %d", &n, &m, &k);
13     for (int i = 1; i <= n; i++) {
14         for (int j = 0; j < k; j++) {
15             int val;
16             scanf("%d", &val);
17             a[i] |= 1 << val - 1;
18         }
19     }
20     
21     memset(f, 0x3f, sizeof(f));
22     f[0] = 0;
23     for (int i = 1; i <= n; i++) {
24         // 把第一维优化后j需要从大到小遍历,避免再使用前就更新了上一层的状态
25         for (int j = (1 << m) - 1; j >= 0; j--) {
26             f[j] = min(f[j], f[j ^ (j & a[i])] + 1);
27         }
28     }
29     
30     printf("%d", f[(1 << m) - 1] == 0x3f3f3f3f ? -1 : f[(1 << m) - 1]);
31     
32     return 0;
33 }
复制代码

  解释一下j的遍历顺序。因为j(j & si)j,因此需要j需要从大到小遍历。但事实上j从小到大遍历也是可以的,因为j(j & si)是把si中所有1的位在j中变为0,即si中为1的位,在j(j & si)中一定是0,又因为从小到大枚举,且j(j & si)j,因此当枚举到j(j & si),一定不可能得到计算(不会认为是一个合法的状态),因为状态j(j & si)不含有第i包糖果中的任何一种糖。

 

参考资料

  AcWing 1243. 糖果(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/769/

  AcWing 1243. 糖果(01背包和状态压缩的结合):https://www.acwing.com/solution/content/23984/

posted @   onlyblues  阅读(79)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2021-03-18 遍历二叉树的递归与非递归代码实现
Web Analytics
点击右上角即可分享
微信分享提示