Luogu 5010 HMR的LIS Ⅲ 题解

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 ∀1im 1 ≤ b i ≤ n 1 \le b_i \le n 1bin
  • ∀ 1 ≤ i < m \forall 1 \le i < m ∀1i<m b i < b i + 1 b_i < b_{i+1} bi<bi+1;
  • ∀ 1 ≤ i < m \forall 1 \le i < m ∀1i<m L < a b i + 1 − a b i < R L < a_{b_{i+1}} - a_{b_i} < R L<abi+1abi<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 1n5×105 1 ≤ k ≤ 1 0 13 1 \le k \le 10^{13} 1k1013
  • 0 ≤ ∣ L ∣ , ∣ R ∣ ≤ 1 0 9 + 1 0 \le |L|, |R| \le 10^9 + 1 0L,R109+1 R − L > 1 R - L > 1 RL>1 0 ≤ a i ≤ 1 0 9 0 \le a_i \le 10^9 0ai109
  • 保证数据合法且有解。

思路

同样是线段树优化 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 kci,也就是说前 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 kci 个方案。

而我们这一个从前往后确定方案的过程,恰好是按照字典序从小到大进行的,所以可行且方便。

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<ajai<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 kci,不管差多少,都要进第二个分支。)

那么只要某个时候它超过了 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;
}
  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值