最长上升子序列模型(LIS)
最长上升子序列问题有两种解决方法
1.\(n^2\)做法
思路
状态表示————集合:f[i]表示从1到i中上升子序列的集合
状态计算————从最后开始分析,因为最后一步一定是加上它自己,所以从前面的上升子序列中找到最长的上升子序列加上自己,就一定也是最长的。
思考
数字三角形模型中,从最后分析,划分集合依靠的是走到当前格子的不同方法,如从上或者从左边来。
最长上升子序列模型中,从最后分析,划分集合依靠的也是怎么走到当前位置是最大的,那么集合被i前面的数划分为i - 1份和它自己一份一共i份。
#include <iostream>
using namespace std;
const int N = 1010;
int a[N], f[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++)
{
f[i] = 1; //最开始最短长度为1
for(int j = 1; j < i; j ++)
{
if(a[j] < a[i])
f[i] = max(f[i], f[j] + 1); //动态滚动,每次向前面的数找最大值
}
}
int ans = 0;
for(int i = 1; i <= n; i++) ans = max(ans, f[i]);
cout << ans <<endl;
}
2.\(nlogn\)做法
思路
用二分将循环内O(n)的操作优化为logn,原理是维护一个最长上升子序列数组,有点类似于滑动窗口
#include <iostream>
using namespace std;
const int N = 100010;
int q[N], a[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
int len = 0;
for(int i = 1; i <= n; i++)
{
int l = 0, r = len, mid;
while(l < r)
{
mid = (l + r + 1) >> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
return 0;
}
习题
AcWing 1017. 怪盗基德的滑翔翼
思路
板子题,就是正着做一次反着做一次求最大值
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int T;
cin >> T;
while (T --) {
int n;
cin >> n;
vector<int> a(n + 1), f(n + 1, 1);
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j < i; j ++) {
if (a[j] > a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
}
int mx1 = 0;
for (int i = 1; i <= n; i ++) {
mx1 = max(mx1, f[i]);
}
reverse(a.begin() + 1, a.end());
for (int i = 1; i <= n; i ++) {
f[i] = 1;
for (int j = 1; j < i; j ++) {
if (a[j] > a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
}
int mx2 = 0;
for (int i = 1; i <= n; i ++) {
mx2 = max(mx2, f[i]);
}
cout << max(mx1, mx2) << endl;
}
}
AcWing 1014. 登山
思路
建立一个先上升后下降的序列,要求最大。其实就是滑翔翼的升级版,正着做一次反着做一次在同一个位置相加减一即为所求
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n + 1), f(n + 1, 1), cnt(n + 1, 0);
int mx = 0;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j < i; j ++) {
if (a[j] < a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
cnt[i] = f[i];
}
reverse(a.begin() + 1, a.end());
for (int i = 1; i <= n; i ++) {
f[i] = 1;
for (int j = 1; j < i; j ++) {
if (a[j] < a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
mx = max(mx, f[i] + cnt[n - i + 1] - 1);
}
cout << mx << endl;
return 0;
}
AcWing 482. 合唱队形
思路
与上题一样的思路,就是答案和上题互补
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n + 1), f(n + 1, 1), cnt(n + 1, 0);
int mx = 0;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j < i; j ++) {
if (a[j] < a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
cnt[i] = f[i];
}
reverse(a.begin() + 1, a.end());
for (int i = 1; i <= n; i ++) {
f[i] = 1;
for (int j = 1; j < i; j ++) {
if (a[j] < a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
mx = max(mx, f[i] + cnt[n - i + 1] - 1);
}
cout << n - mx << endl;
return 0;
}
AcWing 1012. 友好城市
思路
将北岸的城市排序,即可发现南岸的城市形成了一条序列,题意就是让我们求出一条最长的上升子序列
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main () {
int n;
cin >> n;
vector<pair<int,int>> a(n + 1);
vector<int> f(n + 1, 1);
for (int i = 1; i <= n; i ++) {
int x, y;
cin >> x >> y;
a[i] = {x, y};
}
sort(a.begin() + 1, a.end());
int mx = 0;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j < i; j ++) {
if (a[j].second < a[i].second) {
f[i] = max(f[i], f[j] + 1);
}
}
mx = max(mx, f[i]);
}
cout << mx << endl;
return 0;
}
AcWing 1016. 最大上升子序列和
思路
与最大上升子序列唯一的不同就是要求的是和最大,那就不统计数量,统计最大和即可,方法一样
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<long long> a(n + 1), sum(n + 1, 0);
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) {
sum[i] = a[i];
for (int j = 1; j < i; j ++) {
if (a[j] < a[i]) {
sum[i] = max(sum[i], sum[j] + a[i]);
}
}
}
long long mx = 0;
for (auto i : sum) {
mx = max(i, mx);
}
cout << mx << endl;
return 0 ;
}
1010. 拦截导弹
思路
第二问最少需要装备的系统数
贪心做法为:
对于每个导弹,都有两种选择
1:观察所有现有序列的最后一个数,若这个数是大于等于它的,则放到一个集合中,最后从这个集合中挑选最接近该数的数对应的序列
2.若所有序列都不能放,代表它大于所有序列的最后一个数,则开辟一个新的序列
这么做就产生了一种结论:
每个序列的最后一个数组成的数组,是一个单调上升的序列
也就是说该问题可以转化成最长上升子序列问题!
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int x;
vector<int> a;
while (cin >> x) {
a.push_back(x);
}
int n = a.size();
int mx1 = 0, mx2 = 0;
vector<int> f(n, 1), v(n, 1);
for (int i = 0; i < n; i ++) {
for (int j = 0; j < i; j ++) {
if (a[j] < a[i]) {
v[i] = max(v[i], v[j] + 1);
}
if (a[j] >= a[i]) {
f[i] = max(f[i], f[j] + 1);
}
}
mx1 = max(mx1, f[i]);
mx2 = max(mx2, v[i]);
}
cout << mx1 << endl;
cout << mx2 << endl;
return 0;
}
187. 导弹防御系统
思路
和拦截导弹思路差不多,就是对每个导弹的选择上,多了一大类————严格单调下降
也就是说需要对每个导弹先判断是放在单增序列里还是单减序列里,再判断是否增加序列来存放
对于最少种类这种贪心,若常规贪心和dp无法解决,方法只有爆搜————dfs
设置全局变量,不断更新,找到最小值
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 55;
int q[N];
int up[N], down[N];
int n, ans;
void dfs(int u, int su, int sd) { //参数分别表示为:枚举到第几个导弹,当前单增序列数量,当前单减导弹数量
if (su + sd >= ans) return ;
if (u == n) {
ans = su + sd;
return ;
}
//假设把当前数放在单增序列(结果单增,序列单减)(最少需要多少单减序列)中
int k = 0;
while (k < su && q[u] >= up[k]) k ++;
int t = up[k];
up[k] = q[u];
if (k < su) dfs(u + 1, su, sd);
else dfs(u + 1, su + 1, sd);
up[k] = t;
//假设把当前数放在单减序列(最少需要多少单增序列)中
k = 0;
while (k < sd && q[u] <= down[k]) k ++;
t = down[k];
down[k] = q[u];
if (k < sd) dfs(u + 1, su, sd);
else dfs(u + 1, su, sd + 1);
down[k] = t;
}
int main() {
while (cin >> n, n) {
for (int i = 0; i < n; i ++) cin >> q[i];
ans = n;
dfs(0, 0, 0);
cout << ans << endl;
}
return 0;
}
迭代加深解法
与设置全局变量ans更新的方式区别不大
区别就是迭代加深是设置变量depth作为上限,一个一个从小枚举,然后找到最小匹配答案
ans是一开始设置为n,然后更新成最小值
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 60;
int a[N];
int up[N], down[N]; //分别每个存放上升(下降)子序列最后一个数
int n;
bool dfs(int depth, int u, int su, int sd) { //depth代表su + sd的上限,u是枚举到第u个数,su是上升序列的数量,sd是下降序列的数量
if (su + sd > depth) return false;
if (u == n + 1) return true;
//将当前数字放到上升子序列中
bool ok = 0;
for (int i = 1; i <= su; i ++) {
if (a[u] > up[i]) {
int t = up[i];
up[i] = a[u];
if (dfs(depth, u + 1, su, sd)) return true;
up[i] = t;
ok = 1;
break;
}
}
//若该数字无法放到已有序列中,则开辟一个新序列存放
if (!ok) {
up[su + 1] = a[u];
if (dfs(depth, u + 1, su + 1, sd)) return true;
}
//将当前数字放到下降子序列中
ok = 0;
for (int i = 1; i <= sd; i ++) {
if (a[u] < down[i]) {
int t = down[i];
down[i] = a[u];
if (dfs(depth, u + 1, su, sd)) return true;
down[i] = t;
ok = 1;
break;
}
}
//若该数字无法放到已有序列中,则开辟一个新序列存放
if (!ok) {
down[sd + 1] = a[u];
if (dfs(depth, u + 1, su, sd + 1)) return true;
}
return false;
}
int main() {
while (cin >> n, n) {
for (int i = 1; i <= n; i ++) cin >> a[i];
int depth = 0;
while (!dfs(depth, 1, 0, 0)) depth ++;
cout << depth << endl;
}
return 0;
}
272. 最长公共上升子序列
思路
集合:
第一个序列前i个和第二个序列前j个数字,以b[j]为结尾的公共上升子序列
性质:Max
集合划分:
分为包括a[i]和不包括a[i]。(或者说b[j]是否等于a[i])
1.不包括a[i]的结果即是f[i - 1][j]
2.包括a[i]的结果再划分为j个小集合
从前j - 1个最长公共上升子序列中找到最大的那个加1
思路大体是:先找到公共的序列,然后对公共序列找最长上升序列
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3010;
int a[N], b[N];
int f[N][N];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) cin >> b[i];
for (int i = 1; i <= n; i ++) {
int maxv = 1; //代表前j - 1个最长公共上升子序列中最大值
for (int j = 1; j <= n; j ++) {
f[i][j] = f[i - 1][j];
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); //寻找前j - 1个最长公共上升子序列中最大值
}
}
int res = 0;
for (int i = 1; i <= n; i ++) res = max(res, f[n][i]);
cout << res << endl;
}

浙公网安备 33010602011771号