糖果
糖果
糖果店的老板一共有 $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
解题思路
这是一个重复覆盖问题,一个经典问题。与之相对的还有一个精确覆盖问题。
我们把每一种糖看成一列,一共有$1 \sim m$列,把每一包糖看成一行,一共有$1 \sim n$行。
根据样例,我们画出这个矩阵。其中如果某包糖果有重复的某种糖,我们只记一次。矩阵中的$1$代表这一包糖含有这种糖。
因此问题变成,我们至少选多少行,使得每一列都至少存在一个$1$。
这就是重复覆盖问题。精确覆盖问题就是至少选多少行,使得每列都恰好只有一个$1$。
方法是用dfs。搜索顺序是,每次找一个还未被覆盖的列,然后枚举所有可以覆盖这一列的行,每次枚举都选一行,然后递归到下层继续搜索。
直接这样搜的话会超时,所以需要进行优化。
其中一个优化是迭代加深。就是从小到大枚举答案。我们每次搜索都限制可以搜索到的行数数量。由于我们的答案是选择行数的最小值,因此从$1 \sim m$来枚举每次可以搜索到的行数数量。比如我们看看选一行可不可以覆盖所有的列,如果不可以,就看看选两行可不可以覆盖所有的列......以此类推,直到找到某个值的行数可以覆盖所有的列。
另外一个优化是,由于我们每次搜索都可以选择任意一个未被覆盖的列,那么我们应该在这些未被覆盖的列中,选择包含可选择行数最少的那一列。比如在样例中,第一次搜索应该选择搜第$4$列,因为第$4$列可选择的行数最少。这是因为这样每次都可以搜索分支少的情况,可以提高搜索的效率。
还有一个优化是可行性剪支。每次搜索时,我们都根据当前的状态(已选择的列)估计判断一下至少还需要选择多少行可以覆盖所有列。如果至少选择的行数超过还可以选择的行数,那么就return,不再继续搜下去。估计的方法是,如果某一列没有选,那么我们把所有可以覆盖这一列的行全部选上,但只算一行的数量。然而事实是加上这个剪支后运行的时间反而变久。
最后还可以对每一包糖果进行状态压缩,用二进制来表示每包糖果包含哪种糖。如果第$i$位为$1$表示这包糖含有第$i$种糖。同时每次搜索的当前状态也进行状态压缩,如果如果第$i$位为$1$表示第$i$列已经被覆盖了,$0$的话表示第$i$列还没被覆盖。
AC代码如下:
1 #include2 #include 3 #include 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 int dep = 1; // 迭代加深,从1开始枚举 74 while (dep <= m && !dfs(dep, 0)) { // 最多可以搜m行,超过m行说明不可以覆盖所有列。如果还没有覆盖所有列,继续搜索 75 dep++; 76 } 77 printf("%d", dep > m ? -1 : dep); 78 79 return 0; 80 }
可以把h函数去掉,运行时间会更短(至少这题是这样)。还可以把迭代加深的枚举改为二分。
AC代码如下:
1 #include2 #include 3 #include 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 int left = 1, right = m + 1; 54 while (left < right) { 55 int mid = left + right >> 1; 56 if (dfs(mid, 0)) right = mid; 57 else left = mid + 1; 58 } 59 printf("%d", left > m ? -1 : left); 60 61 return 0; 62 }
另外再给出一种dfs的实现方式,这种方式的效率没有前面的效率高,但可以过。
1 #include2 #include 3 #include 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 dfs(0, 0); 57 printf("%d", ans); 58 59 return 0; 60 }
参考资料
AcWing 1243. 糖果(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/769/