T30382: 复制粘贴
greedy + dp/bfs, http://cs101.openjudge.cn/practice/30382/
Alice 有两个字符串 S 和 T,她的希望通过对字符串 T 进行一系列操作,使得字符串 S 为字符串T的一个子序列。这里的“子序列”是指通过从一个字符串中删除一些字符(可以是零个或多个,也可以不删),但不改变剩余字符的顺序,得到的新字符串。
Alice 可以进行的操作有两种:复制和粘贴。其中复制操作会将目前整个T串覆写到剪切板,粘贴操作会将剪切板的内容新增在T串的末尾。注意,一开始剪切板为空,并且粘贴操作不会清空剪切板。
现在,你需要回答,Alice 至少需要操作多少次才能达到目的?如果无论怎样操作,Alice 都无法达到目的,请你输出 “-1”(不包含引号)。
输入
第一行输入一个只包含小写字母的字符串 S(1≤|S|≤10^5); 第二行输入一个只包含小写字母的字符串 T(1≤|T|≤10^5)。
输出
输出一个整数,表示 Alice 需要操作的次数。
样例输入
abbbbbbbbb
cbaca样例输出
7提示
greedy + dp
来源
2025fall
这个问题可以分为两个主要步骤来解决:
计算最少需要的
的副本数量 : 我们需要找到最小的正整数 ,使得字符串 是 个 拼接而成的字符串 的一个子序列。 我们可以使用贪心算法:遍历字符串 的每一个字符,尝试在当前的 副本中找到它。如果当前副本中剩余的部分没有这个字符,我们就开启一个新的 副本(即 增加 1),并从新副本的开头开始匹配。 为了高效地执行此操作,我们可以预处理字符串 ,记录每个位置之后每个字母下一次出现的位置(序列自动机思想)。 计算达到至少
个副本的最少操作次数: 这实际上是一个经典的动态规划问题:从 1 个副本开始,通过“复制全部”和“粘贴”操作,最少需要多少次操作可以得到至少 个副本。 - 复制操作:将当前副本总数存入剪切板(消耗 1 次操作)。
- 粘贴操作:将剪切板中的副本数加到总数中(每次消耗 1 次操作)。 假设当前有
个副本,如果我们执行 1 次复制和 次粘贴,我们将得到 个副本,总共消耗了 次操作。 因此,状态转移方程为: 。 由于我们需要的是“至少” 个,而有时候达到一个比 大但因数分解更优的数(例如 )可能比达到一个较大的质数 更快,所以我们需要在 范围内取 的最小值。
Python 3 实现。
- 输入输出:使用
sys.stdin.read().split()批量读取以提高效率。 - 字符串匹配:使用
bisect_left(二分查找)。 - DP 计算:Python 的循环效率较低,但
的算法(类似于埃氏筛法)在 的量级下配合简洁的循环体是可以跑通的。
import sys
from bisect import bisect_left
def solve():
# 使用 sys.stdin.read 一次性读取,防止多次 I/O 带来的开销
input_data = sys.stdin.read().split()
if not input_data:
return
s = input_data[0]
t = input_data[1]
n = len(s)
m = len(t)
# 1. 预处理 T 中每个字符出现的所有索引位置
char_indices = [[] for _ in range(26)]
ord_a = ord('a')
for i, char in enumerate(t):
char_indices[ord(char) - ord_a].append(i)
# 2. 贪心计算最少需要的副本数 k
k = 1
curr_pos = 0 # 当前在 T 副本中的匹配位置
for char in s:
indices = char_indices[ord(char) - ord_a]
if not indices:
# S 中存在 T 中没有的字符,无法匹配
print("-1")
return
# 使用二分查找寻找当前副本中第一个大于等于 curr_pos 的字符索引
it = bisect_left(indices, curr_pos)
if it < len(indices):
# 在当前副本的剩余部分找到了
curr_pos = indices[it] + 1
else:
# 当前副本匹配完了,需要开启一个新副本
k += 1
curr_pos = indices[0] + 1
# 如果 1 个副本就够了,不需要任何操作
if k == 1:
print(0)
return
# 3. DP 计算最少操作次数
# 这是一个经典的“复制与粘贴”问题,目标是得到至少 k 个副本。
# 达到 x 个副本的最少次数等于其所有质因数之和。
# 由于可以超过 k,我们需要在一个范围内寻找最小值。
# 设置上限。考虑到 2^17 > 10^5,在这个范围内一定能找到最优解。
limit = max(k + 500, 131072)
if limit > 200005:
limit = 200005
# dp[i] 表示得到恰好 i 个副本的最少操作次数
# 初始值设为 i,表示 1 次复制后进行 i-1 次粘贴
dp = list(range(limit + 1))
dp[0] = 0
dp[1] = 0
# 状态转移:从 i 个副本出发,复制一次,粘贴 (j-1) 次,得到 i*j 个副本
# 总代价 = dp[i] + j
for i in range(2, limit // 2 + 1):
base_cost = dp[i]
# v = i * j, 则 j = v // i
# 这个循环类似于素数筛法,复杂度为 O(N log N)
for v in range(i * 2, limit + 1, i):
cost = base_cost + (v // i)
if cost < dp[v]:
dp[v] = cost
# 在所有大于等于 k 的副本数中找最小值
print(min(dp[k:]))
if __name__ == "__main__":
solve()关键点解析:
- 贪心策略:我们总是尽可能在当前的
副本中多匹配字符。只有当当前副本中不再有目标字符时,才“消耗”一个新的副本。这保证了得到的 是最小的。 - DP 逻辑:
- 执行一次“复制”操作后,剪切板里有
个副本。 - 随后执行
次“粘贴”,每次增加 个。 - 总共得到
个副本。 - 操作次数增加
次。 - 因此转移方程为
dp[i * j] = dp[i] + j。
- 执行一次“复制”操作后,剪切板里有
- 为什么
min(dp[k:])?:有时候达到一个比稍大的数字(比如 时,达到 只需要 8 步,而达到 13 需要 13 步)反而更划算。 - 性能:在 Python 中,
range步长循环(如range(i*2, limit+1, i))是由底层 C 实现的,速度非常快,足以在 1 秒内处理量级的数据。
这段代码的逻辑:
python# 确定搜索范围:至少要到 k,但为了找更优解,往后多看一点 limit = max(k + 500, 131072) # 如果算出来的上限太大,把它压回到 200005,防止浪费时间 if limit > 200005: limit = 200005 # ... 后面进行 DP ... # 最后结果取 [k, limit] 范围内的最小值 print(min(dp[k:]))这一段的存在,是为了让程序不只是“死脑筋”地去找恰好
个副本的方法,而是能聪明地发现:“哎呀,我多做一个副本,步数反而更短呢!” 这一段代码的核心逻辑是为了处理“达到
个副本的最少操作次数”这一步。 简单来说:如果目标
是一个很大的质数,直接达到 会非常贵;但如果稍微多做几个副本(达到一个比 大一点点的合数),操作次数反而可能更少。 我们可以从以下几个维度来拆解这段逻辑:
1. 为什么可以超过
? 题目要求“使得字符串 S 为字符串 T 的一个子序列”。如果你通过操作得到了
个 拼接成的字符串,满足了条件,那么如果你通过更少的操作得到了 个 (只要 ), 显然也一定是这 个 的子序列。 因此,我们的目标是:求达到任意 的副本数所需的最少操作次数。 2. 操作次数的规律(合数 vs 质数)
在“复制+粘贴”模型中,得到
个副本的代价,本质上是把 进行因数分解,代价等于所有质因子的和。
- 例子 A: 假设
(质数)。
- 为了刚好得到 13 个,你只能:1次复制,12次粘贴。总次数 = 13。
- 例子 B: 观察比 13 大一点的数,比如
或 或 。
- 得到 14:(1复1粘)得到2,(1复6粘)得到14。总次数 =
。 - 得到 15:(1复2粘)得到3,(1复4粘)得到15。总次数 =
。 - 得到 16:(1复1粘)得到2,(1复1粘)得到4... 总次数 =
。 结论: 即使我们需要 13 个,但多做 2 个达到 15 个,反而能省下 5 次操作。
3. 为什么设置这些具体的数字?
131072(): 因为 ,所以 最大也就是 左右。 是第一个大于 的 2 的幂。 通过不断的“复制+粘贴一次(翻倍)”,达到 的总步数只需要 步。这是一个非常小的代价。 所以,无论 是多少,最优解的步数绝对不会超过 34 步(除非 本身很小)。为了保证 DP 能搜到这些“便宜”的 的数,我们将上限至少设到这个规模。
k + 500: 这是一个经验性的增量。通常在附近的几百个数内,一定能找到一个由小质数(2, 3, 5)组成的合数,它的代价会远低于 本身。
limit = 200005: 这是一个安全上限。防止加上增量后超过了我们预设的数组范围( 是此类题目常见的数组大小)。即使 达到上限 ,我们也只搜到 为止,这在时间复杂度( )上是可以接受的。
第二部分bfs实现
def count_copies():
import bisect
s = input()
t = input()
char_indices = [[] for _ in range(26)]
for ind, i in enumerate(t):
char_indices[ord(i) - ord('a')].append(ind)
cou, last = 1, -1
for i in s:
ind = ord(i) - ord('a')
if not char_indices[ind]:
return -1
if last >= char_indices[ind][-1]:
last = -1
cou += 1
pla = bisect.bisect_right(char_indices[ind], last)
last = char_indices[ind][pla]
return cou
def bfs(i):
if i == 1:
return 0
from collections import deque
visited = set()
queue = deque([(0, 1, 0)]) # steps, now, clip
visited.add((1, 0))
while queue:
steps, now, clip = queue.popleft()
if now + clip >= i:
return steps + 1
if clip: # 操作1: 粘贴
queue.append((steps + 1, now + clip, clip)) # 使当前数增加clipboard
visited.add((now + clip, clip))
if now > clip: # 操作2: 复制
queue.append((steps + 1, now, now)) # 使clipboard=当前数
visited.add((now, now))
if __name__ == '__main__':
needed = count_copies()
print(-1 if needed == -1 else bfs(needed))