最大01互斥矩阵/状态压缩

最大01互斥矩阵

目录

1.题目
2.算法分析
3.算法实现

  ~  

1.题目:

题目描述

给定 1 1 1 1000 1000 1000 行× 20 20 20 列的 01 01 01 矩阵,对于该矩阵的任意 1 1 1 列,其中值为 1 1 1 的元素的数量不超过 10 10% 10.

设有两个非空集合 A A A B B B,每个集合由矩阵的若干列组成.集合 A A A B B B 互斥是指对于矩阵的任意一行,同时满足下列 2 2 2个条件:

( 1 ) (1) (1) A A A中有一个或多个元素在这一行上的值是 1 1 1,则 B B B中的元素在这一行全部是 0 0 0;

( 2 ) (2) (2) B B B中有一个或多个元素在这一行上的值是 1 1 1,则 A A A中的元素在这一行全部是 0 0 0.

请你设计一个算法,找出一对互斥集合 A A A B B B,使得 A A A B B B包含的列的总数最大.

输入格式

输入 1000 1000 1000行× 20 20 20列的 01 01 01矩阵.

输出格式

每组输出两行,使得 A A A B B B的列数和最大.

第一行输出 A A A 集合中的所有列的编号(下标从 0 0 0开始),以空格分开;

第二行输出 B B B集合中的所有编号,格式同上.

如果没有找到非空集合 A A A B B B,则输出两行空行.

为保证输出唯一,每个集合的输出按照升序排列,如果存在并列的情况,则采用以下策略:

A A A B B B元素列数差的绝对值最小;

A A A的列数要大于 B B B的列数 ;

A A A的元素和要小于 B B B的元素和.

2.算法分析
(1)状态压缩【二进制搜索】

考虑用二进制对每一个状态进行表示,此即状态压缩

对于 A A A而言,假设我们选择了第 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3列,可以使用二进制数表示为: 000...001111 000...001111 000...001111

其中,假设最低位是第 0 0 0位,那么最高位为 19 19 19

这个数第 k k k位上如果为 1 1 1,代表 A A A选择了第 k k k列,否则未选择第 k k k列,

可以把 A A A矩阵对列的所有选择方案唯一表示一个 20 20 20位的二进制数

基于这个思想, A A A的所有选择方案可以唯一转化为一个十进制数

这个十进制数的取值范围是 0 0 0 2 20 − 1 2^{20}-1 2201

(2)初步设计【会超时】

显然,我们可以把 A , B A,B A,B的选择情况都看成一个 20 20 20位的二进制数

遍历所有情况,判断 A , B A,B A,B是否互斥,并记录最大列数情况下的 A , B A,B A,B

总计算次数大概为: 2 20 × 2 20 = 2 40 ≈ 1 0 12 2^{20} \times 2^{20} = 2^{40}\approx 10^{12} 220×220=2401012(次)

C + + C++ C++ 1 s 1s 1s大概完成 1 0 9 10^9 109次计算,这样的话需要 1 0 3 s ≈ 17 m i n 10^3s \approx 17min 103s17min

这显然是不可取的,故要进行优化设计.

(3)优化思路【预处理+剪枝】

事实上,当我们确定了 A A A的选择方案后,不一定需要遍历 B B B的所有情况

A A A的选择方案确定以后, B B B事实上存在一种最大选择情况

比如, A A A选择第 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3列后,如果第 7 , 8 , 9...19 7,8,9...19 7,8,9...19列都和 A A A的某些列互斥

那么 B B B最多只能选择第 4 , 5 , 6 4,5,6 4,5,6列.

为了快速地在给定 A A A的选择后找到 B B B的最大选择情况,

我们引入预处理数组 c h o i c e [ k ] choice[k] choice[k]

其中 c h o i c e [ t ] choice[t] choice[t]表示当 A A A选择了第 t t t列后 B B B最多能选择哪些列

举例说明如下:

假设第 1 1 1列与第 3 , 4 , 5 , 6...19 3,4,5,6...19 3,4,5,6...19列都矛盾

