C++算法刷题:排序子序列、削减整数、最长上升子序列(二)题解

今天刷了三道算法题,分别是排序子序列(模拟)削减整数(贪心)最长上升子序列(二)(贪心+二分)。下面分享每道题的思路、代码实现和解题思路总结。


一、排序子序列(模拟)

题目描述

牛牛定义排序子序列为一个数组中一段连续的子序列,这段子序列是非递增或者非递减排序的。牛牛有一个长度为 n的整数数组 a,他现在需要把数组 a分为若干段排序子序列,牛牛想知道最少可以把这个数组分为几段排序子序列。

输入/输出示例

  • 输入:6

    1 2 3 2 2 1

  • 输出:2

  • 说明:可以划分为 [1,2,3][2,2,1]两个排序子序列。

思路分析

要找最少的分段数,需要尽可能让每一段更长。我们可以通过维护当前段的"趋势"(非递增/非递减)来判断是否需要新开一段。

具体步骤:

  1. 初始化段数 res = 1(至少有一个段)。

  2. 遍历数组,判断当前元素与下一个元素的关系,确定当前段的趋势(非递增/非递减)。

  3. 当趋势被破坏时(比如当前段是非递减,下一个元素突然比前一个小,且不是等于的情况?或者需要更细致的判断),就新开一段,段数加一。

但更高效的思路是:记录当前段的可能趋势(非递增、非递减、初始),当出现矛盾趋势时,分段。

举个例子:

  • 数组 1 2 3 2 2 1

    • 前三个 1,2,3非递减,趋势为"递增"。

    • 第四个 23小,此时需要看是否是"非递增"的开始?或者是否破坏当前趋势?

    • 其实,当连续出现"升→降"时,可能需要分段。比如 3→2是降,而之前是升,所以前三个为一段(非递减),后面的 2,2,1为非递增,所以两段。

代码实现

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }
    
    if (n == 0) {
        cout << 0 << endl;
        return 0;
    }
    
    int res = 1;
    // 0: 初始状态;1: 非递减;2: 非递增
    int trend = 0; 
    for (int i = 1; i < n; ++i) {
        if (a[i] > a[i-1]) {
            if (trend == 2) { // 之前是非递增,现在出现递增,需要新分段
                res++;
                trend = 1;
            } else {
                trend = 1;
            }
        } else if (a[i] < a[i-1]) {
            if (trend == 1) { // 之前是非递减,现在出现递减,需要新分段
                res++;
                trend = 2;
            } else {
                trend = 2;
            }
        }
        // 相等的话,趋势不变
    }
    cout << res << endl;
    return 0;
}

测试样例

输入:

复制代码
6
1 2 3 2 2 1

输出:2,与预期一致。


二、削减整数(贪心)

题目描述

给出一个正整数 H,从 1开始减,第一次必须减 1,每次减的数字都必须和上一次相同或者是上一次的两倍,请问最少需要几次能把 H恰好减到 0

输入/输出示例

  • 输入:3

    3

    5

    7

  • 输出:2

    3

    3

  • 说明:(示例解释:比如 H=3时,第一次减1,第二次减2(1的两倍),1+2=3,共2次)

思路分析

贪心策略:每次尽可能选择最大的可能值(即上一次的2倍),直到剩下的数小于当前要减的数,此时调整为当前的剩余值(因为必须减到0,且只能减当前值或其2倍)。

步骤:

  1. 初始化次数 count = 0,当前减数 cur = 1,剩余值 remain = H

  2. 循环直到 remain == 0

    • 如果 remain >= cur,则减去 cur,次数加一,remain -= cur,然后 cur *= 2(尽可能翻倍)。

    • 如果 remain < cur,则只能减去 remain(因为必须减到0,且 remain是剩余的,此时 cur太大,所以调整 cur = remain,然后减去它,次数加一,remain = 0)。

但更严谨的逻辑是:每次先尝试用最大的可能的 cur(即上一次的2倍),如果剩下的数不够,则用剩下的数(因为必须减到0,且 cur不能超过剩余值,否则无法减完)。

代码实现

复制代码
#include <iostream>
using namespace std;

int main() {
    int T;
    cin >> T;
    while (T--) {
        long long H; // 注意H可能很大,用long long
        cin >> H;
        long long count = 0;
        long long cur = 1; // 当前要减的数,初始为1
        while (H > 0) {
            if (H >= cur) {
                H -= cur;
                count++;
                cur *= 2; // 下一次尽可能翻倍
            } else {
                // 剩余H小于cur,只能减H,此时cur调整为H,减完后H为0
                count++;
                H = 0;
            }
        }
        cout << count << endl;
    }
    return 0;
}

测试样例

输入:

复制代码
3
3
5
7

输出:

复制代码
2
3
3
  • 解释:

    • H=3:第一次减1(剩余2),第二次减2(剩余0)→ 2次。

    • H=5:第一次减1(剩4),第二次减2(剩2),第三次减2(剩0)→ 3次?不对,示例输出是3?或者我理解错了?哦示例输入的输出是:

      示例输入的输出是:

      2

      3

      3

      对应输入3、5、7:

      • 3:1+2=3 → 2次。

      • 5:1+2+2=5 → 3次?

      • 7:1+2+4=7 → 3次?哦对,1+2+4=7,三次。所以代码逻辑正确。


