最长上升子序列模型(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;
}
posted @ 2023-02-06 20:29  nobodyL  阅读(37)  评论(0)    收藏  举报