洛谷专题-线性动态规划
P1020导弹拦截(最长上升子序列/最长不降子序列 - O(N2)与O(NlogN)做法)
题意:
有n个导弹分别飞行在不同高度,一颗拦截导弹可以分别拦截几个高度不上升的导弹,问一个拦截导弹可以拦截最多多少个导弹,拦截所有导弹要多少颗拦截导弹
思路:
第一个问题:一个拦截导弹可以拦截最多多少个导弹,这就是一个求最长不降子序列问题
先考虑O(N2)的做法,对于每一个导弹,我们向前遍历之前的每一个导弹,如果小于或者等于其高度,就可以试着更新dp[i]=max(dp[i],dp[j]+1)
dp[i]的每一个元素初始化为1
再来看O(nlogn)的做法,队员前移做法遍历之前每一个元素这里,我们可以选择优化
建立一个树状数组,用数值做下标,维护长度最大值,从后往前循环,每次查询之前已经放到树状数组里面的数中以这个数结尾的最长不上升子序列的长度的最大值,然后把这个最大值+1作为以自己结尾的最长不上升子序列的长度,放到树状数组里面
第二个问题:问最少需要多少个导弹
要用到一个Dilworth定理
Dilworth定理:偏序集的最少反链划分数等于最长链的长度
简单来说就是求一个最长上升子序列,试着来简单证明一下
(1)假设打导弹的方法是这样的:取任意一个导弹,从这个导弹开始将能打的导弹全部打完。而这些导弹全部记为为同一组,再在没打下来的导弹中任选一个重复上述步骤,直到打完所有导弹
(2)假设我们得到了最小划分的K组导弹,从第a(1<=a<=K)组导弹中任取一个导弹,必定可以从a+1组中找到一个导弹的高度比这个导弹高(因为假如找不到,那么它就是比a+1组中任意一个导更高,在打第a组时应该会把a+1组所有导弹一起打下而不是另归为第a+1组),同样从a+1组到a+2组也是如此。那么就可以从前往后在每一组导弹中找一个更高的连起来,连成一条上升子序列,其长度即为K
(3)设最长上升子序列长度为P,则有K<=P;又因为最长上升子序列中任意两个不在同一组内(否则不满足单调不升),则有P>=K,所以K=P
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #define lowbit(x) (x&(-x)) using namespace std; const int maxn=100100; int n=0,len; int a[maxn],mx[maxn]; void add(int x,int val) { while(x<=maxn){ mx[x]=max(mx[x],val); x+=lowbit(x); } } int query(int x) { int ans=0; while(x>=1){ ans=max(mx[x],ans); x-=lowbit(x); } return ans; } int main() { while(scanf("%d",&a[++n])!=EOF); n--; int ans=0; for(int i=n;i>=1;i--){ int x=query(a[i])+1; add(a[i],x); ans=max(ans,x); } cout<<ans<<endl; memset(mx,0,sizeof(mx)); ans=0; for(int i=1;i<=n;i++){ int x=query(a[i]-1)+1; add(a[i],x); ans=max(ans,x); } cout<<ans<<endl; return 0; }
P1091 合唱队列(最长上升子序列)
思路:
分别从左边跟右边求一次最上上升子序列d[i]与f[i],然后枚举最高点i,求max(d[i] + f[i] - 1)(因为i被重复计算)
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> using namespace std; const int maxn=105; int a[maxn],d[maxn],f[maxn]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); d[i]=f[i]=1; } for(int i=1;i<=n;i++) for(int j=1;j<i;j++) if(a[i]>a[j]) d[i]=max(d[i],d[j]+1); for(int i=n;i>=1;i--) for(int j=n;j>i;j--) if(a[i]>a[j]) f[i]=max(f[i],f[j]+1); int ans=0; for(int i=1;i<=n;i++) ans=max(ans,d[i]+f[i]-1); cout<<n-ans<<endl; return 0; }
P1280 尼克的任务(逆推)
题意:
尼克有k个任务,每个任务有起始时间跟完成所需要的时间,同一个时刻有多个任务尼克可任选一个一个完成,如果只有一个任务则必须完成,问尼克的最大闲暇时间是多少
思路:
可以发现第i时刻的最大空闲时间是和后面i+选择任务的持续时间的时刻有关系的,那么,正着找肯定是不行的,我们来试一下倒着搜,即设f[i]表示i~n的最大空闲时间,经尝试,发现是完全可行的,可以列出动态转移方程如下
①(本时刻无任务)f[i]=f[i+1]+1;//继承上一个时刻的最大空闲时间后+1
②(本时刻有任务)f[i]=max(f[i],f[i+a[sum])//a[sum]表示在这个时刻的任务的持续时间,找出选择哪一个本时刻任务使空闲时间最大化
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> using namespace std; const int maxn=1e4+10; int dp[maxn],b[maxn]; struct tim{ int s,h; }a[maxn]; int cmp(tim a,tim b){return a.s>b.s;} int main() { int n,k,cnt=1; scanf("%d%d",&n,&k); memset(b,0,sizeof(b)); for(int i=1;i<=k;i++){ scanf("%d%d",&a[i].s,&a[i].h); b[a[i].s]++; } sort(a+1,a+1+k,cmp); dp[n+1]=0; for(int i=n;i>=1;i--){ if(!b[i]) dp[i]=max(dp[i],dp[i+1]+1); for(int j=1;j<=b[i];j++){ dp[i]=max(dp[i],dp[i+a[cnt].h]); cnt++; } } cout<<dp[1]<<endl; return 0; }
P1880[NOI1995]石子合并(区间DP)
题意:
给你N堆石子为一圈,每堆石子都有其相应的重量,每次可以合并两堆石子,得分为两堆石子的重量和,问得分的最大值与最小值分别是多少
思路:
先考虑所有石子线性排列
我们定义dp[i][j]为从i合并到j的最小值,cost[i][j]为这一堆石子的重量和
可以推出动态规划方程为f[i][j]=f[i][k]+f[k+1][j]+cost[i,j]
之后我们看看需要哪些循环来推出最后的答案
首先得枚举一次合并多少个石子-阶段:len: 2 ~ n
其次得枚举每次合并的区间-状态:i: 1 ~ n – len + 1, j: i + len – 1
再来看看合并哪两堆是比较好的-策略:k: i ~ j
所以时间复杂度为n^4
优化:预处理sum[i]表示前i堆石子总数量,则cost[i,j] = sum[j] – sum[i - 1],时间优化为n^3
最后得试着解出为一圈的情况-只需要将后面的石子再放一次也就是将数组拓展为2*n即可
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #define inf 0x3f3f3f3f using namespace std; const int maxn=205; int dp1[maxn][maxn],dp2[maxn][maxn],sum[maxn][maxn],a[maxn]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); a[i+n]=a[i]; } for(int i=1;i<=n*2;i++) for(int j=i;j<=2*n;j++) for(int k=i;k<=j;k++) sum[i][j]+=a[k]; for(int i=1;i<=n;i++) dp1[i][i]=dp2[i][i]=0; for(int len=1;len<n;len++){ for(int l=1,r=l+len;r<2*n&&l<2*n;l++,r=l+len){ dp1[l][r]=inf; for(int k=l;k<r;k++){ dp1[l][r]=min(dp1[l][r],dp1[l][k]+dp1[k+1][r]+sum[l][r]); dp2[l][r]=max(dp2[l][r],dp2[l][k]+dp2[k+1][r]+sum[l][r]); } } } int ans1=inf,ans2=0; for(int i=1;i<=n;i++){ ans1=min(ans1,dp1[i][i+n-1]); ans2=max(ans2,dp2[i][i+n-1]); } cout<<ans1<<endl<<ans2; return 0; }
P1140 相似基因
题意:
每种碱基对之间都有一堆匹配系数,还可以加入空基因,给两串基因,问最大匹配数是多少
思路:
dp[i][j]=max(dp[i-1][j-1]+(i与j的匹配值),dp[i-1][j]+(i与空的匹配值),dp[i][j-1]+(j与空的匹配值)
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> #define inf 0x3f3f3f3f using namespace std; const int maxn=200; int dp[maxn][maxn]; int tab[5][5]={ {5,-1,-2,-1,-3}, {-1,5,-3,-2,-4}, {-2,-3,5,-2,-2}, {-1,-2,-2,5,-1}, {-3,-4,-2,-1,0} }; int hash(char c){ if(c=='A') return 0; if(c=='C') return 1; if(c=='G') return 2; if(c=='T') return 3; return 4; } int main() { int len1,len2; char a[maxn],b[maxn]; cin>>len1>>a+1>>len2>>b+1; memset(dp,-inf,sizeof(dp)); dp[0][0]=0; for(int i=1;i<=len1;i++) dp[i][0]=dp[i-1][0]+tab[hash(a[i])][4]; for(int i=1;i<=len2;i++) dp[0][i]=dp[0][i-1]+tab[4][hash(b[i])]; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ dp[i][j]=max(dp[i][j],dp[i-1][j-1]+tab[hash(a[i])][hash(b[j])]); dp[i][j]=max(dp[i][j],dp[i-1][j]+tab[hash(a[i])][4]); dp[i][j]=max(dp[i][j],dp[i][j-1]+tab[4][hash(b[j])]); } } cout<<dp[len1][len2]<<endl; return 0; }