算法 {选择若干个数 使得其|
或运算结果为T
的方案数}
选择若干个数 使得其|
或运算结果为T
的方案数
定义
&N
: 二进制长度; &{(Ai,Ci)}
: Ai
为长度为N的二进制, Ci
表示其个数; &S
: 所有的{Ai}
组成的多重集合; &T
: 长度为N
的二进制;
求: 从S中选择若干个元素(不可以为空) 使得其|
或运算结果为T
的方案数;
.
比如N=3
, {(100,2), (010,1)}
, 则S = {100,100,010}
; 比如T=110
, 则答案方案有2个: {(100|010), (100|010)}
;
.
答案方案里的Ai
一定满足(Ai&T)==Ai
, 否则 比如Ai=111, T=101
, 那么Ai
一定不是答案;
性质
很容易想到一个暴力, 枚举2^|S|
方案 (选与不选) 然后看选出来的|
结果是否等于tarST
; 这很像DP 你很容易从这个思路出发 但其实这个思路不对… 因为|S|
太大了 所有的Cont[]
之和 是非常大的;
核心思路是(很多算法他们的本质 都基于此): 对于Cont[x]
, 他里面的2^Cont[x]
个方案 只有一个{}
空方案结果为0
, 其他方案结果都是x
(因为x | ... | x = x
); (不用考虑{}
空方案 因为答案求的方案 不允许为空); 以及a|b >= max(a,b)
; 也就是 本题的关键 其实是看你对|
或运算性质的理解程度, 而不是枚举方案的DP做法;
@TODO;: &CC(st):
所有|
结果为st
的方案数 (这当然就是我们要求的答案); &SC(st):
所有|
结果为x
且(x&st)==x
的方案; (这就很像{容斥/莫比乌斯变换} 即&SC
里是包含了&CC
了 我们先得到&SC
然后再去转换成&CC
), 而构造&SC(st)
(&Sub(st):
所有满足(x&st)==x
的集合, 比如&Sub(110)={110,100,010,000}
) 你只需要得到所有Cont[ &Sub(st)]
之和, 他们的(选与不选) 即2^(Cont[&Sub(tarST)]) - 1
(去掉空方案) 得到的 就是&SC(tarST)
;
@DELI;
这个问题, 和*(重复/精确)覆盖问题* 有点类似; 但也有不同点, 比如T = 101
, 对于{111}
这个方案 他可以覆盖T
但在本问题里 是不合法的; 而且重复覆盖问题 问的是 最小选择元素的个数, 而这里是总方案数;
算法
如果T != (2^N)-1
, 则可以将N
变小
定义
假如说T != (2^N - 1)
, 比如T = 1011, N=4
, 那么你可以把他的[2]
位给去掉 即T = 111
, 然后遍历所有的Ai
: @IF(Ai[2] == 1
){则把他给删除掉}–@ELSE{把Ai
里面的[2]
位 给去掉(比如A[i] = abcd
则变成acd
)};
.
因为当T[2] == 0
时, 那么此时假如Ai[2] == 1
那么Ai
他一定不会是答案 即可以把他给删除掉, 否则把Ai
里的[2]
这个比特位给删除掉;
.
比如N=3
, {(100,2), (111,2), (010,1)}
, T=110
, 答案为2: {(100|010), (100|010)}
; 转换后为: N=2
, 把[0]
位给去掉: {(10,2), (01,1)}
, T=11
, 答案为2: {(10|01), (10|01)}
;
性质
二进制的长度 可以很长 (因此代码模板里 用的是string
来存储二进制); 比如说二进制的长度N == 1e6
非常长, 然而tarST
他里面只有15
个1
, 也就是 所有合法的二进制状态 只有2^15
个(对应代码模板里最终的cont.size()
); 因此 把tarST
里面所有的0
都给去掉 很重要, 因为解决该问题的算法 他是用Cont[ tarST+1]
来存储, 其中tarST
是整数 假如很大 那么你空间就爆了 然而他的时间不会爆(因为tarST
里的1
很少 很多算法的时间是取决于N
(即tarST
里1的个数) 而不是取决于tarST
的大小));
.
代码模板里用的是string
同时cont
是用map
存储, 这会影响效率, 假如你的tarST
不是很大(比如<=1e6
) 那么你的cont
可以改为int Cont[1e6]
, 同时string
改为int
, 随机应变把;
代码
void ___SolutionsOfOr_translate( unordered_map<string,int> & _cont, string & _tarST){ // 字符串表示二进制数 其{开头/末尾}到底对应二进制的高位/低位 都可以 即`100`对应`"100"或"001"`都可以;
ASSERT_MSG_( "for( auto [a,b]:cont){ ASSERT( a.size()==tarST.size());} ; ");
int bits = std::count( _tarST.begin(), _tarST.end(), '1');
string newTarST = string( bits, '1');
while( _tarST != newTarST){
for( int bit = 0; ; ++bit){
if( _tarST[bit] == '1'){}
else{
___SUPIMO::String_::Replace( _tarST, bit, bit, "");
std::decay_t<decltype(_cont)> newCont;
for( auto & [k,v] : _cont){
if( k[bit] == '1'){ continue;}
else{ auto newK = k; ___SUPIMO::String_::Replace(newK, bit,bit, ""); newCont[newK]=v;}
}
_cont = std::move(newCont);
break;
}
}
}
} // ___SolutionsOfOr_translate
DP递推 可求任意的tarT
(2^N * 2^N * log
)
定义
&N:
二进制长度; &Cont[1<<N]:
每个二进制出现的次数; (无需指定tarST
我们可以求出他的任意值)
.
因为[0, 1<<N)
这些二进制状态 任一方案{st}
的结果 都是在[0, 1<<N)
范围的; 即答案范围也是[0, 1<<N)
;
&DP(i,j):
元素{st}
均在[0,1,2,...,i]
范围内的 且结果为j
的方案数; (比如i=0b10, j=0b11
, 那么&DP(i,j)
的方案 可以是(01|10)
但不可以是(11|00)
(因为其中11 > i
这个方案是在DP(>= 0b11, j)
里面的));
.
DP转移: 后继递推curDP -> {不选i: (i+1,j)=curDP; 选i: (i+1,j|i)=curDP*(2^Cont[i]-1)}
;
#耗时#: &C:
Cont[?]
的最大值; 则时间为2^N * 2^N * log(&C)
, 因为要进行2.power(Cont[])
操作, 你可以对&C
进行预处理 得到Pow2[ &C]
这样就变成2^N * 2^N
了;
性质
(i,j):
从[0,1,...,i]
这些状态里 选择若干状态使得其|
的结果为j
的 方案数; (注意, 这里定义的方案数 是要与Cont相关联的, 比如i=101,j=101
虽然可能的合法方案有{(101), (101|000), (101|001), (101|100), (001|100), (001|100|000), ...}
, 但这并不是DP的答案, 比如说Cont[101]=4, Cont[其他]=0
, 那么此时 其实只会有(101)
这个一个方案, 而且答案不是1
而是2^Cont[101]-1 = 15
;
@DELI;
代码里的DP
是进行了数组压缩的;
@DELI;
未数组压缩的代码:
static Mod_ DP[1<<N][ 1<<N]; // (i,j): 从`[0,1,...,i]`这些状态里 选择若干状态使得其`|`的结果为`j`的 方案数; (注意, 这里定义的*方案数* 是要与Cont相关联的, 比如`i=101,j=101`虽然他表示的方案有`{(101), (101|0), (101|1), (101|100), (1|100)}`, 但这并不是DP的答案, 比如说`Cont[101]=4, Cont[其他]=0`, 那么此时 其实只会有`(101)`这个一个方案, 而且答案不是`1` 而是`2^Cont[101]=16`;
memset( DP, 0, sizeof(DP));
FOR_( dpI, 0, (1<<N)-1){ // `{Ai}`的`|或`等于`T`, 那么一定有`Ai<=T`;
auto & curDP = DP[dpI];
if( dpI == 0){
curDP[0] = (Cont[0]的二次幂 - 1); // 一定要注意这里, 要把*空方案*给去掉; 即有C个x, 我们要选择若干个 使得他们的`|`结果为`x` 那么必须至少选1个`x`;
continue;
}
auto const& preDP = DP[ dpI-1];
memcpy( curDP, preDP, sizeof(curDP)); // 不选`dpI`
{ // 选`dpI`;
curDP[ dpI] += (Cont[dpI]的二次幂 - 1); // `dpI`是单独一个
//>> `dpI`和前面的方案进行合并
FOR_( dpJJ, 0, TarST){ // 所有`(dpI-1)`的合法的`j`, 不一定就是`<=(dpI-1)`的; (比如`dpI=2`, 而`1|10 = 11`);
if( preDP[ dpJJ] == 0){ continue;}
curDP[ dpJJ | dpI] += (preDP[ dpJJ] * (Cont[dpI]的二次幂 - 1)); // 可以提前预处理出`Pow2[x]`表示二次幂;
}
}
}
代码
namespace ___SolutionsOfOr_DP{
//< #代码#: `___SolutionsOfOr_DP::Work<Mod_>( Cont, Bits);` -> `___SolutionsOfOr_DP::DP<Mod_>[?].Value`为答案;
template< class _TypeANS_> std::vector< _TypeANS_> DP; // `DP[x]:` 从`count`里选择若干二进制(不可为空) 其他们的`|`或运算的结果为`x`的方案数;
template< class _TypeANS_, class _TypeCount_> static void Work( _TypeCount_ const* _count, int _bits){ // `count`的范围是`[0, (1<<bits))` (`count[st]`表示二进制`st`的个数);
DP<_TypeANS_>.resize( 1<<_bits); std::memset( DP<_TypeANS_>.data(), 0, sizeof(DP<_TypeANS_>[0])*DP<_TypeANS_>.size());
for( int dpI = 0; dpI < (1<<_bits); ++dpI){
if( dpI == 0){
DP<_TypeANS_>[0] = _TypeANS_(2).Power(_count[0]) - 1; // 因为DP定义里 是不包含*空方案*的 所以要`-1`;
continue;
}
for( int dpJ = (1<<_bits)-1; dpJ >= 0; --dpJ){ // 选`dpI`放到前面方案里; 所有合法的`(i,j)` 不一定有`j<=i`(比如`i=2`, 此时`01|10==3`);
if( DP<_TypeANS_>[ dpJ] == 0){ continue;}
DP<_TypeANS_>[ dpJ | dpI] += (DP<_TypeANS_>[ dpJ] * (_TypeANS_(2).Power( _count[dpI]) - 1));
}
DP<_TypeANS_>[ dpI] += (_TypeANS_(2).Power(_count[dpI]) - 1); // [选`dpI]&&[`dpI`是单独一个];
//>< DP[j]: 从`[0,1,...,i]`里选 结果为`j`的方案;
}
}
}; //} namespace ___SolutionsOfOr_DP
子集前缀和+还原 (2^N * N
)
定义
已知&Cont[st]:
每个st
二进制的个数; 已知&Subs(st):
二进制st
的子集 (比如&Subs(101)={101,100,001,000}
);
求出&SumC[st]:
所有Cont[ &Subs(st)]
之和; ( &SumC
他是个算法模板(子集前缀和));
&ANS[st]:
或运算结果为st
的(非空方案)的方案数 (这是要求的答案);
求出&SumANS[st]:
或运算结果为任意&Subs(st)
的方案数 (即&SumANS[101] = &ANS[101] + &ANS[100] + &ANS[001] + &ANS[000]
); 我们要求&SumANS
(当然不是通过定义式来求), 他其实就等于2 ^ &SumC[st] - 1(去掉空集)
(这依据|
或运算的性质 {Ai}的或运算 = T
那么一定有Ai \in &Subs(T)
, 因此&Subs(T)
里(选/不选)的总方案数 一定包含了&ANS[st]
不仅如此 其实他等于&ANS[ &Subs(st)]之和
); 即得到了&SumC[st]
你就得到了&SumANS[st]
;
然后根据&SumANS
来还原出&ANS
, 这可以通过: 子集前缀和的还原 (也是算法模板);
代码
1: 通过*子集前缀和*得到得到`&SumC[]`;
2: `&SumANS[st] = (2^SumC[st] -1);
3: 子集前缀和的还原, 可以通过{容斥(获取某一个tarST的答案), 差分(获取任意tarST的答案)};
应用
@LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=137920019
;
结合LCM的性质, 转换成模板题;