---
# System prepended metadata

title: '演算法課程題解 - 動態規劃\: 最長遞增子序列 LIS'
tags: [SCIST 演算法 題解]

---

# 演算法課程題解 - 動態規劃\: 最長遞增子序列 LIS

# UVa 497
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?497

敵國發射了洲際飛彈過來，然而我們的反飛彈系統有嚴重的缺陷，發射角只能生不能降
也就是說每次你只能摧毀比上一枚摧毀的飛彈還高的飛彈
現在告訴你飛彈的高度依序是多少，請問你最多能擋下多少飛彈?
請依序輸出攔截的飛彈高度

## 想法 By Koios
每次都只能選擇比上次更高的高度，所以如果把全部的飛彈高度當作是一個序列來看，題目等同於要求最長的遞增子序列長度
對於每個序列的元素，都可以選擇要成為新遞增子序列的開頭或是舊遞增子序列的一部分
如果要成為舊遞增子序列的一部分，那麼首先我們的值要比序列尾端的值還要大
當我們找出所有可以跟我們接在一起的舊遞增子序列，那麼我就知道我最長可以是多少

根據上面的討論可以得出簡單的 dp 式
定義 $dp[i]$ 表示以第 $i$ 項作結尾的 LIS 最長長度
則有轉移式
$$
dp[i] = max(dp[j]) + 1 \quad \texttt{(arr[i] > arr[j]) && j < i}
$$
並且所有 $dp$ 值最少都有 $1$ ，也就是自己本身

至於回朔攔截的飛彈高度，在 DP 的過程當中我們就可以知道 $i$ 是從哪個 $j$ 轉移過來的，最後就可以簡單遞迴輸出

:::warning
如果你跟我一樣採用 getline 的方式讀取每個數字輸入，請記得要轉換回數字的樣子
:::

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int T, dp[10005], from[1005], arr[10005], last, ans;
string tmp;

void print_ans(int p){
	if(from[p] != -1){
		print_ans(from[p]);
	}
	cout<<arr[p]<<"\n";
}

int to_int(string s){
	int res=0;
	for(int i=0 ; i<s.size() ; i++){
		res *= 10;
		res += s[i]-'0';
	}
	return res;
}

int main(){
	while(cin>>T){
		cin.ignore();
		cin.ignore();
		for(int Case=0 ; Case<T ; Case++){
			int N=0;
			while(getline(cin, tmp) && tmp != ""){
				arr[N++] = to_int(tmp);
			}
			ans = 0;
			last = 0;
			for(int i=0 ; i<N ; i++){
				// dp 最少也會是 1
				dp[i] = 1;
				from[i] = -1;
				for(int j=i-1 ; j>=0 ; j--){
					// 不直接以 DP 式的 max 表示，而是找到最大，方便記錄回朔
					if(arr[i] > arr[j] && dp[j]+1 > dp[i]){
						dp[i] = dp[j] + 1;
						from[i] = j;
					}
				}
				if(dp[i] > ans){
					last = i;
					ans = dp[i];
				}
			}
			if(Case) cout<<"\n";
			cout<<"Max hits: "<<ans<<"\n";
			print_ans(last);
		}
	}
	
	return 0;
}
```

### 時間複雜度分析
DP 每個狀態轉移時間複雜度可估計為 $N$，雖然實際上會比較小
總共有 $N$ 個狀態，DP 總時間複雜度為 $O(N^2)$

###### tags: `SCIST 演算法 題解`