【算法导论】DJ 0830笔试题题解

上升子序列

给定一个长度为n的排列A。在排列A中,从1~n这n个正整数,有且仅有一个。

另外我们规定,A的一个子序列指的是从A中将若干个元素(不一定连续)提取出来并按它们的原始的相对次序罗列而成的一个新序列。

对于A的一个子序列B,如果B中的元素单调递增,我们就称B是A的一个上升子序列。

对于A的一个上升子序列C,如果在A中找不到比C长度更长的上升子序列了,我们就称C是A的一个最长上升子序列。显然,A可以有多个最长上升子序列。

对于每个i∈[1, n],它会出现在多少个A的最长上升子序列当中呢?

输入描述

第一行n

第二行中按次序输入A中的n个元素

题目保证A一定是一个排列。另外1<=n<=2*10^5

输出描述

输出n行。第i行的输出表示A[i]出现在多少个A的最长上升子序列当中。

因为答案可能很大,请你在输出前将每行的答案先对998244353取模。

测试用例

输入:

复制代码
5
3 1 4 2 5

输出:

复制代码
1
2
2
1
3

说明: 对于排列3 1 4 2 5,它存在三个不同的最长上升子序列1 2 51 4 53 4 5。仅一个上升子序列中包含A[0]=3,仅两个上升子序列中包含A[1]=1,...

暴力解法

时间复杂度大约在O(2^n)这个量级,只能过20%的测试用例...

c++ 复制代码
#include <bits/stdc++.h>

std::vector<std::vector<int>> seqs;

int64_t max_len = 0;

void Find(const std::vector<int>& nums, std::vector<int>& seq, int64_t pos) {
    if (pos == nums.size()) {
        if (seq.size() < max_len) {
            return;
        }
        if (seq.size() > max_len) {
            max_len = seq.size();
            seqs.clear();
        }
        seqs.emplace_back(seq);
        return;
    }
    
    if (nums.size() - pos + 1 + seq.size() < max_len) {
        return;
    }
    
    // 当前元素大于前一个,或者没有前一个,那么可以选
    if (seq.empty() || nums[pos] > seq.back()) {
        seq.push_back(nums[pos]);
        Find(nums, seq, pos + 1);
        seq.pop_back();
    }

    if (nums.size() - pos + 1 + seq.size() < max_len) {
        return;
    }
    
    // 不选
    Find(nums, seq, pos + 1);
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    
    int n;
    std::cin >> n;
    
    int max_val = 0;
    std::vector<int> nums(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> nums[i];
        max_val = std::max(max_val, nums[i]);
    }
    
    std::vector<int> seq;
    Find(nums, seq, 0);
    
    std::vector<int> count(max_val + 1, 0);
    for (const auto& seq : seqs) {
        for (int num : seq) {
            ++count[num];
        }
    }
    
    for (int num : nums) {
        std::cout << count[num] % 998244353 << std::endl;
    }
}

稍优化解法(双向动态规划)

时间复杂度约为O(n^2)

C++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>


int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);

    int n;
    std::cin >> n;

    std::vector<int> nums(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> nums[i];
    }

    // 正向DP
    // f[i] 以nums[i]结尾的递增子序列的长度最长可以达到多少?
    // f_count[i] 以nums[i]结尾的最长递增子序列在原数组nums中有几个?
    std::vector<int> f(n, 1);
    std::vector<int> f_count(n, 1);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            if (nums[j] < nums[i]) {
                if (f[j] + 1 > f[i]) {
                    f[i] = f[j] + 1;
                    f_count[i] = f_count[j];
                } else if (f[j] + 1 == f[i]) {
                    f_count[i] += 1;
                }
            }
        }
    }

    // 求出全局最长递增子序列的长度是多少?
    // 因为全局最长递增子序列的末尾元素一定也在区间[0, n-1]之间,
    // 因此我们遍历一遍f[i],找到的最大值即为所求。
    int global_max_len = 0;
    for (int i = 0; i < n; ++i) {
        global_max_len = std::max(global_max_len, f[i]);
    }

    // 反向DP
    // g[i] 以nums[i]开头的递增子序列的长度最长可以达到多少?
    // g_count[i] 以nums[i]开头的最长递增子序列在原数组nums中有几个?
    std::vector<int> g(n, 1);
    std::vector<int> g_count(n, 1);
    for (int i = n - 1; 0 <= i; --i) {
        for (int j = i + 1; j < n; ++j) {
            if (nums[i] < nums[j]) {
                if (g[j] + 1 > g[i]) {
                    g[i] = g[j] + 1;
                    g_count[i] = g_count[j];
                } else if (g[j] + 1 == g[i]) {
                    g_count[i] += 1;
                }
            }
        }
    }

    // 输出答案
    for (int i = 0; i < n; ++i) {
        // nums[i]被重复统计了两次,这里要减去1
        if (f[i] + g[i] - 1 == global_max_len) {
            int ans = f_count[i] * g_count[i];
            std::cout << ans % 998244353 << std::endl;
        } else {
            // 没有任何最长上升子序列中包含nums[i]
            std::cout << 0 << std::endl;
        }
    }
}

最优化版本(双向动态规划+线段树)

以下是ai写的代码,据称时间复杂度可以达到O(nlogn)...

C++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 定义模数
const int MOD = 998244353;

