最大01互斥矩阵
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 220−1
(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=240≈1012(次)
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 103s≈17min
这显然是不可取的,故要进行优化设计.
(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 220−1
所以用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 220−1
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组用例供调试使用.
链接:测试用例