题目框架
回顾:最长上升子序列
方法二:贪心。设 q [ l e n ] q[len] q[len] 表示的是长度为 l e n len len 的上升子序列中末尾数的最小值, r e s res res 是最终答案。对于每一个数 a i a_i ai,如果它大于当前的LIS的末尾数的最小值,那么把它接到这个LIS后面即可,q[++res]=ai
,否则二分出最小的 l e n len len 使得 q [ l e n ] > a i q[len]>a_i q[len]>ai,用 a i a_i ai 去替换 q [ l e n ] q[len] q[len] q[len]=ai
,这样使得长度为 l e n len len 的上升子序列的末尾数的最小值更小。时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
//贪心代码
int find(int x){
//二分
int l = 1, r = len;
while (l < r){
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
void solve(){
for(int i = 2; i <= n; i ++){
if (a[i] > q[len]) q[++ len] = a[i];
else int l = find(a[i]), q[l] = a[i];
}
}
一、怪盗基德的滑翔翼
正向、反向求两次LIS长度,取最大值。
二、登山
对每个点求以它结尾的LIS以及反向LIS长度之和减一,取最大值。
三、合唱队形
题目问的是“最少需要几位同学出列”,只要上题登山输出 n − r e s n-res n−res 即可。
四、友好城市
所有桥与桥之间不能相交即 ∀ \forall ∀已经架桥的二元组 ( x , y ) (x,y) (x,y),若 x i < x j x_i<x_j xi<xj,则 y i < y j y_i<y_j yi<yj,否则 ( x i , y i ) (x_i,y_i) (xi,yi) 与 ( x j , y j ) (x_j,y_j) (xj,yj) 相交,其中至少有一组不能架桥。将所有友好城市 ( x , y ) (x,y) (x,y) 按照 x x x( y y y) 从小到大排序,题意即求 y ( x ) y(x) y(x) 上的LIS长度。
五、最大上升子序列和
与LIS状态表示和计算类似,只不过状态表示的是以该点结尾的最大上升子序列和,转移的时候加的是数值,不是1。
六、拦截导弹
第一问:最长不上升子序列长度;
第二问:可由贪心求得。由狄尔沃斯定理,即最长上升子序列长度。
第二问贪心证明:要证 A = B A=B A=B,只要证 A ≤ B A\le B A≤B (显然 A ≥ B A\ge B A≥B)。
假设最优解与贪心方案不同,找到第一个不同的地方( A A A 中方案为 a a a, B B B 中方案为 b b b):
可知, x ≤ a ≤ b x\le a\le b x≤a≤b,那么 b b b 后面的序列与 a a a 后面的序列完全可以交换,并且保持最终结果不变。因此,在贪心结果下,最多调整 n n n 次,可以将其变为最优结果,而最终结果不会变得更差,即 A ≥ B A\ge B A≥B。得证。
//第二问贪心的代码
//cnt为第二问结果
for(int i = 1; i < n; i ++){
int k = 0;
while (k < cnt && g[k] < a[i]) k ++; //找到结尾大于等于它的最小子序列(g[]数组一定是有序的,找到的第一个就是最小的)
g[k] = a[i]; //接到子序列后面
if (k >= cnt) cnt ++; //重新创建新的子序列
}
七、导弹防御系统
这道题并不是DP,是DFS问题。每个导弹有两种情况:放在单调上升序列中、放在单调下降序列中 (怎么放如上题贪心流程),再对每一种情况继续深搜下一个导弹,直到得到答案。
//dfs代码
void dfs (int s, int su, int sd){
if (su + sd >= ans) return; //肯定不是最优解,不用再搜索下去
if (s == n){
ans = su + sd; //更新最优解
return;
}
int k = 0, tmp;
while (k < su && up[k] >= a[s]) k ++;
tmp = up[k], up[k] = a[s]; //同上题第二问
if (k >= su) dfs(s + 1, su + 1, sd);
else dfs(s + 1, su, sd), up[k] = tmp;
k = 0;
while (k < sd && down[k] <= a[s]) k ++;
tmp = down[k], down[k] = a[s];
if(k >= sd) dfs(s + 1, su, sd + 1);
else dfs(s + 1, su, sd), down[k] = tmp;
}
八、最长公共上升子序列
状态表示:设 f [ i , j ] f[i,j] f[i,j] 表示由第一个序列前 i i i 个字母和第二个序列前 j j j 个字母 (LCS),且以 b [ j ] b[j] b[j] 结尾 (LIS) 的最长公共上升子序列的长度。
状态计算:先对 f [ i , j ] f[i,j] f[i,j] 分两种情况:
1. a [ i ] ≠ b [ j ] a[i]\ne b[j] a[i]=b[j],那么 f[i,j]=f[i-1,j]
;
2. a [ i ] = b [ j ] a[i]=b[j] a[i]=b[j],这时候按照LIS的做法,用 f [ i , 1 ] ~ f [ i , j − 1 ] f[i,1]~f[i,j-1] f[i,1]~f[i,j−1] 更新 f [ i , j ] f[i,j] f[i,j] 的答案。
//2如下
if (a[i] == b[j])
for (int k = 1; k < j; k ++)
if (b[k] < b[j]) //此时a[i]=b[j]
f[i][j] = max(f[i][j], f[i][k] + 1);
最后求 f [ n , 1 ] ~ f [ n , m ] f[n,1]~f[n,m] f[n,1]~f[n,m] 的最大值即为答案。时间复杂度 O ( n 3 ) O(n^3) O(n3) 会爆。
考虑优化一重循环:在做LIS过程中,需要枚举所有小于 b [ j ] b[j] b[j] 的 b [ k ] b[k] b[k],而此时 b [ j ] = a [ i ] b[j]=a[i] b[j]=a[i],因此要枚举所有小于 a [ i ] a[i] a[i] 的 b [ k ] b[k] b[k],发现这一循环已经与 j j j 无关了,此时这一循环的含义是找到满足 b [ k ] b[k] b[k] 小于 a [ i ] a[i] a[i] 的 f [ i − 1 , k ] f[i-1,k] f[i−1,k] 的最大值。用 m a x v maxv maxv 记录这一最大值,并在循环 j j j 时更新该最大值即可。
//2可变为
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
if (b[j] < a[i]) maxv = max(maxv, f[i][j] + 1);
//省去了k的一重循环