那么 A A A选择了第 1 1 1列后, B B B最多只能选择第 0 , 2 0,2 0,2

用二进制数作记录, c h o i c e [ 1 ] = 000...00101 choice[1]=000...00101 choice[1]=000...00101

我们如果把这个二进制数化为十进制数,显然取值范围为: 0 0 0 2 20 − 1 2^{20}-1 2201

所以用int显然已经可以满足需要.

引入了 c h o i c e choice choice数组后,快速得到 B B B的最大选择方案可以极大减少搜索次数.

设当前 A A A选择第 0 , 1 0,1 0,1列,

c h o i c e [ 0 ] = 000...01110 , c h o i c e [ 1 ] = 000...0101 choice[0]=000...01110,choice[1]=000...0101 choice[0]=000...01110,choice[1]=000...0101

表示 A A A选择了第 0 0 0列后, B B B最多只能选择第 1 , 2 , 3 1,2,3 1,2,3

A A A选择了第 1 1 1列后, B B B最多只能选择第 0 , 2 0,2 0,2

显然 B B B最多只能选择第 2 2 2

我们对 c h o i c e [ 0 ] choice[0] choice[0] c h o i c e [ 1 ] choice[1] choice[1] & \& &运算即得 B B B的最大选择

000...01110 & 000...0101 = 000...0100 000...01110 \& 000...0101=000...0100 000...01110&000...0101=000...0100

(4)优化算法

0.预处理 c h o i c e choice choice数组

1.遍历 A A A的所有选择情况,即从 0 0 0 2 20 − 1 2^{20}-1 2201

2.根据 c h o i c e choice choice数组得到 B B B的最大选择方案

3.由于我们总是可以使得 A A A的列数不小于 B B B

所以如果当前 A A A的列数小于 B B B就进行下一次循环(剪枝)

否则判断总列数是否大于之前记录的最大列数,若大于则更新.

有相同的列数和时,结合实际情况判断即可

(5)时间复杂度

由于 A A A 20 20 20列可选,至多需要不超过计算 2 20 2^{20} 220级别,也即 1 0 6 10^6 106级别

C + + C++ C++显然是可以在 1 s 1s 1s内跑完的.

3.算法实现

基于以上分析,得到算法实现如下:

#include<iostream>
using namespace std;

const int M = 25, N = 1010;
bool judge[M][M];//记录两列是否矛盾
int a[N][M], choice[M];//a数组记录输入
int sum[M];//存储列的和
int ares = -1, bres = -1;//存储A,B的最优选择
//asum,bsum是当前的A,B的元素和
//eps是列数差的绝对值,tc是当前最大列数和
int asum, bsum, eps = 100, tc;
//判断两列是否互斥
bool dispose(int x, int y)
{
    for (int i = 0; i < 1000; i++)
        if (a[i][x] + a[i][y] == 2)return false;
    return true;
}
//预处理judge[][]数组判断两列是否互斥
void init()
{
    for (int i = 0; i < 20; i++)
        for (int j = i + 1; j < 20; j++)
            if (dispose(i, j))judge[i][j] = judge[j][i] = true;
}
//得到choice[]数组
void get_choice()
{
    for (int i = 0; i < 20; i++)
    {
        int sum = 0;
        for (int j = 0; j < 20; j++)
            if (judge[i][j])sum += 1 << j;
        choice[i] = sum;
    }
}

//lowbit函数
int lowbit(int x)
{
    return x & (-x);
}
//返回二进制中1的个数
int get_one(int x)
{
    int res = 0;
    while (x)x -= lowbit(x), res++;
    return res;
}
//预处理所有列的和
void get_c()
{
    for (int i = 0; i < 20; i++)
    {
        int res = 0;
        for (int j = 0; j < 1000; j++)
            res += a[j][i];
        sum[i] = res;
    }
}

