问题描述
糖果店的老板一共有M种口味的糖果出售。为了方便描述,我们将M种口味编号1~M。
小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是K颗一包整包出售。
幸好糖果包装上注明了其中K颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。
给定包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。
【输入】
第一行包含三个整数
N
N
N、
M
M
M 和
K
K
K。
接下来
N
N
N行每行
K
K
K这整数
T
1
T_1
T1,
T
2
T_2
T2,…,
T
K
T_K
TK,代表一包糖果的口味。
1<=
N
N
N<=100,1<=
M
M
M<=20,1<=
K
K
K<=20,1<=
T
i
T_i
Ti<=M。
【输出】
一个整数表示答案。如果小明无法品尝所有口味,输出-1。
【样例输入】
6 5 3
1 1 2
1 2 3
1 1 3
2 3 5
5 4 2
5 1 2
【样例输出】
2
题目解析
1.暴力破解
拿到题目我们可以直接想到暴力破解:对 n n n包糖果做任意组合,找到其中一种组合,能覆盖所有口味,并且需要的糖果包数量最少即可。但是当 n n n=100的时候,组合数就达到了 2 n 2^n 2n数量级,会严重超时。
2.状态压缩dp
动态规划( D y n a m i c p r o g r a m m i n g , d p Dynamic programming,dp Dynamicprogramming,dp)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,其所耗时间往往远少于朴素解法。
dp问题满足三大重要性质
最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
子问题重叠性质:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
无后效性:每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
dp分类
-
简单基础dp:这类dp主要是一些状态比较容易表示,转移方程比较好想,问题比较基本常见的。主要包括递推、背包、LIS(最长递增序列),LCS(最长公共子序列)。
-
区间dp:一般是枚举区间,把区间分成左右两部分,然后求出左右区间再合并。
-
树形dp:是建立在树这种数据结构上的dp,一般状态比较好想,通过dfs维护从根到叶子或从叶子到根的状态转移。
-
数位dp:主要用来解决统计满足某类特殊关系或有某些特点的区间内的数的个数,它是按位来进行计数统计的,可以保存子状态,速度较快。数位dp做多了后,套路基本上都差不多,关键把要保存的状态给抽象出来,保存下来。
-
概率(期望) dp:一般来说概率正着推,期望逆着推。有环的一般要用到高斯消元解方程。期望可以分解成多个子期望的加权和,权为子期望发生的概率,即 E ( a A + b B + … ) = a E ( A ) + b E ( B ) + … E(aA+bB+…) = aE(A) + bE(B) +… E(aA+bB+…)=aE(A)+bE(B)+…
-
状态压缩dp:这类问题有TSP、插头dp等。
-
数据结构优化的dp:有时尽管状态找好了,转移方程的想好了,但时间复杂度比较大,需要用数据结构进行优化。常见的优化有二进制优化、单调队列优化、斜率优化、四边形不等式优化等。
本题思路
-
定义状态 d p [ i ] dp[i] dp[i],表示得到口味组合 i i i所需要的最少糖果包数量。
-
状态转移。往口味组合 i i i中加入一包糖果,设得到新的口味组合 j j j,说明从 i i i到j需要糖果包数量 d p [ i ] + 1 dp[i]+1 dp[i]+1。若原来的 d p [ j ] dp[j] dp[j]大于 d p [ i ] + 1 dp[i]+1 dp[i]+1,说明原来得到j的方法不如现在的方法,更新 d p [ j ] = d p [ i ] + 1 dp[j]=dp[i]+1 dp[j]=dp[i]+1。
C++代码
#include<bits/stdc++.h>
using namespace std;
int dp[1<<20]; //dp[v]表示口味为v时所需要的最少糖果包数
int ST[100]; //100包糖果
int main()
{
int n,m,k;
cin>>n>>m>>k; //输入n包糖果,m个口味,每包k个口味
int tot = (1<<m)-1; //表示所有m种口味各种组合方式
memset(dp,-1,sizeof dp); //初始化dp全部为-1
for(int i=0;i<n;i++) //依次处理n包糖果
{
int st = 0;
for(int j=0;j<k;j++) //依次处理每颗糖果口味
{
int x;
cin>>x;
st|=(1<<x-1); //把第i包的第j颗糖果加入该包的口味st中
}
dp[st] = 1;
ST[i] = st;
}
for(int i=0;i<=tot;i++) //遍历所有口味组合方式
{
if(dp[i]!=-1) //表示已存在得到该口味的最少糖果包数量
{
for(int j=0;j<n;j++) //检查给定的n包糖果
{
int st = ST[j];
if(dp[i|st]==-1||dp[i|st]>dp[i]+1) //状态转移
dp[i|st] = dp[i]+1;
}
}
}
cout<<dp[tot]; //得到所有口味tot的最少糖果包数
return 0;
}