動態規劃:最長上升子序列

seekerZhao發表於2021-08-08

動態規劃:最長上升子序列

碎碎念

前天覆習dp時學習了一遍,本來覺得太簡單了,沒想寫的,結果今天周賽的第四題直接給了道模板題,我還沒默出來,罰了5分鐘。趕緊複習一下
學習了加速cin的方法,怕忘了,先寫在這裡

ios::sync_with_stdio(false);

正文

Longest Increasing Subsequence,簡稱LIS。

給一串數字,找出子序列中最長的上升序列。

基礎解法:

dp[i]表示以i結尾的最長上升子序列的長度,可以得到以下狀態轉移方程:

\[\left\{ \begin{matrix} dp[i]=max(dp[j])+1,(j<i,a[j]<a[i]) \\ dp[i]>=1(i\in N) \end{matrix} \right. \]

dp[i]從前項推導得出,遍歷 1……i-1 ,找到比a[i]小,並且dp最大的j,在此基礎上加一,即為模擬上升子序列的新增。

導彈模擬為例題,此題分別要求求出最長非升子序列和最長升序子序列:

#include<iostream>
using namespace std;
int a[1 << 20];
int before[1 << 20];
int len[1 << 20];
int last[1 << 20], tot = 0, ans = 0;
int main() {
	int n = 0;
	while (cin >> a[++n]) {
		len[n] = 1;
		int distance = 1<<20;
		int mini = 1;
		for (int i = 1; i <= tot; i++) {
			if (a[n] <= last[i] && last[i] - a[n] < distance)
			{
				distance = last[i] - a[n];
				mini = i;
			}
		}
		if (distance == 1<<20) last[++tot] = a[n];
		else last[mini] = a[n];
		for (int i = n - 1; i >= 1; i--) {
			if (a[n] <= a[i]) if (len[n] <= len[i]) 
					len[n] = len[i] + 1;
			
		}
	}
	int maxi = 1;
	for (int i = 1; i <= n; i++) if (len[i] > len[maxi]) maxi = i;
	cout << len[maxi] << endl;
	cout << tot;
}

此種解法的時間複雜度為O(n²),不足以解決1e5規模的問題,需要優化

優化解法

這篇部落格寫的非常好。參考了一下。

工具介紹

引入了STLlower_boundup_bound。兩者用法基本一致:

\[lower\_bound(首地址,末地址,查詢元素,查詢規則(可以參省)); \]

在順序序列裡,查詢的是第一個大於等於該元素的元素(upper_bound沒有等於),返回值是地址,如果要轉換成下標,減去首地址即可:

\[\pmb{int}\ p = lower\_bound(a+1,a+n+1,find) \pmb{- a}; \]

查詢預設是升序序列,如果要在降序序列裡查詢,需要在查詢規則裡寫上greater<int>()

方案執行

為得到最長上升子序列,假設一個陣列d,同時用top記錄陣列的末元素。

  1. 如果a[i] > d[top],那麼就把a[i]加到d的末端,更新top,這樣,保證d是一條單調上升的序列。
    (此時top代表著a[i]結尾的最長升序列的長度)
  2. 如果a[i] <= d[top],那麼就用lower_bound在d中找到小於等於a[i]的元素下標p,用a[i]替換掉d[p]。
    (此時p代表這a[i]結尾的最長升序列的長度)

為什麼成立?

  1. 在我們用a[i]更新的時,無論是哪種情況,我們都保證d前面的陣列的元素排在a[i]前面。
  2. 每次遇到第二種情況時,我們都是選擇用現有的元素將原有元素進行替代,而不是插入,這樣保證了最長序列的長度是不變的,同時降低原有元素的值,優化了序列的質量:本來就能放後面的值,還是能,本來放不了的值,現在降低了門檻,能替代,這樣,最後降低了末尾元素的值,就可以使序列保證整體最長,元素最小

由於替代操作改變了a陣列元素的位置,因此,i前面的元素並不是按照a順序排列的,是按照大小排列的,d陣列的元素d[i]代表的不是以d[i]結尾的序列是d[1……i],而是代表著當a[j]更新到d[i]後,以a[j]結尾的序列的lis是i。

理論先寫到這裡,依然是導彈攔截為例題,程式碼如下:

#include<iostream>
#include<algorithm>
using namespace std;
int great[1010];
int les[1010];
int main() {
	ios::sync_with_stdio(false);
	int t , top1 = 0 , top2 = 0;
	cin >> t;
	great[++top1] = t;
	les[++top2] = t;
	while(cin >> t) {
		if( t > great[top1] )
			great[++top1] = t;
		else {
			int p = lower_bound(great+1 , great+top1+1 , t) - great;
			great[p] = t;
		}
		if( t <= les[top2] )
			les[++top2] = t;
		else {
			int p = upper_bound(les+1 , les+top2+1, t,greater<int>()) - les;
			les[p] = t;
		}
	}
	cout << top2 << " " << top1;
}

相關文章