糖果

糖果

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

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

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

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

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

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

输入格式

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

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

输出格式

一个整数表示答案。

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

数据范围

$1 \leq N \leq 100$,
$1 \leq M,K \leq 20$,
$1 \leq T_{i} \leq M$

输入样例:

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

输出样例:

2

 

解题思路

dfs

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

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

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

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

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

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

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

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

  另外一个优化是,由于我们每次搜索都可以选择任意一个未被覆盖的列,那么我们应该在这些未被覆盖的列中,选择包含可选择行数最少的那一列。比如在样例中,第一次搜索应该选择搜第$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$包糖果,每包糖果可以选或不选,然后用一个维度来记录状态(表示已选择的糖的种类),进行转移。

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

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

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

  时间复杂度为$O \left( n \times 2^{m} \right)$,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 \wedge \left( j ~\&~ s_i \right) \leq j$,因此需要$j$需要从大到小遍历。但事实上$j$从小到大遍历也是可以的,因为$j \wedge \left( j ~\&~ s_i \right)$是把$s_{i}$中所有$1$的位在$j$中变为$0$,即$s_{i}$中为$1$的位,在$j \wedge \left( j ~\&~ s_i \right)$中一定是$0$,又因为从小到大枚举,且$j \wedge \left( j ~\&~ s_i \right) \leq j$,因此当枚举到$j \wedge \left( j ~\&~ s_i \right)$,一定不可能得到计算(不会认为是一个合法的状态),因为状态$j \wedge \left( j ~\&~ s_i \right)$不含有第$i$包糖果中的任何一种糖。

 

参考资料

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

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

posted @ 2022-03-18 22:47  onlyblues  阅读(72)  评论(0编辑  收藏  举报
Web Analytics