把「贪心匹配一份 s2」压成一次确定性状态转移,再对「走 2^k 份 s2」倍增建表,在 O(|s1|²|s2| + |s1|log n1) 内求出最多能放多少份 s2。
OJ: roj
题目 ID: 3155
难度:提高+/省选-
标签:倍增贪心字符串python
创建: 2026-10-01 21:31
更新: 2026-10-10 11:30
形式化题目
记
给定
其中
正解
思路
第一步:把 还原成"能放几份 "
因为
那么答案就是
于是问题只剩一件事:数出
第二步:朴素扫描,以及它为什么不够快
最直接的做法是逐字符贪心:展开 s1[pos] == s2[j] 就匹配并让
以样例第一组(
T : a c b a c b a c b a c b
下标: 0 1 2 3 4 5 6 7 8 9 10 11
第1份: a . b → 跨过 1 份 s1,落点回到份首
第2份: a . b → 跨过 1 份 s1
第3份: a . b → 跨过 1 份 s1
第4份: a . b → 跨过 1 份 s1其余四组样例各自检验一个侧面:
| 组 | 输出 | 它在检验什么 | ||
|---|---|---|---|---|
| 1 | ab 2 | acb 4 | 2 | 每份 |
| 2 | acb 1 | acb 1 | 1 | |
| 3 | aa 1 | aaa 3 | 4 | 9 个 a 里能放 4 个 aa,答案可以超过 |
| 4 | baab 1 | baba 11 | 7 | 起点偏移 0/1/2 都要跨过份边界才能匹配完 |
| 5 | aaaaa 1 | aaa 20 | 12 | 60 个 a 刚好被 12 份 aaaaa 铺满 |
"能匹配就立刻匹配"是安全的:匹配得越早,留给
但这个扫描是
第三步:把"匹配一份 "压成一次状态转移
一份
match_once(p) = ( 匹配一份 s2 跨过的完整 s1 份数 blocks, 匹配结束时的份内偏移 q )状态只有
| 起点偏移 |
消耗的 blocks |
落点偏移 |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 2 | 0 |
| 2 | 2 | 0 |
match_once 也不需要展开
第四步:倍增——把"走 份 "预计算出来
只走"份数"次仍然是
关键观察:转移是确定性的,所以"匹配
up[0][p] = match_once(p)
up[k][p] = 先走 up[k-1][p],再在它的落点上走一次 up[k-1]样例第一组的完整倍增表((消耗份数, 落点偏移)):
| 步数 |
||||
|---|---|---|---|---|
| 0 | 1 | (1, 0) | (2, 0) | (2, 0) |
| 1 | 2 | (2, 0) | (3, 0) | (3, 0) |
| 2 | 4 | (4, 0) | (5, 0) | (5, 0) |
读表的要点:
第五步:用二进制拆分挑出最大的
最后从最大的
used, p, copies = 0, 0, 0
对 k 从大到小:
blocks, q = up[k][p]
need = used + blocks + (q > 0) # 落点不在份首时,当前这一份 s1 也被占用
若 need <= n1: used, p, copies = used + blocks, q, copies + 2^k判据里 used 是此前已经完整跨过的 blocks 是这一步跨过的份数,最后一项
样例第一组的挑选过程:
| 步数 | 累计占用 |
是否采纳 | ||
|---|---|---|---|---|
| 2 | 4 | (4, 1) | ✗ | |
| 1 | 2 | (2, 1) | ✓ | |
| 0 | 1 | (1, 1) | ✗ |
代码
/**
* 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-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 最大重复:s2 作为子序列在 conn(s1, n1) 中的最大重复次数,贪心 + 倍增状态。
#include <cstdio>
#include <string>
#include <vector>
#include <iostream>
typedef long long ll;
struct St {
ll blocks; // 跨过的完整 s1 份数
ll pos; // 下一份内的起点
};
std::string s1, s2;
ll L1, L2;
// 从 s1 的下标 p 出发贪心匹配一整份 s2
St match_once(ll p) {
ll j = 0;
ll blocks = 0, pos = p;
while (j < L2) {
if (s1[pos] == s2[j]) j++;
pos++;
if (pos == L1) { // 走完一份 s1,落到下一份的开头
pos = 0;
blocks++;
}
}
St r;
r.blocks = blocks;
r.pos = pos;
return r;
}
ll bit_length(ll x) {
ll c = 0;
while (x) {
c++;
x >>= 1;
}
return c;
}
// 返回 s2 作为子序列最多能在 conn(s1, n1) 中出现多少次
ll max_copies(ll n1) {
bool has[256];
for (int i = 0; i < 256; i++) has[i] = false;
for (ll i = 0; i < L1; i++) has[(int)(unsigned char)s1[i]] = true;
for (ll i = 0; i < L2; i++) {
if (!has[(int)(unsigned char)s2[i]]) return 0; // 有字符不在 s1 里,一份都放不下
}
std::vector<St> base(L1);
for (ll p = 0; p < L1; p++) base[p] = match_once(p);
ll LOG = bit_length(n1 * L1 / L2 + 1);
if (LOG < 1) LOG = 1;
std::vector<std::vector<St> > up(LOG);
up[0] = base;
for (ll k = 1; k < LOG; k++) {
up[k].assign(L1, St());
for (ll p = 0; p < L1; p++) {
St a = up[k - 1][p];
St b = up[k - 1][a.pos];
up[k][p].blocks = a.blocks + b.blocks;
up[k][p].pos = b.pos;
}
}
ll used = 0, p = 0, copies = 0;
for (ll k = LOG - 1; k >= 0; k--) { // 从大步到小步,能加就加
St a = up[k][p];
ll need = used + a.blocks + (a.pos > 0 ? 1 : 0); // 落点不在份首时当前这一份也被占用
if (need <= n1) {
used += a.blocks;
p = a.pos;
copies += 1LL << k;
}
}
return copies;
}
int main() {
std::vector<std::string> tok;
std::string t;
while (std::cin >> t) tok.push_back(t);
// 输入末尾可能粘着一个 '}',它不是字符串内容
if (!tok.empty() && tok.back() == "}") tok.pop_back();
for (size_t g = 0; g + 3 < tok.size(); g += 4) {
s2 = tok[g];
ll n2 = atoll(tok[g + 1].c_str());
s1 = tok[g + 2];
ll n1 = atoll(tok[g + 3].c_str());
L1 = (ll)s1.size();
L2 = (ll)s2.size();
// conn(s2, n2) 重复 m 次就是 s2 重复 m * n2 次
printf("%lld\n", max_copies(n1) / n2);
}
return 0;
}#!/usr/bin/env python3
# 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-10-01 21:33
# update_at: 2026-10-01 21:52
import sys
def match_once(p: int, s1: bytes, s2: bytes) -> tuple[int, int]:
"""从 s1 的下标 p 出发贪心匹配一整份 s2,返回(跨过的完整 s1 份数, 下一份内的起点)。
每读满一份 s1 至少吃掉 s2 的一个字符(否则该字符根本不在 s1 里,会死循环),
所以最多扫过 len(s2) 份 s1;调用方必须先用字符集检查排除无解情况。
"""
j = 0 # s2 中下一个待匹配的字符
blocks, pos = 0, p
while j < len(s2):
if s1[pos] == s2[j]:
j += 1
pos += 1
if pos == len(s1): # 走完一份 s1,落到下一份的开头
pos, blocks = 0, blocks + 1
return blocks, pos
def max_copies(s1: bytes, s2: bytes, n1: int) -> int:
"""返回 s2 作为子序列最多能在 conn(s1, n1) 中出现多少次(贪心 + 倍增)。"""
if not set(s2) <= set(s1):
return 0 # s2 有字符不在 s1 里,一份都放不下
L1 = len(s1)
L2 = len(s2)
# base[p]:从一份 s1 的下标 p 出发匹配完一份 s2,消耗的完整 s1 份数与落点。
# 匹配是确定性的,所以「走 c 份 s2 后的状态」唯一,可以对状态做倍增。
base = [match_once(p, s1, s2) for p in range(L1)]
LOG = (n1 * L1 // L2 + 1).bit_length() # 份数上限的二进制位数,够倍增到它
up = [base]
for _ in range(1, LOG):
prev = up[-1]
nxt: list[tuple[int, int]] = []
for p in range(L1):
blocks, q = prev[p] # 先走 2^(k-1) 份 s2,落在偏移 q
more, q2 = prev[q] # 再走 2^(k-1) 份 s2
nxt.append((blocks + more, q2))
up.append(nxt)
used, p, copies = 0, 0, 0
for k in reversed(range(LOG)): # 从大步到小步,能加就加
blocks, q = up[k][p]
need = used + blocks + (q > 0) # 落点不在份首时,当前这一份 s1 也被占用
if need <= n1:
used, p, copies = used + blocks, q, copies + (1 << k)
return copies
def solve() -> None:
raw = sys.stdin.buffer.read().rstrip() # 输入末尾可能粘着一个 '}'(不是字符串内容),先把空格换行退掉
data = raw.removesuffix(b'}').split()
out: list[str] = []
for group in range(0, len(data), 4): # 每组固定 4 个 token
s2, n2, s1, n1 = data[group: group + 4] # 题面的顺序:先 s2 n2,再 s1 n1
# conn(s2, n2) 重复 m 次就是 s2 重复 m * n2 次,故 m = 最多份数 // n2
out.append(str(max_copies(s1, s2, int(n1)) // int(n2)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
设
- 预处理
base:, 时约 次字节索引; - 倍增建表与挑选:
,其中 ; - 每组总计
,空间 。
对比朴素扫描的
总结
- 由
把答案化为 ,问题变成"数出 在 里最多能不重叠出现多少次"。 - 贪心匹配(能匹配就匹配)保证最优;把一份
的匹配压缩成"份内偏移 → (消耗份数, 落点偏移)"的确定性转移。 - 转移确定性
可以倍增: up[k]由up[k-1]自拼得到,于是用二进制拆分在内求出 。 - 判据是占用份数
need = used + blocks + (q > 0) <= n1,落点不在份首时不能漏掉当前这一份。
图示解析
下图左侧是一份
读图要点:
- 每个"份内偏移"只有一条出边,边权是吃掉几份
——这正是"路线唯一、可倍增"的来源; up[1] = up[0] ∘ up[0]、up[2] = up[1] ∘ up[1],走份 的答案一步都不用真走; - 挑选时先试大步再试小步,绿色表示采纳(占用份数不超
),灰色表示超出;样例中只有 被采纳,得到 、 。