//得到数x二进制表示中所有列的和
int get_sum(int x)
{
    int k = 0, res = 0;
    while (x)
    {
        if (x & 1)res += sum[k];
        k++;
        x >>= 1;
    }
    return res;
}
//输出答案
void get_answer(int x)
{
    int k = 0, len = 20;
    while (len--)
    {
        if (x & 1)
            cout << k << " ";
        k++;
        x >>= 1;
    }
    cout << endl;
}
//同列数和列数差时比较排序次序
//返回true表示x应在y之后
bool compare(int x, int y)
{
    while (x && y)
    {
        if (lowbit(x) == lowbit(y))
        {
            x -= lowbit(x), y -= lowbit(y);
            continue;
        }
        else
        {
            if (lowbit(x) > lowbit(y))return true;
            return false;
        }
    }
    return false;
}
//更新函数
void update(int i, int j)
{
    ares = i, bres = j;
    tc = asum + bsum;
    eps = asum - bsum;
}

int main()
{
    for (int i = 0; i < 1000; i++)
        for (int j = 0; j < 20; j++)
            cin >> a[i][j];

    //预处理choice[]数组,judge[]数组,sum[]数组
    init(), get_choice(), get_c();
    //遍历A的所有选择
    for (int i = 1; i < 1 << 20; i++)
    {
        int temp = i, k = 0, j = -1;//j初始化-1是因为-1是111...111
        while (temp)
        {
            if (temp & 1)//如果这一位是1
                j = (j & choice[k]);//j和这一位的选择作&
            temp >>= 1;
            k++;
        }
        //得到i,j选择的列数
        asum = get_one(i), bsum = get_one(j);

        if (asum >= bsum && bsum)
        {
            if (asum + bsum > tc)//如果列数更大
                update(i, j);
            else if (asum + bsum == tc)//如果列数一样
            {
                //如果列数差的绝对值更小则更新
                if (asum - bsum < eps)
                    update(i, j);
                else if (asum - bsum == eps)//如果列数差相等
                {
                    if (compare(ares, i))
                        update(i, j);
                    else if(get_sum(ares) >= get_sum(bres) && get_sum(i) < get_sum(j))
                        update(i, j);
                }
            }
        }
    }

    if (~ares)//ares!=-1表示存在非空集合,~是取反,~(-1)=0
        get_answer(ares),get_answer(bres);

    else cout << endl << endl;
    return 0;
}

附加说明:

l o w b i t lowbit lowbit函数 l o w b i t ( x ) = x & ( − x ) lowbit(x)=x \& (-x) lowbit(x)=x&(x)

返回二进制表示下数 x x x的最后一位 1 1 1的位置

假设 x > 0 x>0 x>0,那么~ x x x是其按位取反

− x -x x是其相反数,为按位取反再加 1 1 1

x x x的最后 1 1 1 1 1 1按位取反为 0 0 0,再加 1 1 1那么这 1 1 1位就为 1 1 1

如果把 x x x和~ x x x & \& &运算,那么除了最后 1 1 1位1之外其他位置都为 0 0 0

如:假设 x x x是4位有符号数

x = 0110 , − x = 1010 , l o w b i t ( x ) = x & ( − x ) = 0010 x=0110,-x=1010,lowbit(x) = x \& (-x)=0010 x=0110,x=1010,lowbit(x)=x&(x)=0010

c o m p a r e compare compare函数

判断在满足题目所有条件以及排序方式下还存在多个答案的情况下,哪一个答案应该先输出.

如对于 A : 1 , 2 , 3 A:1,2,3 A:1,2,3, B : 4 B:4 B:4 A : 0 , 1 , 2 A:0,1,2 A:0,1,2, B : 3 B:3 B:3

如果这两组解在所有排序方式下都相同,优先输出 A : 0 , 1 , 2 A:0,1,2 A:0,1,2(按照选择的列数大小排序, 0 , 1 , 2 0,1,2 0,1,2 1 , 2 , 3 1,2,3 1,2,3之前,也就是这种情况下不使用 1 , 2 , 3 1,2,3 1,2,3来更新 0 , 1 , 2 0,1,2 0,1,2)

4.数据测试

提供了3组用例供调试使用.

链接:测试用例

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指针常量

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值