动态规划:最长上升子序列
附上Leetcode链接
https://leetcode-cn.com/problems/longest-increasing-subsequence/
\(O(n^2)\)的算法
思路:使用dp[i]表示结尾字符为a[i],首字符为a[0]的字符串的最长子序列长度。使用简单遍历的方法求解,对于
每一个结尾字符为a[i]的序列,寻找所有a[k] (\(k<i\)),如果a[i]大于a[k],则dp[i] = max{dp[k] + 1, dp[i]},不断的
更新dp[i]的值,直到找到一个最大值。
最优子结构特性: 如果dp[i]是结尾字符为a[i]的序列的最长单调递增子序列的长度,则dp[k] (\(k<i\))一定是结尾字
符为a[i]的序列的最长单调递增子序列的长度。
反证法:如果dp[k]不是最长单调递增子序列的长度,那么有dp[k]'>dp[k],则后面未更新的dp[i]也不是结尾字符
为a[i]的序列的最长单调子序列的长度,会更长
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
int dp[2505];
int a[2505];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
for (int i = 0; i < n; i++)
{
dp[i] = 1;
}
for (int i = 0; i < n; i++)
{
if (i == 0)
dp[i] = 1;
else
{
for (int j = 0; j < i; j++)
{
if (a[j] < a[i])
dp[i] = max(dp[j] + 1, dp[i]);
}
}
}
int maxx = 0;
for (int i = 0; i < n; i++)
if (maxx < dp[i])
maxx = dp[i];
cout << maxx << endl;
return 0;
}
\(O(nlogn)\)的算法
分析上述算法可以发现,在搜索\(max(dp[k] + 1, dp[i])\) 时使用的是\(O(n)\)方式的简单遍历查找。可以从这里入手进
行优化。
进一步分析可以发现,对于长度为 \(i\) 前缀子序列的 \(dp[i]\) 要使得 \(dp[j] = dp[i]+1 (i<j)\) 则 \(a[j]\) 一定要大于长度
为 \(i\) 前缀子序列的最长上升子序列的最小结尾字符 \(a[k]\)
因此我们可以使用 \(b[k]\) 来表示长度为k的最长上升子序列的最小结尾字符,对于每个 \(a[i]\) ,如果 \(a[i]>b[k]\) ,则
\(b[++k] = a[i]\) ,如果\(a[i] < b[k]\) ,就更新相应的 \(b[j]\)的值,其中\(b[j-1]<=a[i]<b[j]\) ,这样就可以保证
\(b[k]\) 的值一定是最小值。
如何更新相应的\(b[j]\)值?
使用二分查找来寻找 \(j\)
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
int dp[2505];
int a[2505];
int b[2505];
int Bin(int i, int k); //寻找b[1:k]中 b[j-1]<=a[i]<b[j]的值
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
b[0] = a[0];
int k = 0;
for (int i = 0; i < n; i++)
{
if (a[i] > b[k] || k == 0 && i == 0)
b[++k] = a[i]; //更新最大长度
else
{
int j = Bin(i, k);
b[j] = a[i]; //更新最小值
}
}
cout << k << endl;
return 0;
}
int Bin(int i, int k) //寻找b[1:k]中 b[j-1]<=a[i]<b[j]的值
{
if (a[i] < b[1])
return 1; //防止找不到
int p = 1;
int q = k;
while (p < q)
{
int mid = (p + q) / 2;
if (b[mid] >= a[i])
q = mid;
else
p = mid + 1;
}
return q;
}