代码改变世界

华为OJ-合唱队-最长递增子序列-动态规划

2017-07-27 16:54  mengmz  阅读(598)  评论(0编辑  收藏  举报

题目描述

计算最少出列多少位同学,使得剩下的同学排成合唱队形

说明

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK, 则他们的身高满足存在i(1<=i<=K)使得T1<T2<......<Ti-1Ti+1>......>TK。你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入描述:

整数N

输出描述:

最少需要几位同学出列

示例1

输入

8
186 186 150 200 160 130 197 200

输出

4


求解思路

  • 属于最长递增子序列,可采用动态规划方法;
  • 第一次从左至右计算dp_pos ,第二次从右至左计算dp_neg,将下标i处的dp值相加得到numnum-1即为以i为中心的合唱团队伍人数,故而需要减去的人数为N - num + 1;
原始数组 186 186 150 200 160 130 197 200
递增序列 1 1 1 2 2 1 3 4
递减序列 3 3 2 3 2 1 1 1

本人测试的代码如下:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// O(N^2)
void get_dp1(vector<int> &array, vector<int> &dp) {
    for (int i = 0; i < dp.size(); ++i) {
        dp[i] = 1;
        for (int j = 0; j < i; ++j) {
            if (array[i] > array[j]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    int N;
    cin >> N;
    vector<int> height(N, 0);
    for (int i = 0; i < N; ++i) {
        cin >> height[i];
    }
    // 正向数组的最长递增子序列
    vector<int> dp_pos(N);
    get_dp1(height, dp_pos);

    // 逆向数组的最长递增子序列--正向数组的最长递减子序列
    reverse(height.begin(), height.end());
    vector<int> dp_neg(N);
    get_dp1(height, dp_neg);

    for (int i = 0; i < N; ++i) {
        dp_pos[i] += dp_neg[N - 1 - i];
    }
    auto pos = max_element(dp_pos.begin(), dp_pos.end());
    cout << N - *pos + 1 << endl;


    return 0;
}

最长递增子序列的\(O(logN)\)解法

  • 利用二分查找来加快dp的计算
  • 代码如下:
//O(NlogN)
void get_dp2(vector<int> &array, vector<int> &dp) {
    vector<int> ends(dp.size());
    ends[0] = array[0];
    dp[0] = 1;
    int right = 0;
    int l = 0;
    int r = 0;
    int m = 0;

    for(int i = 1; i< array.size(); ++ i){
        l = 0;
        r = right;
        // binary search
        while( l <= r){
            m = (l+r)/2;
            if(array[i] > ends[m])
                l = m + 1;
            else
                r = m - 1;
        }
        right = max(right, l);
        ends[l] = array[i];
        dp[i] = l + 1;
    }
}

解析

end数组用来储存递增子序列LIS的末尾元素的最小值,如果有ends[b]==c,则表示遍历到目前为止,在所有长度为b+1的递增序列中(数组下标起始为0),最小的结尾数是c

right + 1 记录了当前LIS的长度。在while循环中,采用二分查找方法找到元素array[i] 在LIS中的位置。有三种情况:

  • array[i]ends(LIS)中的任意元素都要小,此时l = 0, r = -1,此时LIS的长度不变,该字符处的dp[i] = 1;
  • array[i]ends(LIS)中的任意元素都要大,此时l = right + 1, r = right,此时LIS的长度增加1,即right = l,更新ends,该字符处的dp[i] = l + 1;
  • 普通情况, r < m == l, 此时LIS的长度不发生变化,但要更新right位置的值,因为此时array[i] <= ends[m], 按照ends的定义,它该记录LIS的最小尾数值;

这个算法来自于《程序员代码面试指南》左程云著,理解起来存在难度,算法设计 - LCS 最长公共子序列&&最长公共子串 &&LIS 最长递增子序列一文中的解释更加的通俗易懂,可作为参照。