三、最长上升子序列(二)(贪心+二分)

题目描述

给定数组 arr,设长度为 n,输出 arr的最长上升子序列。(如果有多个答案,请输出其中按数值进行比较的字典序最小的那个)

要求:空间复杂度 O(n),时间复杂度 O(nlogn)

输入/输出示例

  • 输入:[2,1,5,3,6,4,8,9,7]

  • 输出:[1,3,4,8,9]

  • 说明:最长上升子序列长度为5,字典序最小的是 [1,3,4,8,9]

思路分析

经典的最长上升子序列(LIS)问题的优化版本,要求字典序最小的LIS。

传统LIS的贪心+二分方法(时间 O(nlogn))是维护一个数组 d,其中 d[i]表示长度为 i+1的LIS的最小末尾元素。但对于字典序最小的情况,需要调整策略:

  1. 维护两个数组:

    • d:记录长度为 i+1的LIS的最小末尾元素(同传统方法)。

    • pos:记录每个元素在 d中的位置,用于回溯路径。

    • path:记录每个元素的前驱,用于最后构造字典序最小的序列。

  2. 对于每个元素 x

    • 如果 x大于 d的最后一个元素,则加入 d,并更新路径。

    • 否则,找到 d中第一个大于等于 x的位置 idx,替换 d[idx]x,并更新路径(因为要字典序最小,所以当有多个位置可以放 x时,选择最左边的,这样后续元素更容易形成更小的字典序)。

  3. 最后,从后往前回溯路径,构造字典序最小的LIS。

代码实现

复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> findNumberOfLIS(vector<int>& nums) {
    int n nums.size();
    = if (n == 0) return {};
    
    vector<int> d(n + 1, 0); // d[i]表示长度为i的LIS的最小末尾
    vector<int> pos(n, 0);   // pos[i]表示nums[i]在d中的位置
    vector<int> pre(n, -1);  // pre[i]表示nums[i]的前驱节点
    int len = 0;             // 当前LIS的长度
    
    for (int i = 0; i < n; ++i) {
        int x = nums[i];
        // 找到d中第一个大于等于x的位置
        int l = 0, r = len;
        while (l < r) {
            int mid = (l + r) / 2;
            if (d[mid] < x) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        pos[i] = l;
        pre[i] = (l > 0) ? i : -1; // 前驱是上一个位置的元素?
        // 修正:pre[i]应该是d[l-1]对应的元素的索引,所以需要记录每个d[l]对应的元素索引
        // 重新设计:用另一个数组记录d中每个位置的元素索引
        vector<int> idx(n + 1, -1); // idx[i]表示d[i]对应的nums的索引
        if (l == len) {
         len++;
        }
           d[l] = x;
        idx[l] = i;
        if (l > 0) {
            pre[i] = idx[l - 1];
        }
    }
    
    // 回溯构造字典序最小的LIS
    vector<int> res(len);
    int cur = idx[len - 1]; // 最后一个元素的索引
    for (int i = len - 1; i >= 0; --i) {
        res[i] = nums[cur];
        cur = pre[cur];
    }
    return res;
}

int main() {
    vector<int> arr = {2,1,5,3,6,4,8,9,7};
    vector<int> ans = findNumberOfLIS(arr);
    for (int x : ans) {
        cout << x << " ";
    }
    cout << endl;
    return 0;
}

测试样例

输入:[2,1,5,3,6,4,8,9,7]

输出:1 3 4 8 9(与示例一致)


总结

今天的三道题分别用到了模拟贪心贪心+二分的策略:

  1. 排序子序列:通过维护趋势来判断分段点,注意相等元素的处理。

  2. 削减整数:贪心选择最大可能的减数(翻倍),确保次数最少。

  3. 最长上升子序列(二):在传统贪心+二分的基础上,额外维护前驱和索引,以构造字典序最小的LIS。

这些题目都需要仔细分析题意,找到最优策略,并注意数据范围和边界条件(比如大整数用long long,数组越界等)。

相关推荐
tankeven2 小时前
HJ157 剪纸游戏
c++·算法
迈巴赫车主2 小时前
蓝桥杯 19717 挖矿java
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
Sag_ever2 小时前
Java String 类详解:字符串常用方法 + 不可变性 一网打尽
java·开发语言
顶点多余2 小时前
死锁+线程安全
linux·开发语言·c++·系统安全
专注API从业者2 小时前
淘宝 API 调用链路追踪实战:基于 SkyWalking/Pinpoint 的全链路监控搭建
大数据·开发语言·数据库·skywalking
airuike1232 小时前
高性能MEMS IMU:机器人自主运动的核心感知中枢
人工智能·算法·机器人
jinanwuhuaguo2 小时前
OpenClaw v2026.4.1 深度剖析报告:任务系统、协作生态与安全范式的全面跃迁
java·大数据·开发语言·人工智能·深度学习
郝学胜-神的一滴2 小时前
PyTorch张量维度操控:transpose与permute深度拆解与实战指南
人工智能·pytorch·python·深度学习·算法·机器学习
小邓的技术笔记2 小时前
Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册
开发语言·python