Luogu 5010 HMR的LIS Ⅲ
题意简化
给定长度为 n n n 的序列 a 1 , a 2 , … , a n a_1, a_2, \dots, a_n a1,a2,…,an,以及常数 L L L、 R R R。要求构造序列 b b b,满足(设 b b b 序列的长度为 m m m):
- ∀ 1 ≤ i ≤ m \forall 1 \le i \le m ∀1≤i≤m, 1 ≤ b i ≤ n 1 \le b_i \le n 1≤bi≤n;
- ∀ 1 ≤ i < m \forall 1 \le i < m ∀1≤i<m, b i < b i + 1 b_i < b_{i+1} bi<bi+1;
- ∀ 1 ≤ i < m \forall 1 \le i < m ∀1≤i<m, L < a b i + 1 − a b i < R L < a_{b_{i+1}} - a_{b_i} < R L<abi+1−abi<R;
并在此基础上使得长度 m m m 最大。
然而,在保证长度最大的前提下,仍然有许多中可能的构造方案。给定 k k k,输出字典序第 k k k 小的构造方案。
对于 100 % 100\% 100% 的数据:
- 1 ≤ n ≤ 5 × 1 0 5 1 \le n \le 5 \times 10^5 1≤n≤5×105, 1 ≤ k ≤ 1 0 13 1 \le k \le 10^{13} 1≤k≤1013;
- 0 ≤ ∣ L ∣ , ∣ R ∣ ≤ 1 0 9 + 1 0 \le |L|, |R| \le 10^9 + 1 0≤∣L∣,∣R∣≤109+1, R − L > 1 R - L > 1 R−L>1, 0 ≤ a i ≤ 1 0 9 0 \le a_i \le 10^9 0≤ai≤109;
- 保证数据合法且有解。
思路
同样是线段树优化 dp,但是我感觉这题比 NOIP2023 T4 天天爱打卡 简单。 往期回顾
a
i
a_i
ai 这么大,存线段树之前需要离散化 ,这是个常识吧。
k
k
k 这么大,说明方案数可能很大,记得用 long long
来存,这也是个常识吧。
正序倒序都可以把 m m m 求出来,但是 倒序 在构造方案的时候更方便。
设 f i f_i fi 表示 b 1 = i b_1 = i b1=i 时 m m m 的最大值, c i c_i ci 表示此时的方案数。
要求出字典序第 k k k 小的方案。
这个东西怎么讲呢?如果能够从位置 i i i 转移到前一个已选定的状态,如果 k ≤ c i k \le c_i k≤ci,也就是说前 c i c_i ci 个方案全部都要选 i i i,那么第 k k k 个也一定要选 i i i。
如果 k > c i k > c_i k>ci,那么排除了这 c i c_i ci 个方案之后,去到下一个可行的位置找第 k − c i k - c_i k−ci 个方案。
而我们这一个从前往后确定方案的过程,恰好是按照字典序从小到大进行的,所以可行且方便。
for (int i = 1, lst = 0; i <= n && ans; ++i) {
if (f[i] == ans && (!lst || (L+a[lst]<a[i] && R+a[lst]>a[i]))) { //能够转移
if (k > c[i]) k -= c[i];
else {
printf("%d ", i);
lst = i;
--ans;
}
}
}
具体来讲讲 怎样 dp。
设 h ( x ) h(x) h(x) 表示离散化后序号为 x x x 的值,原来是多少。
考虑一个问题:如果当前为 a i a_i ai,那么能够转移过来的 a j a_j aj 应该在哪个区间?
对 L < a j − a i < R L < a_j - a_i < R L<aj−ai<R 进行变形,有 L + a i < a j < R + a i L + a_i < a_j < R + a_i L+ai<aj<R+ai。
开区间。。。没关系。。。
二分查找 h ( l ) > L + a i h(l) > L + a_i h(l)>L+ai, l l l 的最小值; h ( r ) < R + a i h(r) < R + a_i h(r)<R+ai, r r r 的最大值。如果区间 [ l , r ] [l, r] [l,r] 合法,到线段树中查找这个区间的 f m a x f_{max} fmax 以及总共的方案数 c ′ c' c′,则 f i = f m a x + 1 f_i = f_{max} + 1 fi=fmax+1。
如果 f m a x ≠ 0 f_{max} \ne 0 fmax=0,那么 c i = c ′ c_i = c' ci=c′;否则 c i = 1 c_i = 1 ci=1。
(当然如果连这个区间都不合法的话,显然 f i = 0 f_i = 0 fi=0, c i = 1 c_i = 1 ci=1。)
然后就把当前的情况拿去更新线段树 h ( a i ) h(a_i) h(ai) 位置就行了。
for (int i = n; i >= 1; --i) {
int l = upper_bound(hdld+1, hdld+num+1, L + a[i]) - hdld;
int r = lower_bound(hdld+1, hdld+num+1, R + a[i]) - hdld - 1;
int p = lower_bound(hdld+1, hdld+num+1, a[i]) - hdld;
if (l <= r) {
Segment res = query(1, l, r);
if (res.mx) {
f[i] = res.mx + 1, c[i] = res.cnt;
} else {
f[i] = 1, c[i] = 1;
}
} else {
f[i] = 1, c[i] = 1;
}
ans = max(ans, f[i]);
update(1, p, f[i], c[i]);
}
你以为这就结束了?
亲测:就算你该开 long long
的地方都开好了,还是会收获 听取WA一片。
为什么?因为 c i c_i ci 可以非常大,甚至可以远超 1 0 18 10^{18} 1018。
64 不够 128 来凑。(当然没必要。)
进一步分析,我们知道, k k k 是相对比较小的。如果 c i c_i ci 超过了 k k k,不管它超过了多少,都会被选掉。(看看上面贴出来的构造方案的片段,只要 k ≤ c i k \le c_i k≤ci,不管差多少,都要进第二个分支。)
那么只要某个时候它超过了
k
k
k,手动给它赋值
k
+
1
k + 1
k+1,就不存在超 long long
表示范围的问题了。当然真正实现对
c
i
c_i
ci 做加法的是线段树里面,要在线段树里面修改。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 5e5+5;
struct Segment {
int l, r;
int mx;
long long cnt;
} s[MAXN<<2];
long long k, c[MAXN];
int n, L, R, a[MAXN], f[MAXN];
int hdld[MAXN], num, ans;
Segment merge(Segment lc, Segment rc) {
Segment res;
res.l = lc.l, res.r = rc.r;
if (lc.mx > rc.mx) {
res.mx = lc.mx, res.cnt = lc.cnt;
} else if (lc.mx < rc.mx) {
res.mx = rc.mx, res.cnt = rc.cnt;
} else {
res.mx = lc.mx, res.cnt = lc.cnt + rc.cnt;
}
if (res.cnt > k) res.cnt = k + 1;
return res;
}
void pushup(int cur) {
s[cur] = merge(s[cur*2], s[cur*2+1]);
}
void build(int cur, int l, int r) {
s[cur].l = l, s[cur].r = r;
if (l == r) {
s[cur].mx = 0;
s[cur].cnt = 1;
return;
}
int mid = (s[cur].l + s[cur].r) >> 1;
build(cur*2, l, mid);
build(cur*2+1, mid+1, r);
pushup(cur);
}
void update(int cur, int p, int mx, long long cnt) {
if (s[cur].l == s[cur].r) {
if (mx > s[cur].mx) {
s[cur].mx = mx;
s[cur].cnt = cnt;
} else if (mx == s[cur].mx) {
s[cur].cnt += cnt;
}
if (s[cur].cnt > k) s[cur].cnt = k + 1;
return;
}
int mid = (s[cur].l + s[cur].r) >> 1;
if (p <= mid) update(cur*2, p, mx, cnt);
else update(cur*2+1, p, mx, cnt);
pushup(cur);
if (s[cur].cnt > 1e18) {
printf("%lld\n", s[cur].cnt);
exit(0);
}
}
Segment query(int cur, int l, int r) {
if (l <= s[cur].l && r >= s[cur].r) {
return s[cur];
}
int mid = (s[cur].l + s[cur].r) >> 1;
if (r <= mid) return query(cur*2, l, r);
if (l > mid) return query(cur*2+1, l, r);
return merge(query(cur*2, l, r), query(cur*2+1, l, r));
}
int main() {
#ifndef ONLINE_JUDGE
freopen("ibvl.in", "r", stdin);
freopen("ibvl.out", "w", stdout);
#endif
scanf("%d%lld%d%d", &n, &k, &L, &R);
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
hdld[i] = a[i];
}
sort(hdld+1, hdld+n+1);
num = unique(hdld+1, hdld+n+1) - hdld - 1;
build(1, 1, num);
for (int i = n; i >= 1; --i) {
int l = upper_bound(hdld+1, hdld+num+1, L + a[i]) - hdld;
int r = lower_bound(hdld+1, hdld+num+1, R + a[i]) - hdld - 1;
int p = lower_bound(hdld+1, hdld+num+1, a[i]) - hdld;
if (l <= r) {
Segment res = query(1, l, r);
if (res.mx) {
f[i] = res.mx + 1, c[i] = res.cnt;
} else {
f[i] = 1, c[i] = 1;
}
} else {
f[i] = 1, c[i] = 1;
}
ans = max(ans, f[i]);
update(1, p, f[i], c[i]);
}
printf("%d\n", ans);
for (int i = 1, lst = 0; i <= n && ans; ++i) {
if (f[i] == ans && (!lst || (L+a[lst]<a[i] && R+a[lst]>a[i]))) {
if (k > c[i]) k -= c[i];
else {
printf("%d ", i);
lst = i;
--ans;
}
}
}
return 0;
}