洛谷P17628 信封的选择 题解 根号分治 + 连续段模拟

题目链接:https://www.luogu.com.cn/problem/P17628

一、将纸条离散化

所有折点都是整数,因此折叠后,纸条的厚度在每个单位区间内保持不变。

我们可以将当前长度为 \(L\) 的纸条表示为一个厚度序列:

\[h_1,h_2,\ldots,h_L \]

其中 \(h_i\) 表示从左到右第 \(i\) 个单位区间的厚度。初始时,序列长度为 \(n\),所有元素均为 \(1\)。

不需要记录纸条在数轴上的实际坐标,因为每次折点都是相对于当前最左端给出的。

设折点距离左端为 \(\ell\)。从折点向两侧观察:

  • 左侧厚度依次为 \(h_\ell,h_{\ell-1},\ldots,h_1\);
  • 右侧厚度依次为 \(h_{\ell+1},h_{\ell+2},\ldots,h_L\)。

折叠后,两侧距离折点相同的位置会重合,因此从折点向外的厚度为

\[g_j=h_{\ell-j+1}+h_{\ell+j}, \qquad 1\le j\le \max(\ell,L-\ell), \]

其中越界的元素视为 \(0\)。

于是:

  • 当 \(p=1\) 时,折叠后从左到右的序列就是 \(g\);
  • 当 \(p=2\) 时,折叠后从左到右的序列是 \(g\) 的逆序。

这也说明:在同一个折点向不同方向折叠,得到的两个结果互为镜像。


二、按照操作次数分治

设某张纸条一共被操作 \(q\) 次,选择阈值

\[B=\max(1,\lfloor\sqrt n\rfloor). \]

将纸条分为两类:

  • 重纸条:\(q>B\),使用数组逐个单位区间模拟;
  • 轻纸条:\(q\le B\),维护厚度相同的连续段。

不同纸条之间互不影响,可以离线分组后逐张处理,并复用存储空间。

代码中使用四轮按 \(8\) 位划分的稳定基数排序,将操作按照纸条编号分组。稳定性保证同一张纸条的操作顺序不变,再通过原始操作编号将答案放回对应位置。

由于纸条编号不超过 \(10^9\),这一预处理的时间复杂度为 \(O(m)\)。


三、重纸条:数组模拟与均摊分析

1. 始终枚举较短的一侧

如果直接枚举题目指定的折叠部分,复杂度可能很高。例如,反复将整张纸条翻转,每次都可能枚举整个数组,但长度没有任何减少。

根据前面的结论,同一折点向两个方向折叠,结果互为镜像。因此可以:

  1. 始终将较短的一侧叠加到较长的一侧;
  2. 如果这与题目要求的折叠方向不同,就将结果标记为翻转。

这样,每次只需要枚举

\[\min(\ell,L-\ell) \]

个单位区间。

2. 数组如何维护方向

使用:

  • a[l...r]:当前保留的厚度数组;
  • rev:实际从左到右的顺序是否与数组顺序相反。

当 rev = 0 时,实际顺序为

\[a_l,a_{l+1},\ldots,a_r. \]

当 rev = 1 时,实际顺序为

\[a_r,a_{r-1},\ldots,a_l. \]

设当前长度为 \(L=r-l+1\),读入折点偏移 \(d\) 和方向 \(p\)。

如果当前处于翻转状态,需要先将操作转换到数组从左到右的方向:

\[d\leftarrow L-d,\qquad p\leftarrow 3-p. \]

然后分两种情况。

左侧较短,即 \(d\le L-d\)

将左侧反向叠加到右侧,删除数组左端的 \(d\) 个位置。

这相当于向右折叠。如果转换后的操作要求向左折叠,即 \(p=2\),就将 rev 翻转。

右侧较短,即 \(d>L-d\)

将右侧反向叠加到左侧,删除数组右端的 \(L-d\) 个位置。

这相当于向左折叠。如果转换后的操作要求向右折叠,即 \(p=1\),就将 rev 翻转。

当折点位于端点时,较短一侧长度为 \(0\),不需要逐格相加,只可能改变方向标记。

3. 如何维护最大厚度

折叠只会让厚度相加,不会让最大厚度减小。

即使原来的最大值位于被删除的一侧,它也会被加到保留的一侧,对应位置的新厚度不会比它小。

因此只需要维护一个 res,每次相加后用新厚度更新它,无需重新扫描整个数组。

4. 为什么单张纸条是 \(O(n+q)\)

一次操作后,纸条长度变为

