经典问题——重复覆盖问题

本文介绍了一种算法,通过枚举和优化策略,帮助小明在购买N包糖果的情况下,最少地品尝到M种不同口味。核心在于使用Dancing Links或优雅暴力搜索,通过迭代加深和lowbit优化,计算出最小购买包数,如果无法覆盖所有口味则输出-1。
摘要由CSDN通过智能技术生成

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

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

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

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

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

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

输入格式
第一行包含三个整数 N,M,K。

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

输出格式
一个整数表示答案。

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

数据范围
1≤N≤100,
1≤M,K≤20,
1≤Ti≤M
输入样例:
6 5 3
1 1 2
1 2 3
1 1 3
2 3 5
5 4 2
5 1 2
输出样例:
2

题解
样例解释
在这里插入图片描述
重复覆盖问题最少选几行使得每一列至少存在一个1
Dancing links(十字链表,跳舞表),最快做法,但很难写。

我们使用优雅的暴力搜索
搜索顺序
枚举选择一个没有被选过的列,枚举它选择哪一行。

优化

  1. 迭代加深(枚举1行够不够,不够再枚举两行……)
  2. 找选择最少的列
  3. 可行性剪枝 估价函数h():至需要再选多少行。
    1和3优化叫IDA

lowbit优化,我们用二进制表示物品的选或没选,初始的时候1表示已经选了,0表示没有选择。但lowbit可以快速求出末尾1的位置(lowbit(x)返回的是x末尾1代表的2^pos值,但log2[lowbit(x)]j就表示末尾1的位置pos),所以我们对其反转一下,0表示已经选了,1表示没有选择,这样就可以利用lowbit快速找到没有选择物品的列了。

//糖果
//大致顺序:先枚举可选择数最少的一列,然后再在这一列中枚举选择哪一行
//这样的时间复杂度应该是最低的,


#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>

using namespace std;

const int N=110, M=1<<20;
vector<int > col[N];//用[j][j]]
int n,m,k;
int log2[M];//预处理,方便计算 log2(2的n次方)
int lowbit(int x)
{
    return x& -x;
}

int h(int state)
{
    //编写估价函数,看这时的state最少需要用几行来完成
    int res=0;
    //求最小方案数时,假设选择了某一列,则等价于选择了这一列的全部方案数
    for(int i=(1<<m)-1-state;i;i-=lowbit(i))
    {
        int c=log2[lowbit(i)];//i返回最后一位1,通过log2直接映射为最后一位1的位置
        res++;
        for(auto row:col[c])
        {
            i=i&~row; //row表示哪一列有1,每次选择一种方案,等价于将这种方案对应的位变为0,通过&操作实现
        }


    }
    return res;
}
// depth表示层数,state用于维护选择糖果过程中已经选择了哪些口味
bool dfs(int depth,int state)
{
    if(!depth||h(state)>depth)
    {
        //若可选择的方案为0或者最小需要选择的方案数都小于当前可选的方案数的话,则判断是否合法
        //判断方法:看state是否全为1
        return state==(1<<m)-1;// (1<<m)-1表示m位全是一, 即2^m-1
    }

    //接下来找可选择数最少的一列

    int t=-1;//t是指向选择数最少的那一列的指针

    //(全1 - state): 得到没选的为1、选择为0的状态i,然后用lowbit()取出最后一位1的值。
    for (int i = (1 << m) - 1 - state; i; i -= lowbit(i))
    {
        int c = log2[lowbit(i)];
        if (t == -1 || col[t].size() > col[c].size())
            t = c;
    }
    //接下来枚举选择哪一行
    for(auto row: col[t])
    {
        if(dfs(depth-1,state|row)) return true;
    }
    return false;

}


int main()
{
    scanf("%d%d%d",&n,&m,&k);
    //预处理log2
    for(int i=0;i<m;i++)
    {
        log2[1<<i]=i;
    }

    for(int i=0;i<n;i++)
    {
        int state=0;
        //将该包糖果所包含的糖果对应的位数置为1
        for(int j=0;j<k;j++)
        {
            int c;
            cin>>c;
            // state将c-1列置为1
            state=state|(1<<(c-1));

        }

        //找出这包糖果 哪个位置可以填成1,将该列对应的col+1
        for(int j=0;j<m;j++)
        {
            if(state>>j&1)//若第j位有1
            {
                col[j].push_back(state);
            }
        }


    }

    int depth=0;// 枚举至少需要选择的行数
    while(!dfs(depth,0)&&depth<=m) depth++;
    if(depth>m) depth=-1;
    cout<<depth<<endl;

    return 0;


}

import sys
sys.setrecursionlimit(10**9)
IA = lambda:map(int, input().split())
M = 1 << 20
n, m, k = IA()
col = [[] for i in range(m + 10)]
log2 = [0 for i in range((1 << m) + 10)]
for i in range(m):
    log2[1 << i] = i
    
for i in range(n):
    state = 0
    x = list(IA())
    for c in x:
        state = state | (1 << (c - 1))
    for j in range(m):
        if state >> j & 1:
            col[j].append(state)
def h(state):
    global m
    res = 0
    i = (1 << m) - 1 - state
    while i:
        c = log2[i & (-i)]
        for row in col[c]:
            i = i & ~ row
        i -= i & (-i)
    return res
    
    
def dfs(depth, state):
    global m
    if depth == 0 or h(state) > depth:
        return state == (1 << m) - 1
        
    t = -1
    i = (1 << m) - 1 - state
    while i:
        c = log2[i & (-i)]
        if t == -1 or len(col[t]) > len(col[c]):
            t = c
        i -= i & (-i)
        
    for row in col[t]:
        if dfs(depth-1, state | row): return True
    return False

depth = 0
while dfs(depth, 0) == False and depth <= m:
    depth += 1
    
if depth > m:
    depth = -1
print(depth)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值