CF597C 子序列
1 CF597C 子序列
2 题目说明
时间限制 \(1s\) | 空间限制 \(256M\)
对于给定的 \(n\) 个不同元素组成的序列找出 \(k+1\) 个元素的上升子序列个数。结果最大不超过 \(8 \times 10^{18}\)。
数据范围:\(1 ≤ n ≤ 10^5, 0 ≤ k ≤ 10\)
3 题解
最长上升子序列的 \(dp\) 状态为 \(dp_i\),表示以第 \(i\) 个数为结尾的最长上升子序列长度。这道题中增加了 \(k\) 这一限定条件,我们便相应地加上 \(j\) 这一维度,使 \(dp_{i, j}\) 表示长度为 \(i\) 且以 \(j\) 为结尾的上升子序列个数。我们不难推出转移:
如果我们暴力求解,那么时间复杂度就是 \(O(n^2k)\),无法通过此题。枚举 \(i\) 和 \(j\) 的循环看起来不怎么好优化,我们就来想想如何优化里面的循环。我们发现,找前 \(l-1\) 个数中小于第 \(l\) 个数的数这种问题可以利用权值线段树在 \(log\space n\) 的时间复杂度内解决。但是,我们的线段树只能把个数求出来,不能帮助我们计算具体数值。为了解决这个问题,我们可以把 \(dp_{i-1, l}\) 的值存储在 \(a_l\) 在线段树中的位置上,使得我们在统计时就可以直接加上 \(dp\) 值。但是我们现在这样仍然需要建 \(11\) 棵线段树:每棵线段树记录对于一个长度的所有 \(dp\) 值。为了节省空间,我们可以在每次计算完 \(dp\) 值后重新 \(build\) 一遍整棵线段树,然后在计算第 \(l\) 个数时把 \(dp_{i-1, l}\) 的值加给 \(a_l\) 这个数在权值线段树中对应的位置。这样,我们只需要一颗线段树就可以算出来所有的 \(dp\) 值。
最终的答案就是 \(\sum_{l = k}^{n} \limits dp_{k, l}\)。
4 代码(空格警告):
#include <iostream>
using namespace std;
const int N = 1e5+10;
const int K = 15;
#define int long long
int n, k, aans;
int a[N], dp[K][N];
struct node
{
int l, r, sum;
}t[N*4];
void build(int p, int l, int r)
{
t[p].l = l;
t[p].r = r;
if (l == r)
{
t[p].sum = 0;
return ;
}
int mid = (l+r)/2;
build(p*2, l, mid);
build(p*2+1, mid+1, r);
t[p].sum = t[p*2].sum + t[p*2+1].sum;
}
void modify(int p, int pos, int d)
{
if (t[p].l == t[p].r)
{
t[p].sum += d;
return ;
}
int mid = (t[p].l + t[p].r) / 2;
if (pos <= mid) modify(p*2, pos, d);
if (pos > mid) modify(p*2+1, pos, d);
t[p].sum = t[p*2].sum + t[p*2+1].sum;
}
int query(int p, int l, int r)
{
if (t[p].l >= l && t[p].r <= r) return t[p].sum;
int mid = (t[p].l + t[p].r) / 2;
int ans = 0;
if (l <= mid) ans += query(p*2, l, r);
if (r > mid) ans += query(p*2+1, l, r);
return ans;
}
signed main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
build(1, 1, n);
for (int i = 1; i <= n; i++) dp[0][a[i]] = 1;
for (int i = 1; i <= k; i++)
{
build(1, 1, n);
for (int j = 1; j <= n; j++)
{
modify(1, a[j], dp[i-1][j]);
dp[i][j] = query(1, 1, a[j] - 1);
}
}
for (int i = k; i <= n; i++) aans += dp[k][i];
cout << aans;
return 0;
}