\[L'=\max(d,L-d). \]

本次枚举的单位区间数量恰好为

\[\min(d,L-d)=L-L'. \]

也就是说,枚举多少个位置,纸条长度就减少多少。

初始长度为 \(n\),因此所有操作的枚举次数之和不超过 \(n-1\)。

加上初始化数组和每次操作的固定开销,单张重纸条的复杂度为

\[O(n+q). \]


四、轻纸条:维护等厚连续段

轻纸条的操作次数较少,没有必要初始化长度为 \(n\) 的数组。

将相邻且厚度相同的位置合并为一个连续段,每个段记录:

  • len:长度;
  • v:厚度。

初始只有一个段:

\[(n,1). \]

1. 为什么连续段数量只有 \(O(q)\)

假设一次操作前有 \(s\) 个连续段。

在折点处将纸条分成左右两部分,最多会把一个段切成两段,因此两侧的段数之和最多为

\[s+1. \]

接下来,将两侧从折点向外进行合并。

每次输出一个新段时,至少会消耗完左侧或右侧的一个当前段,所以输出的段数不会超过两侧段数之和。

因此,一次折叠后最多有 \(s+1\) 个段。

初始只有一个段,所以经过 \(j\) 次操作后,段数至多为

\[j+1. \]

对于轻纸条,这个数量始终是 \(O(B)\)。

2. 如何在线性时间内完成一次折叠

首先找到折点所在的段,并将其按折点分成左右两部分。

然后用两个指针:

  • 左指针从折点开始,向左遍历;
  • 右指针从折点开始,向右遍历。

假设当前两侧剩余长度分别为 \(x,y\),厚度分别为 \(u,v\)。

当两侧都存在时,接下来一段的长度为

\[\min(x,y), \]

厚度为

\[u+v. \]

输出这一段后,减少两侧的剩余长度;哪一侧的当前段被消耗完,就移动对应指针。

如果某一侧已经遍历结束,则将其厚度视为 \(0\),继续处理另一侧。

为了保持连续段表示的紧凑性,若新段与上一个段厚度相同,就直接合并长度。

这样得到的是从折点向外的厚度序列:

  • 若 \(p=1\),直接保留;
  • 若 \(p=2\),将段序列整体逆序。

定位折点、双指针合并、必要时逆序,都只需要 \(O(s)\) 时间。

3. 单张轻纸条的复杂度

第 \(j\) 次操作前只有 \(O(j)\) 个段,因此一张被操作 \(q\) 次的轻纸条,总复杂度为

\[O(1+2+\cdots+q)=O(q^2). \]


五、正确性证明

引理 1

同一折点向左折叠与向右折叠,所得厚度序列互为逆序。

证明:

两种操作中,距离折点相同的左右两部分都会重合,其厚度之和相同。

区别仅在于这些厚度位于折点左侧还是右侧,因此从左到右观察时,两种结果互为逆序。证毕。

引理 2

重纸条的数组维护能够正确表示每次折叠后的厚度序列。

证明:

程序先根据 rev 将操作转换到数组的方向,再将较短一侧按照到折点的距离,逐个叠加到较长一侧,因此每个重合位置的厚度均被正确计算。

如果实际要求的折叠方向与所选方向不同,根据引理 1,只需翻转整个序列。程序通过修改 rev 完成这一操作。

所以数组及方向标记始终正确表示当前厚度序列。

此外,最大厚度不会减小,且所有增加厚度的位置都会更新答案,因此维护的最大值正确。证毕。

引理 3

轻纸条的连续段维护能够正确表示每次折叠后的厚度序列。

证明:

两个指针分别按距离折点从近到远的顺序遍历左右两侧。

每次取两侧剩余长度的较小值,在这一长度内,两侧厚度均保持不变,因此输出厚度之和恰好等于折叠后的厚度。

当一侧结束时,另一侧不再发生重叠,直接保留其厚度也是正确的。

最后根据折叠方向决定是否逆序,便得到实际从左到右的厚度序列。证毕。

定理

算法输出的每个答案均为对应操作后纸条的最大厚度。

证明:

不同纸条之间相互独立,稳定排序保留了同一纸条内部的操作顺序。

根据引理 2、引理 3,两类纸条均能被正确维护,并得到每次操作后的最大厚度。

最后按照原始操作编号输出,所以所有答案均正确。证毕。


六、复杂度分析

重纸条数量不超过 \(\frac mB\),总复杂度为

\[O\left(\frac{nm}{B}+m\right). \]

对于轻纸条,因为 \(q\le B\),所以

\[\sum q^2\le B\sum q\le mB. \]

总时间复杂度为

\[O\left(\frac{nm}{B}+mB+m\right). \]

取 \(B=\max(1,\lfloor\sqrt n\rfloor)\),得到

\[\boxed{O(m\sqrt n)}. \]

操作、临时排序数组和答案占用 \(O(m)\) 空间,重纸条数组占用 \(O(n)\) 空间,轻纸条连续段占用 \(O(B)\) 空间。逐张处理时复用这些空间,因此空间复杂度为

\[\boxed{O(n+m)}. \]

纸条的总厚度之和始终为 \(n\),所有长度和厚度均可使用 int 保存。

按上述算法核对,两组样例的答案分别为:

  • 样例一:2 2 4 2 5 6 4 4
  • 样例二:2 4 2 4 4 6 2 7 10 4

均与题面一致。


七、参考代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 5, maxm = 1.2e5 + 5;

int n, m, b, a[maxn], ans[maxm], cnt[256];

struct Op {
    int t, p, l, id;
} q[maxm], tmp[maxm];

struct Node {
    int len, v;
} s[2][1005];

void radix_sort() {
    for (int k = 0; k < 32; k += 8) {
        memset(cnt, 0, sizeof cnt);
        for (int i = 1; i <= m; i++) {
            int x = (q[i].t >> k) & 255;
            cnt[x]++;
        }
        for (int i = 1; i < 256; i++)
            cnt[i] += cnt[i - 1];
        for (int i = m; i >= 1; i--) {
            int x = (q[i].t >> k) & 255;
            tmp[cnt[x]] = q[i];
            cnt[x]--;
        }
        for (int i = 1; i <= m; i++)
            q[i] = tmp[i];
    }
}

void solve_heavy(int st, int ed) {
    fill(a + 1, a + n + 1, 1);
    int l = 1, r = n, rev = 0, res = 1;
    for (int k = st; k <= ed; k++) {
        int len = r - l + 1, d = q[k].l, p = q[k].p;
        if (rev) {
            d = len - d;
            p = 3 - p;
        }
        if (d <= len - d) {
            for (int i = 0; i < d; i++) {
                a[l + d + i] += a[l + d - i - 1];
                res = max(res, a[l + d + i]);
            }
            l += d;
            if (p == 2) rev ^= 1;
        }
        else {
            for (int i = 0; i < len - d; i++) {
                a[l + d - i - 1] += a[l + d + i];
                res = max(res, a[l + d - i - 1]);
            }
            r = l + d - 1;
            if (p == 1) rev ^= 1;
        }
        ans[q[k].id] = res;
    }
}

void solve_light(int st, int ed) {
    int cur = 0, num = 1;
    s[0][1] = {n, 1};
    for (int u = st; u <= ed; u++) {
        int k = 1, sum = 0;
        while (sum + s[cur][k].len < q[u].l) {
            sum += s[cur][k].len;
            k++;
        }

        int i = k, j = k, x = q[u].l - sum;
        int y = s[cur][k].len - x;

        if (!x) {
            i--;
            if (i) x = s[cur][i].len;
        }
        if (!y) {
            j++;
            if (j <= num) y = s[cur][j].len;
        }

        int nxt = cur ^ 1, tot = 0, res = 0;
        while (i || j <= num) {
            int len, v = 0;
            if (!i) len = y;
            else if (j > num) len = x;
            else len = min(x, y);

            if (i) v += s[cur][i].v;
            if (j <= num) v += s[cur][j].v;

            if (tot && s[nxt][tot].v == v)
                s[nxt][tot].len += len;
            else {
                tot++;
                s[nxt][tot] = {len, v};
            }
            res = max(res, v);

            if (i) {
                x -= len;
                if (!x) {
                    i--;
                    if (i) x = s[cur][i].len;
                }
            }
            if (j <= num) {
                y -= len;
                if (!y) {
                    j++;
                    if (j <= num) y = s[cur][j].len;
                }
            }
        }

        if (q[u].p == 2)
            reverse(s[nxt] + 1, s[nxt] + tot + 1);
        cur = nxt;
        num = tot;
        ans[q[u].id] = res;
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d", &q[i].t, &q[i].p, &q[i].l);
        q[i].id = i;
    }

    b = max(1, (int)sqrt(n));
    radix_sort();

    for (int l = 1, r; l <= m; l = r + 1) {
        r = l;
        while (r < m && q[r + 1].t == q[l].t) r++;
        if (r - l + 1 > b) solve_heavy(l, r);
        else solve_light(l, r);
    }

    for (int i = 1; i <= m; i++)
        printf("%d\n", ans[i]);
    return 0;
}
posted @ 2026-10-07 21:05  quanjun  阅读(3)  评论(0)    收藏  举报