旋律压缩

无限循环播放只是周期重复,先用 c mod L 折回单周期,再把压缩串解析成段并用前缀和定位对应音符。

OJ: roj

题目 ID: 20018

难度:普及-

标签:字符串前缀和模拟

日期: 2026-08-28 22:10

形式化题目

给定一个行程长度编码字符串 ss':由若干"小写字母 + 十进制正整数"段组成,第 ii(chi,cnti)(ch_i, cnt_i) 表示 cnticnt_i 个连续音符 chich_i,解压得到原始旋律串 SS(长度为 LL)。

S[cmodL]S[c \bmod L],即旋律无限循环播放时第 cc 个音符(从 0 开始编号),输出该小写字母。LL 可能高达约 1.4×10161.4 \times 10^{16},不能显式构造 SS

思路

一句话本质:无限循环只是周期重复,任意大的 cc 先用 k=cmodLk = c \bmod L 折回单周期;压缩串按"字母 + 连续数字"解析成段后,用前缀和区间定位 kk 落在哪一段,输出该段音符。

先看一个直接按题面展开的朴素解,它只适合小数据:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-28 21:26
 * update_at: 2026-08-28 21:26
 */
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 思路:按压缩规则把压缩串直接展开成完整的旋律字符串,
// 再按下标 c % len 直接取第 c 个音符。展开的长度受小数据限制。

string s;     // 压缩旋律串
long long c;  // 询问的第 c 个音符

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> s >> c;

    // 展开:先读一个字母,再把后面连续的数字拼成次数。
    string melody;
    int n = (int)s.length();
    int i = 0;
    while (i < n) {
        char ch = s[i];
        i++;
        long long cnt = 0;
        while (i < n && s[i] >= '0' && s[i] <= '9') {
            cnt = cnt * 10 + (s[i] - '0');
            i++;
        }
        for (long long j = 0; j < cnt; j++) {
            melody.push_back(ch);
        }
    }

    // 无限循环 = 周期重复,取余后直接按下标取音符。
    long long k = c % (long long)melody.length();
    cout << melody[k] << '\n';

    return 0;
}

问题? 压缩串里的每一段到底代表什么?

每段是"音符 + 连续出现次数":a4b1c2d4 表示 4 个 a、1 个 b、2 个 c、4 个 d。解析时先读一个字母,再把后面连续的数字逐位拼成次数——次数可能占多位(d10 表示 10 个 d,不是 d 和 1 和 0)。这一步只是把输入翻译成 (ch,cnt)(ch, cnt) 段,还没有碰到核心问题。

问题? 无限循环播放时,第 cc 个音符落在哪里?

循环就是周期重复:旋律每 LL 个音符回到完全相同的状态。把 cc 写成 c=qL+kc = qL + k,前 qq 个完整周期不影响位置,所以第 cc 个音符 = 单周期内第 kk 个音符,其中 k=cmodLk = c \bmod L。这就是样例 2 的做法:L=17L = 17100mod17=15100 \bmod 17 = 15,只查单周期内第 15 个(0 基)音符。

问题? 知道 kk 后,如何不展开旋律就找到对应音符?

给段做前缀和:第 ii 段覆盖区间 [prei,prei+cnti)[\text{pre}_i, \text{pre}_i + \text{cnt}_i),这些区间互不相交、并成 [0,L)[0, L),所以 kk 恰好落在唯一一段中。从左到右第一个满足 prei+cnti>k\text{pre}_i + \text{cnt}_i > k 的段就是答案段。仍以样例 2 为例:a4 覆盖 [0,4)[0,4)b1 覆盖 [4,5)[4,5)c2 覆盖 [5,7)[5,7)d10 覆盖 [7,17)[7,17)k=15k = 15 落在 d 段,输出 d。

问题? 为什么不能真的把旋律展开?

单周期长度最多约 2×10514×10121.4×1016\frac{2 \times 10^5}{14} \times 10^{12} \approx 1.4 \times 10^{16},展开必然超时超内存。但压缩串本身只有 2×1052 \times 10^5 个字符,解析一遍、前缀和扫一遍都是 O(s)O(|s'|),这就是压缩存在的意义。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-28 21:26
 * update_at: 2026-08-28 21:26
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;

string s;           // 压缩旋律串,只含小写字母和数字
long long c;        // 询问的第 c 个音符,从 0 开始编号
int m;              // 段的个数
char chs[MAXN];     // chs[i]:第 i 段的音符
long long cnts[MAXN]; // cnts[i]:第 i 段音符连续出现的次数
long long sum;      // 单周期总长度 L

// 把压缩串解析成 (音符, 次数) 段,并累加单周期长度。
void parse() {
    int n = (int)s.length();
    int i = 0;
    while (i < n) {
        chs[++m] = s[i]; // 当前位置一定是字母(音符)
        i++;
        long long cnt = 0;
        // 次数可能占多位数字,如 d10 表示 10 个 d,逐位拼出来。
        while (i < n && s[i] >= '0' && s[i] <= '9') {
            cnt = cnt * 10 + (s[i] - '0');
            i++;
        }
        cnts[m] = cnt;
        sum += cnt;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> s >> c;

    parse();

    // 无限循环:第 c 个音符只取决于它在单周期内的位置 k = c mod L。
    c %= sum;

    // 前缀和定位:第 i 段覆盖区间 [pre, pre + cnts[i]),
    // 第一个 pre + cnts[i] > c 的段就是答案段。
    long long pre = 0;
    for (int i = 1; i <= m; i++) {
        if (pre + cnts[i] > c) {
            cout << chs[i] << '\n';
            return 0;
        }
        pre += cnts[i];
    }

    return 0;
}

复杂度

  • 时间:O(s)O(|s'|),解析一遍压缩串,前缀和定位一遍段数组;
  • 空间:O(s)O(|s'|),存储段数组 chs[] / cnts[]

总结

  • 循环 = 取模:周期性重复的问题,第一步总是把大下标折回单周期;
  • 压缩串解析:行程长度编码的解析要小心多位数字次数,逐位拼数即可;
  • 段粒度定位:把"字符下标"查询改成"段区间"查询,一次前缀和扫描完成,不需要真的展开字符串。