P4310 绝世好题 [位运算优化dp]
题目描述
给定一个长度为 \(n\) 的数列 \(a_i\),求 \(a_i\) 的子序列 \(b_i\) 的最长长度 \(k\),满足 \(b_i \& b_{i-1} \ne 0\),其中 \(2\leq i\leq k\), \(\&\) 表示位运算取与。
输入格式
输入文件共 \(2\) 行。 第一行包括一个整数 \(n\)。 第二行包括 \(n\) 个整数,第 \(i\) 个整数表示 \(a_i\)。
输出格式
输出文件共一行。 包括一个整数,表示子序列 \(b_i\) 的最长长度。
输入输出样例
输入 #1
3
1 2 3
输出 #1
2
说明/提示
对于\(100\%\)的数据,\(1\leq n\leq 100000\),\(a_i\leq 10^9\)。
分析
看到题的第一反应就被标签里的枚举给迷惑到了,然后快速打了一个暴力枚举,显然直接\(90\)分\(TLE\)了,然后开始想正解。
首先题目中的条件是含有位运算的,我们就可以从这里开始入手优化。
因为题目中给的条件是\(a_i\&a_{i-1}\neq 0\),所以这就证明了只要两个数之间有一个二进制位上相同的\(1\)就可以让长度加一,那么我们就可以枚举每一个二进制位,找出他们与运算后是否为\(0\),如果不是\(0\)就让长度加一,我们的一个状态转移方程就出来了:
其中\(dp[c]\)是二进制第\(c\)位时最大的长度,当前状态下满足条件就加一,然后与最大长度\(Max\)取最大值。
需要注意的一个地方就是我们在状态转移后需要每次更新一下\(dp[c]\),因为当前满足条件的最长长度已经找出来是\(Max\)了,所以所有当前满足条件的二进制位的最大长度都变为\(1\),那么就又有一个转移:
最后再统计一下所有更新后\(Max\)的最大值就好了。
代码
首先先放一下我的暴力\(90points\)代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
int n,a[maxn],ans;
int dp[maxn];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;++i){
dp[i]=1;
for(int j=1;j<i;++j){
if((a[i]&a[j])!=0)dp[i]=max(dp[i],dp[j]+1);
}
ans=max(ans,dp[i]);
}
printf("%d",ans);
return 0;
}
然后是正解:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 32;
int dp[maxn];
int Max,ans;
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i){
int b;
scanf("%d",&b);
for(int c=0;c<=31;++c){//枚举每一个二进制位
if((1<<c)&b)Max=max(Max,dp[c]+1);
}
for(int c=0;c<=31;++c){//更新满足条件的二进制位的最大值
if((1<<c)&b)dp[c]=max(Max,dp[c]);
}
ans=max(ans,Max);//统计最大值答案
}
cout<<ans<<"\n";
return 0;
}