#P3756. 第2题-打击序列计数
-
1000ms
Tried: 24
Accepted: 8
Difficulty: 5
所属公司 :
米哈游
时间 :2025年9月21日
-
算法标签>动态规划
第2题-打击序列计数
解题思路
设最终听到的序列为字符串 s(仅含 L/R)。一次击打会产生 1 个或 2 个相同字母:
- 左鼓:产生
L或LL;右鼓:产生R或RR。
观察可知:一次击打不会跨越字母种类边界,因此答案对每个由相同字符组成的连续段(run) 彼此独立,再把每段的方案数相乘即可。
对一个长度为 k 的 L(或 R)连续段,我们要把它切分成若干块,每块长度只能是 1 或 2(分别代表一次击打发出 1 个或 2 个相同的声)。
这正是典型的 1/2 划分计数,令 f[i] 表示长度为 i 的连续段的切分方案数,则
因此 f[i]=Fib(i+1)。 于是整串的答案为:各连续段长度 k 的 f[k] 的乘积,对模 109+7 取模。
实现要点(算法:RLE + DP(斐波那契)):
- 预处理斐波那契(或上述 f)到所有测试用例中出现的最大串长(≤2×105)。
- 扫描 s,做一次游程编码(run-length encoding),每遇到一个连续段长度为 k,把答案乘上 f[k]。
- 乘法取模。
复杂度分析
- 预处理:O(N),其中 N=max∣s∣(全体测试用例的最大长度,≤2×105)。
- 逐例计算:线性扫描,每个字符只访问一次,总计 O(∑∣s∣)。
- 空间复杂度:O(N) 用于存储 f 数组。
代码实现
Python
import sys
MOD = 10**9 + 7
def precompute_f(max_n):
# f[i]: 长度为 i 的连续段(L 段或 R 段)的切分方案数
f = [0] * (max_n + 2)
f[0] = 1 # 空段 1 种
f[1] = 1 # 长度 1 只能切成 [1]
for i in range(2, max_n + 1):
f[i] = (f[i-1] + f[i-2]) % MOD
return f
def count_for_string(s, f):
# 对字符串 s 做 RLE,并累乘对应的 f[len]
n = len(s)
ans = 1
i = 0
while i < n:
j = i
# 找到 s[i] 这一段的右端
while j < n and s[j] == s[i]:
j += 1
run_len = j - i
ans = (ans * f[run_len]) % MOD
i = j
return ans
def main():
data = sys.stdin.read().strip().split()
t = int(data[0])
strings = data[1:1+t]
max_len = max(len(s) for s in strings) if t > 0 else 0
f = precompute_f(max_len)
out = []
for s in strings:
out.append(str(count_for_string(s, f)))
print("\n".join(out))
if __name__ == "__main__":
main()
Java
import java.util.*;
public class Main {
static final long MOD = 1_000_000_007L;
// 预处理 f 到 maxN
static long[] precomputeF(int maxN) {
long[] f = new long[maxN + 2];
f[0] = 1; // 空段
if (maxN >= 1) f[1] = 1; // 长度 1
for (int i = 2; i <= maxN; i++) {
f[i] = (f[i-1] + f[i-2]) % MOD;
}
return f;
}
// 计算单个 s 的答案
static long solveOne(String s, long[] f) {
long ans = 1;
int n = s.length();
int i = 0;
while (i < n) {
int j = i + 1;
// 找到连续段 [i, j)
while (j < n && s.charAt(j) == s.charAt(i)) j++;
int len = j - i;
ans = (ans * f[len]) % MOD;
i = j;
}
return ans;
}
public static void main(String[] args) {
// 数据总长 <= 2e5,用 Scanner 足够
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
String[] arr = new String[t];
int maxLen = 0;
for (int i = 0; i < t; i++) {
arr[i] = sc.next();
maxLen = Math.max(maxLen, arr[i].length());
}
long[] f = precomputeF(maxLen);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < t; i++) {
sb.append(solveOne(arr[i], f)).append('\n');
}
System.out.print(sb.toString());
sc.close();
}
}
C++
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007LL;
// 预处理 f 到 maxN:f[0]=1, f[1]=1, f[i]=f[i-1]+f[i-2]
vector<long long> precomputeF(int maxN) {
vector<long long> f(maxN + 2, 0);
f[0] = 1;
if (maxN >= 1) f[1] = 1;
for (int i = 2; i <= maxN; ++i) {
f[i] = (f[i-1] + f[i-2]) % MOD;
}
return f;
}
// 计算单个 s 的答案
long long solveOne(const string& s, const vector<long long>& f) {
long long ans = 1;
int n = (int)s.size();
int i = 0;
while (i < n) {
int j = i + 1;
while (j < n && s[j] == s[i]) ++j; // 找到连续段 [i, j)
int len = j - i;
ans = (ans * f[len]) % MOD;
i = j;
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
if (!(cin >> t)) return 0;
vector<string> strs(t);
int maxLen = 0;
for (int i = 0; i < t; ++i) {
cin >> strs[i];
maxLen = max(maxLen, (int)strs[i].size());
}
auto f = precomputeF(maxLen);
for (int i = 0; i < t; ++i) {
cout << solveOne(strs[i], f) << "\n";
}
return 0;
}
题目内容
在这个世界里,有左右两个大鼓。对左鼓的击打记为 "L",对右鼓的击打记为 "R"。有趣的是,一次击打可能发出一次声音,也可能因为共鸣而发出两次相同的声音:
- 一次左鼓击打要么发出 "L",要么发出 "LL";
- 一次右鼓击打要么发出 "R",要么发出 "RR"。
你只记录到了最终听到的声音序列s (仅由字符 'L' 和 'R' 组成)。请问,有多少种原始的击打序列 p 能产生该声音序列?
输入描述
第一行包含一个整数 t(1≤t≤105),表示测试用例数。 接下来 t 行,每行包含一个非空字符串 s ,由字符 'L' 和 'R' 组成,长度记为 ∣s∣ 。 保证所有测试用例中 ∑∣s∣≤2×105。
输出描述
对于每个测试用例,输出一行,包含一个整数——能产生该声音序列的击打序列总数。 由于答案可能很大,请对 109+7 取模后输出。
样例1
输入
5
LR
LLRR
LRLR
L
RRR
输出
1
4
1
1
3
说明
LR:
- 左鼓击打一次发出 L,右鼓击打一次发出 R;
- 只有一种击打序列 LR。
LLRR:
- 左鼓击打一次发出 LL,右鼓击打一次发出 RR;
- 左鼓击打两次各发出 L,右鼓击打一次发出 RR;
- 左鼓击打一次发出 LL,右鼓击打两次各发出 R;
- 左鼓击打两次各发出 L,右鼓击打两次各发出 R;
- 共 4 种击打序列。
RRR:
- 右鼓击打一次发出 RR,右鼓击打一次发出 R;
- 右鼓击打一次发出 R,右鼓击打一次发出 RR;
- 右鼓击打三次各发出 R;
- 共 3 种击打序列。