题解:P8687 [蓝桥杯 2019 省 A] 糖果
按某日某集训某作业要求写题解。
原题
[蓝桥杯 2019 省 A] 糖果
题目描述
糖果店的老板一共有 M M M 种口味的糖果出售。为了方便描述,我们将 M M M 种口味编号 1 1 1 ∼ M M M。
小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是 K K K 颗一包整包出售。
幸好糖果包装上注明了其中 K K K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。
给定 N N N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。
输入格式
第一行包含三个整数 N N N、 M M M 和 K K K。
接下来 N N N 行每行 K K K 这整数 T 1 , T 2 , ⋯ , T K T_1,T_2, \cdots ,T_K T1,T2,⋯,TK,代表一包糖果的口味。
输出格式
一个整数表示答案。如果小明无法品尝所有口味,输出 − 1 -1 −1。
样例 #1
样例输入 #1
6 5 3 1 1 2 1 2 3 1 1 3 2 3 5 5 4 2 5 1 2
样例输出 #1
2
提示
对于 30 % 30\% 30% 的评测用例, 1 ≤ N ≤ 20 1 \le N \le 20 1≤N≤20。
对于所有评测样例, 1 ≤ N ≤ 100 1 \le N \le 100 1≤N≤100, 1 ≤ M ≤ 20 1 \le M \le 20 1≤M≤20, 1 ≤ K ≤ 20 1 \le K \le 20 1≤K≤20, 1 ≤ T i ≤ M 1 \le T_i \le M 1≤Ti≤M。
蓝桥杯 2019 年省赛 A 组 I 题。
题目链接:P8687 [蓝桥杯 2019 省 A] 糖果 - 洛谷
(题目复制自洛谷)
题意分析
共有M种糖果,N种组合,每组K个糖果。
由题意,每组内同种糖果可重复。
求最少选择多少组糖果,可以凑齐所有种类。
如果无法凑齐,则输出-1。
解题
分析
-
暴力枚举:
将每组糖果组合,取凑齐所有种类的最小情况。
-
状态压缩DP:
很显然这是一道比较标准的状态压缩题。
具体思路
-
状态压缩
使用一个二进制数表示每一个状态下每种糖果是否被包含。
例如:样例数据中
1 1 2
对应的二进制数是11000
,5 4 2
对应01011
。如果选择2种组合,则将对应的两个二进制数做或运算
例如:选择
1 1 2
和5 4 2
,则包含1 2 4 5
糖果,对应11011
;二进制表示为 11000 ∣ 01011 = 11011 11000\space|\space01011=11011 11000 ∣ 01011=11011。 -
DP状态转移方程
d p i dp_i dpi表示品尝 i i i口味的状态所需的购买数量。
例如买
5 4 2
可以品尝的口味为01011
,那么 d p 001011 = 1 dp_{001011}=1 dp001011=1(只需买1包)若 已有 i i i组合,向 i i i组合中加入一包糖得到 j j j组合,那么 d p j = d p i + 1 dp_j=dp_i+1 dpj=dpi+1。
以 c a n d y k candy_k candyk表示第 k k k包糖果的口味,则: d p c a n d y k ∣ i = d p i + 1 dp_{candy_k|i}=dp_i+1 dpcandyk∣i=dpi+1
代码实现
输入数据
每包糖对应的口味使用candy数组表示:
unsigned int candy[105];
对于每包糖,分别读入口味编号:
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= k; j++)
{
unsigned int temp;
cin >> temp;
}
}
然后将口味编号转成二进制,并与这包糖已有的口味做或运算:
candy[i] = candy[i] | (1 << (temp - 1));
注意这种方法读入的二进制数据与之前具体思路中的二进制表示方法相反;
例如原本
5 4 2
对应01011
,此时变为对应11010
;但是不影响运算。
读完一包糖以后顺便初始化动态规划数组:
dp[candy[i]] = 1;
vis[candy[i]] = 1;
读取部分完整代码如下:
for (int i = 1; i <= n; i++)
{
candy[i] = 0;
for (int j = 1; j <= k; j++)
{
unsigned int temp;
cin >> temp;
candy[i] = candy[i] | (1 << (temp - 1));
}
dp[candy[i]] = 1;
vis[candy[i]] = 1;
}
动态规划
使用dp数组表示动态规划数组;
使用vis数组表示某一状态是否存在(可省略):
int dp[1 << 25];
bool vis[1 << 25];
每加入一包糖,更新整个dp数组:
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < (1 << (m + 1)); j++)
{
}
}
如果dp[j]
存在,则更新dp[j|candy[i]]
:
if (vis[j])
{
}
如果dp[j|candy[i]]
原本不存在,则直接更新;如果原本存在,则用min()
取更优情况:
if (!vis[j | candy[i]]) dp[j | candy[i]] = dp[j] + 1;
else dp[j | candy[i]] = min(dp[j] + 1, dp[j | candy[i]]);
最后更新vis数组:
vis[j | candy[i]] = 1;
动态规划部分完整代码如下:
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < (1 << (m + 1)); j++)
{
if (vis[j])
{
if (!vis[j | candy[i]]) dp[j | candy[i]] = dp[j] + 1;
else dp[j | candy[i]] = min(dp[j] + 1, dp[j | candy[i]]);
vis[j | candy[i]] = 1;
}
}
}
输出
最后输出数据:
if (!vis[(1 << m) - 1]) cout << -1;
else cout << dp[(1 << m) - 1];
如果无解输出-1;否则第4个点会WA
完整代码
#include <iostream>
using namespace std;
int n, m, k;
int dp[1 << 25];
bool vis[1 << 25];
unsigned int candy[105];
int main()
{
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
{
candy[i] = 0;
for (int j = 1; j <= k; j++)
{
unsigned int temp;
cin >> temp;
candy[i] = candy[i] | (1 << (temp - 1));
}
dp[candy[i]] = 1;
vis[candy[i]] = 1;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < (1 << (m + 1)); j++)
{
if (vis[j])
{
if (!vis[j | candy[i]]) dp[j | candy[i]] = dp[j] + 1;
else dp[j | candy[i]] = min(dp[j] + 1, dp[j | candy[i]]);
vis[j | candy[i]] = 1;
}
}
}
if (!vis[(1 << m) - 1]) cout << -1;
else cout << dp[(1 << m) - 1];
}