上升子序列
给定一个长度为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 5
、1 4 5
和3 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;
}