视频加载失败

安吉D1-L

1996 字
10 分钟
安吉D1-L
原题呈现

P15862 回去!#

题目描述#

有一个 nn 个点 nn 条边的有向图 GG,满足每个点的出度为 11.每个点有一个小写字母作为权值.

现在 DZ1000 准备在 GG 上游走,他打算用一个字符串 ss 和一个栈 tt 来记录他的行程.初始时栈 tt 为空.

每一步他都有两种可能的行动选择:

  1. 「前进?」:DZ1000 有 pp 的概率沿着当前点的出边移动,并将他到达的节点放进栈 tt 中.
  2. 「回去!」:DZ1000 有 (1p)(1-p) 的概率弹出栈 tt 顶部的节点,然后回到当前位于栈 tt 顶部的节点.(若行动前栈 tt 为空则他不会这么做且一定会前进,若行动前栈 tt 大小为 11 则回到起点.)

走完一步,DZ1000 都会将当前他所在节点的权值接在字符串 ss 的后面.

现在 DZ1000 将从 GG 上等概率随机选择一个起始节点,开始走 2k2k 步,他想让你求出走完 2k2k 步后字符串 ss 的前 kk 位和后 kk 位相等的概率.答案乘上 nn 后对 998,244,35399\textcolor{#fec52b}8,\textcolor{purple}{24}4,353 取模.

注意起始节点的权值不等同于字符串 ss 的第一位.

输入格式#

输入共 33 行.

第一行,三个正整数 n,k,pn,k,p,分别表示 GG 的点数、行动的步数和往前进的概率(pp 原本是一个介于 0011 之间的有理数,现给出其对 998,244,35399\textcolor{#fec52b}8,\textcolor{purple}{24}4,353 取模后的结果).

第二行,由小写字母组成的长度为 nn 的字符串,第 ii 个字母 cic_i 表示 ii 号点的权值.

第三行,nn 个正整数 vv,第 ii 个正整数 viv_i 表示 ii 号点出边指向的点.

输出格式#

输出共 11 行,一个正整数表示走完 2k2k 步后字符串 ss 的前 kk 位和后 kk 位相等的概率乘 nn 的结果,对 998,244,35399\textcolor{#fec52b}8,\textcolor{purple}{24}4,353 取模.

输入输出样例 #1#

输入 #1#

3 1 499122177
aba
2 3 1

输出 #1#

1

输入输出样例 #2#

输入 #2#

10 10 882315098
nqqmlmnnon
6 3 4 1 4 4 9 4 10 7

输出 #2#

106458929

说明/提示#

样例 11 解释#

12\frac12 在模 998244353998244353 下是 499122177499122177,故原始的 p=12p=\frac12

有如下路径:

  • 1231\to 2\to 3,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 ba,不符合条件.
  • 1211\to 2\to 1,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 ba,不符合条件.
  • 2312\to 3\to 1,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 aa,符合条件.
  • 2322\to 3\to 2,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 ab,不符合条件.
  • 3123\to 1\to 2,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 ab,不符合条件.
  • 3133\to 1\to 3,概率为 1n×p=16\frac1n\times p=\frac16,字符串为 aa,符合条件.

故有 16+16=13\frac16+\frac16=\frac13 的概率符合条件,乘 nn 的结果为 11,故输出 11

数据规模#

对于 100%100\% 的数据,满足 1n30,1k301 \le n \le 30,1 \le k \le 30

子任务编号nnkk特殊性质分值
119\le 99\le 955
22无特殊限制^^1010
33^17\le 17^3030
44^无特殊限制A55
55^^B55
66^^C1010
77^^3535

特殊性质 A:所有 cic_i 相同.

特殊性质 B:对于任意 1in1\le i\le n,有 vi=iv_i=i

特殊性质 C:对于任意 1i<n1\le i<n,有 vi=i+1v_i=i+1,且 vn=1v_n=1

思路#

由于每一个节点出度相同,考虑枚举起点.如果枚举了起点,一直往前走的整条路径就固定了,可以将其预处理出来.将图上的问题转化为链上的问题.

接下来,问题转化为:有一个人在一条链上跳,可以向前跳,也可以向后跳,但是不能跳出界.问如何跳才能满足得到的字符串前 kk 个和后 kk 个相等.

由于想要前后两半相等,可以想象有两个人,一个人跳前半段,一个人跳后半段,枚举权值与第一个点 fifi 相等的点 sese,两人从 fifisese 开始跳,每跳一步都保证落点权值相等,且最终第一个人的最终落点和第二个人的起点相邻的可能.

为了计算可能性,我们定义 dp[i][x][y] 表示两个人都走了 i 步,第一个人走到 x,第二个人走到 y 这种情况的可能性.

存在如下初始状态:

dp[1][fi][se] = 1;

存在如下转移方程:

int q = (1 - p + MOD) % MOD; // 后退的概率
if (x - 1 >= 0) {
int p1 = (x - 1 == 0) ? 1 : p; // 玩家 1 前进的概率
if (y - 1 >= 0) {
int p2 = (y - 1 == 0) ? 1 : p; // 玩家 2 前进的概率
// 都前进
dp[i][x][y] =
(dp[i][x][y] + 1ll * dp[i - 1][x - 1][y - 1] *
(1ll * p1 * p2 % MOD)) %
MOD;
}
if (y + 1 <= 2 * k)
// 玩家 1 前进,玩家 2 后退
dp[i][x][y] =
(dp[i][x][y] + 1ll * dp[i - 1][x - 1][y + 1] *
(1ll * p1 * q % MOD)) %
MOD;
}
if (x + 1 <= 2 * k) {
if (y - 1 >= 0) {
// 玩家 1 后退,玩家 2 前进
int p2 = (y - 1 == 0) ? 1 : p; // 玩家 2 前进的概率
dp[i][x][y] =
(dp[i][x][y] + 1ll * dp[i - 1][x + 1][y - 1] *
(1ll * q * p2 % MOD)) %
MOD;
}
if (y + 1 <= 2 * k)
// 都后退
dp[i][x][y] =
(dp[i][x][y] + 1ll * dp[i - 1][x + 1][y + 1] *
(1ll * q * q % MOD)) %
MOD;
}

统计结果时,只统计第一人终点和第二人起点相邻的情况:

for (int x = 0; x <= 2 * k; x++) { // 枚举第一人终点
if (x != se - 1 && x != se + 1)
continue; // 距离恰好等于 1
int bp = (se == x + 1) ? ((x == 0) ? 1 : p) : q; // 连接步(k->k+1)走对的概率
long long s = 0;
for (int y = 0; y <= 2 * k; y++) // 枚举第二人终点
s += dp[k][x][y];
ans = (ans + (s % MOD) * bp) % MOD;
}

显然,通过观察,我们不难发现,dp[i][..][..] 的值只与 dp[i-1][..][..] 有关,因此可以使用滚动数组优化.

标程

#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
const int N = 70;
int n, k, p;
char c[N];
int nxt[N];
int dp[2][N][N]; // 使用滚动数组
vector<int> pane;
int ans;
void start(int fi, int se) {
memset(dp, 0, sizeof(dp));
dp[1][fi][se] = 1;
int q = (1 - p + MOD) % MOD;
for (int i = 2; i <= k; i++) {
int cur = i & 1, pre = (i - 1) & 1;
// cur => i; pre => i - 1
for (int x = 0; x <= 2 * k; x++) {
for (int y = 0; y <= 2 * k; y++) {
dp[cur][x][y] = 0;
if (c[pane[x]] != c[pane[y]])
continue;
if (x - 1 >= 0) {
int p1 = (x - 1 == 0) ? 1 : p;
if (y - 1 >= 0) {
int p2 = (y - 1 == 0) ? 1 : p;
dp[cur][x][y] =
(dp[cur][x][y] + 1ll * dp[pre][x - 1][y - 1] *
(1ll * p1 * p2 % MOD)) %
MOD;
}
if (y + 1 <= 2 * k)
dp[cur][x][y] =
(dp[cur][x][y] + 1ll * dp[pre][x - 1][y + 1] *
(1ll * p1 * q % MOD)) %
MOD;
}
if (x + 1 <= 2 * k) {
if (y - 1 >= 0) {
int p2 = (y - 1 == 0) ? 1 : p;
dp[cur][x][y] =
(dp[cur][x][y] + 1ll * dp[pre][x + 1][y - 1] *
(1ll * q * p2 % MOD)) %
MOD;
}
if (y + 1 <= 2 * k)
dp[cur][x][y] =
(dp[cur][x][y] + 1ll * dp[pre][x + 1][y + 1] *
(1ll * q * q % MOD)) %
MOD;
}
}
}
}
int bk = k & 1;
for (int x = 0; x <= 2 * k; x++) {
if (x != se - 1 && x != se + 1)
continue; // 距离恰好等于 1
int bp = (se == x + 1) ? ((x == 0) ? 1 : p) : q;
long long s = 0;
for (int y = 0; y <= 2 * k; y++)
s += dp[bk][x][y];
ans = (ans + (s % MOD) * bp) % MOD;
}
}
void solve(int st) {
pane.clear();
for (int i = 1; i <= 2 * k + 1; i++) {
pane.push_back(st);
st = nxt[st];
}
for (int i = 0; i <= 2 * k; i++) {
if (c[pane[i]] == c[pane[1]]) {
start(1, i);
}
}
}
signed main() {
cin >> n >> k >> p;
for (int i = 1; i <= n; i++)
cin >> c[i];
for (int i = 1; i <= n; i++)
cin >> nxt[i];
for (int i = 1; i <= n; i++)
solve(i);
cout << (ans + MOD) % MOD;
}

实际上,对于一个对象进行两组重复的动作时,这也可以等同于两个对象分别做一组相同的动作.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

安吉D1-L
https://blog.jerrylab.top/posts/problem/anji2026/D1/L/
作者
Jerry
发布于
2026-08-01
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Jerry
Hello, I'm Jerry.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
80
分类
3
标签
35
总字数
89,198
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0