考虑从后往前做。对于 a i a_i ai,从高位到低位贪心考虑,对于第 p p p 位,若这一位为0,那么只要后面的数能找到2个这一位为1的数,即可保证由 a i a_i ai构成的最后答案这一位为 1。把后面这一位为1的数字加入到一个集合中 S p S_p Sp。下一次对于第 q q q 位( q > p q > p q>p),若这一位为0,那么从 S p S_p Sp中再选出第 q q q 位为 1的数字加入到 S q S_q Sq 中(注意是从 S p S_p Sp集合中选,因为要满足第 p p p 位也为 1),只要超过两个即可,若不超过两个那么显然只能放弃这一位。
这个做法的正确性很容易理解。考虑怎么维护这个集合:用二进制来表示集合的种类,如 1010表示二进制中右起第二位和第四位为1的数的个数。若一个数的二进制为 10110,令 x = 10110,这个数对 x 以及 x 的所有子集都有一个贡献。修改这个贡献的过程,是SOS DP 的逆过程,由于每个状态只需要找2个数,可以用记忆化加剪枝来暴力修改。
SOS DP求子集和时是从下往上不断更新上来,子节点是父节点的一个子集。
这题需要倒过来,即从父结点到子节点,维护时就直接从树的根开始向下更新,遇到已经大于 1 的状态就直接退出,因为它的子集必定也大于 1,这样做比较暴力但复杂度仍然是SOS DP的复杂度。
还有一种正着做的想法,即每个状态维护下标最右的两个数即可,然后做一遍倒着的SOS DP,推出所有状态下标最右的两个数,获得答案的方法还是一样,这种方法应用SOS DP的思维比较直接明显。
代码是倒过来做的那一种
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
typedef long long ll;
int n;
int dp[1 << 21][21];
int a[maxn];
void upd(int num,int k) {
if(k > 20) return;
if(dp[num][k] > 1) return;
dp[num][k]++;
upd(num,k + 1);
if(num >> k & 1) upd(num ^ (1 << k),k);
}
int main() {
scanf("%d",&n);
for(int i = 1; i <= n; i++) {
scanf("%d",&a[i]);
}
int ans = 0;
for(int i = n; i >= 1; i--) {
int res = 0,cnt = 0;
for(int j = 20; j >= 0; j--) {
if(a[i] >> j & 1) res |= (1 << j);
else {
if(dp[cnt | (1 << j)][20] > 1) {
res |= (1 << j);
cnt |= (1 << j);
}
}
}
if(n - i >= 2) ans = max(ans,res);
upd(a[i],0);
}
printf("%d\n",ans);
return 0;
}