题目链接
题目描述
小苯是“小红书app”的一位博主,他在自己的账号中发布了 篇分享,编号从
到
。
有一天他想试试看自己能不能准确的记住这 篇分享的点赞数,但他很快就发现凭自己的记忆力根本没法准确地记住每一篇的点赞数。但他记得如下两种信息:
-
每篇分享的点赞量都为正数,且不超过
。
-
第
篇分享的点赞量和第
篇分享的点赞量的大小关系。
他想知道,在已知这些信息的前提下,所有分享的点赞量有多少种不同的可能。
(结果可能很大,请输出其对 取模的值)
输入描述:
第一行两个正整数 (
),分别表示分享的总个数和每篇分享的点赞数的上限。
第二行一个长度为 的字符串
,其中只包含
>
,<
,=
这三种字符。如果 的第
个字符 (0-indexed) 是
>
,则第 篇分享的点赞量严格高于第
篇,
<
和 =
同理。
思路分析
这是一道典型的计数类动态规划问题。由于数据范围 ,一个时间复杂度为
的解法是可行的。
1. DP 状态定义
我们定义 为:考虑了前
篇分享,且第
篇分享的点赞数为
时的合法方案数。
我们的最终目标是求出所有 篇分享的合法方案总数,即
。
2. 状态转移
为了计算 ,我们需要根据第
篇和第
篇分享之间的关系(由字符串
的第
个字符给出),从
的各个状态转移而来。
-
基础状态:
对于第 1 篇分享 (
),它可以取
中的任何一个值,每种取法都只有 1 种方案。
所以,
,对于所有的
。
-
递推关系 (对于
):
令
为连接第
篇和第
篇分享的关系符号。
-
如果
:
这意味着第
篇的点赞数必须等于第
篇的点赞数。如果第
篇点赞数为
,那么第
篇的点赞数也必须为
。
-
如果
:
这意味着第
篇的点赞数必须小于第
篇的点赞数。如果第
篇点赞数为
,那么第
篇的点赞数可以是
中的任意一个。
-
如果
:
这意味着第
篇的点赞数必须大于第
篇的点赞数。如果第
篇点赞数为
,那么第
篇的点赞数可以是
中的任意一个。
-
3. 使用前缀和优化
直接按照上述转移方程计算,时间复杂度为 ,会超时。
我们可以观察到,转移过程中频繁出现了对
数组的区间求和。这种操作可以使用前缀和来进行优化。
我们定义一个前缀和数组 。
这个数组可以在计算完第
行的所有
值后,
的时间内计算出来。
这样,转移方程就可以被优化为 的查询:
-
对于
:
-
对于
:
可以通过总和减去前面部分得到:
通过这种方式,我们可以在 的时间内完成从第
行到第
行的计算。总的时间复杂度就降到了
。
4. 空间优化
在计算 的值时,我们只依赖于
的信息。因此,没有必要存储整个
的二维数组。我们可以使用两个一维数组(一个存当前行,一个存上一行)或者一个一维数组(滚动数组)来将空间复杂度从
优化到
。
代码
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
using namespace std;
using ll = long long;
const int MOD = 1e9 + 7;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
string s;
cin >> s;
vector<ll> dp(m + 1, 0);
// 初始化 i = 1 的情况
for (int j = 1; j <= m; ++j) {
dp[j] = 1;
}
for (int i = 2; i <= n; ++i) {
vector<ll> prefix_sum(m + 1, 0);
for (int j = 1; j <= m; ++j) {
prefix_sum[j] = (prefix_sum[j - 1] + dp[j]) % MOD;
}
vector<ll> new_dp(m + 1, 0);
char op = s[i - 2];
if (op == '=') {
for (int j = 1; j <= m; ++j) {
new_dp[j] = dp[j];
}
} else if (op == '<') {
for (int j = 1; j <= m; ++j) {
new_dp[j] = prefix_sum[j - 1];
}
} else { // op == '>'
for (int j = 1; j <= m; ++j) {
new_dp[j] = (prefix_sum[m] - prefix_sum[j] + MOD) % MOD;
}
}
dp = new_dp;
}
ll total_sum = 0;
for (int j = 1; j <= m; ++j) {
total_sum = (total_sum + dp[j]) % MOD;
}
cout << total_sum << endl;
return 0;
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
String s = sc.next();
final int MOD = 1_000_000_007;
long[] dp = new long[m + 1];
// 初始化 i = 1
for (int j = 1; j <= m; j++) {
dp[j] = 1;
}
for (int i = 2; i <= n; i++) {
long[] prefixSum = new long[m + 1];
for (int j = 1; j <= m; j++) {
prefixSum[j] = (prefixSum[j - 1] + dp[j]) % MOD;
}
long[] newDp = new long[m + 1];
char op = s.charAt(i - 2);
if (op == '=') {
for (int j = 1; j <= m; j++) {
newDp[j] = dp[j];
}
} else if (op == '<') {
for (int j = 1; j <= m; j++) {
newDp[j] = prefixSum[j - 1];
}
} else { // op == '>'
for (int j = 1; j <= m; j++) {
newDp[j] = (prefixSum[m] - prefixSum[j] + MOD) % MOD;
}
}
dp = newDp;
}
long totalSum = 0;
for (int j = 1; j <= m; j++) {
totalSum = (totalSum + dp[j]) % MOD;
}
System.out.println(totalSum);
}
}
import sys
def solve():
try:
n, m = map(int, sys.stdin.readline().split())
s = sys.stdin.readline().strip()
except (IOError, ValueError):
return
MOD = 10**9 + 7
# dp[j] 表示当前处理到第 i 篇,且点赞数为 j 的方案数
dp = [0] * (m + 1)
# 初始化 i = 1 的情况
for j in range(1, m + 1):
dp[j] = 1
for i in range(2, n + 1):
prefix_sum = [0] * (m + 1)
for j in range(1, m + 1):
prefix_sum[j] = (prefix_sum[j - 1] + dp[j]) % MOD
new_dp = [0] * (m + 1)
op = s[i - 2]
if op == '=':
for j in range(1, m + 1):
new_dp[j] = dp[j]
elif op == '<':
for j in range(1, m + 1):
new_dp[j] = prefix_sum[j - 1]
else: # op == '>'
for j in range(1, m + 1):
new_dp[j] = (prefix_sum[m] - prefix_sum[j] + MOD) % MOD
dp = new_dp
total_sum = sum(dp) % MOD
print(total_sum)
solve()
算法及复杂度
-
算法:动态规划 + 前缀和优化
-
时间复杂度:
。我们有一个两层的循环,外层循环
次,内层循环
次来构建前缀和数组和新的 DP 数组。
-
空间复杂度:
。通过使用滚动数组(或在每次迭代中创建新数组)的方式,我们只需要存储上一行和当前行的 DP 状态,以及一个前缀和数组,空间都与
成正比。