// 线段树节点结构
struct Node {
    int len = 0;         // 存储当前区间的最大LIS长度
    long long count = 0; // 存储达到该长度的子序列数量
};

// 全局线段树和数组大小
vector<Node> tree;
int N_val;

// 合并两个线段树节点信息的函数
Node combine(Node a, Node b) {
    if (a.len > b.len) {
        return a;
    }
    if (b.len > a.len) {
        return b;
    }
    // 如果长度相等,但为0,说明是初始状态或空区间,返回一个即可
    if (a.len == 0) {
        return {0, 0};
    }
    // 如果长度相等且不为0,长度不变,数量相加
    return {a.len, (a.count + b.count) % MOD};
}

// 线段树更新操作
// node: 当前节点索引, start, end: 当前节点代表的区间, idx: 待更新的数组值, val: 新的(长度, 数量)对
void update(int node, int start, int end, int idx, Node val) {
    if (start == end) {
        // 在叶子节点处,合并新旧信息
        tree[node] = combine(tree[node], val);
        return;
    }
    int mid = start + (end - start) / 2;
    if (start <= idx && idx <= mid) {
        update(2 * node, start, mid, idx, val);
    } else {
        update(2 * node + 1, mid + 1, end, idx, val);
    }
    // 回溯时更新父节点信息
    tree[node] = combine(tree[2 * node], tree[2 * node + 1]);
}

// 线段树查询操作
// node: 当前节点索引, start, end: 当前节点代表的区间, l, r: 待查询的区间
Node query(int node, int start, int end, int l, int r) {
    // 查询区间与当前节点区间无交集
    if (r < start || end < l || l > r) {
        return {0, 0};
    }
    // 查询区间完全覆盖当前节点区间
    if (l <= start && end <= r) {
        return tree[node];
    }
    int mid = start + (end - start) / 2;
    Node p1 = query(2 * node, start, mid, l, r);
    Node p2 = query(2 * node + 1, mid + 1, end, l, r);
    return combine(p1, p2);
}

int main() {
    // 提高I/O效率
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    N_val = n;

    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }

    vector<int> f(n);
    vector<long long> count_f(n);
    tree.assign(4 * N_val + 4, {0, 0});

    // --- 正向DP ---
    // 计算以 A[i] 结尾的 LIS 信息
    for (int i = 0; i < n; ++i) {
        int val = a[i];
        // 查询值域 [1, val-1] 上的最优 LIS 信息
        Node res = query(1, 1, N_val, 1, val - 1);
        if (res.len == 0) { // 如果没有比 a[i] 小的元素,则 a[i] 自身构成长度为1的 LIS
            f[i] = 1;
            count_f[i] = 1;
        } else {
            f[i] = res.len + 1;
            count_f[i] = res.count;
        }
        // 将 a[i] 的 LIS 信息更新到线段树的值域位置 val 上
        update(1, 1, N_val, val, {f[i], count_f[i]});
    }

    // 找到全局 LIS 的最大长度
    int max_len = 0;
    for (int len : f) {
        max_len = max(max_len, len);
    }

    vector<int> g(n);
    vector<long long> count_g(n);
    tree.assign(4 * N_val + 4, {0, 0});

    // --- 反向DP ---
    // 计算以 A[i] 开始的 LIS 信息
    for (int i = n - 1; i >= 0; --i) {
        int val = a[i];
        // 查询值域 [val+1, n] 上的最优 LIS 信息
        Node res = query(1, 1, N_val, val + 1, N_val);
        if (res.len == 0) { // 如果没有比 a[i] 大的元素,则 a[i] 自身构成长度为1的 LIS
            g[i] = 1;
            count_g[i] = 1;
        } else {
            g[i] = res.len + 1;
            count_g[i] = res.count;
        }
        // 将 a[i] 的 LIS 信息更新到线段树的值域位置 val 上
        update(1, 1, N_val, val, {g[i], count_g[i]});
    }

    // --- 合并结果 ---
    for (int i = 0; i < n; ++i) {
        // 判断 A[i] 是否在某条最长上升子序列上
        if (f[i] + g[i] - 1 == max_len) {
            long long ans = (count_f[i] * count_g[i]) % MOD;
            cout << ans << "\n";
        } else {
            cout << 0 << "\n";
        }
    }

    return 0;
}
相关推荐
PAK向日葵2 小时前
【算法导论】LXHY 0830 笔试题题解
算法·面试
聪明的笨猪猪3 小时前
面试清单:JVM类加载与虚拟机执行核心问题
java·经验分享·笔记·面试
麦麦麦造3 小时前
DeepSeek突然发布 V3.2-exp,长文本能力加强,价格进一步下探
算法
lingran__4 小时前
速通ACM省铜第十七天 赋源码(Racing)
c++·算法
MobotStone4 小时前
手把手教你玩转AI绘图
算法
CappuccinoRose5 小时前
MATLAB学习文档(二十二)
学习·算法·matlab
学c语言的枫子6 小时前
数据结构——基本查找算法
算法
yanqiaofanhua6 小时前
C语言自学--自定义类型:结构体
c语言·开发语言·算法
sali-tec6 小时前
C# 基于halcon的视觉工作流-章39-OCR识别
开发语言·图像处理·算法·计算机视觉·c#·ocr