算法 {选择若干个数 使得其`|`或运算结果为`T`的方案数}

算法 {选择若干个数 使得其|或运算结果为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 他里面只有151, 也就是 所有合法的二进制状态 只有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的性质, 转换成模板题;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值