CF1286D LCC 题解
一道神仙题,很考验各位对于期望 DP 的掌握程度与数据结构优化 DP 的熟练度。
本题好像有线段树套矩阵优化 DP 做法?不管了不管了,反正我没写
实际上一开始想的时候我迅速发现这道题与以前做过的一道题目很相似,感觉可以线段树套矩阵做,但是想了一个晚上没想出解法。
后来看题解,发现确实有这样的做法,但是 @OMG_wc 给出了一种更加简洁的不用矩阵的做法,我就学习了这种做法。
首先一个事实是第一次发生碰撞的粒子一定是相邻的粒子,因为如果两个不相邻的粒子撞到了,那么中间的粒子一定会与其中的某一个粒子撞到或者是同时撞到。
因此我们可以考虑先预处理出所有可能会碰撞的粒子对以及碰撞的两个粒子的方向、距离、速度,至多有 对(至多是因为会有速度相同的粒子),按时间排序。
这里用 0 表示向左,1 表示向右。
下面设 表示 能否分别往 方向走。
之前我们已经预处理出了所有能够发生碰撞的粒子对,现在按照时间排序,对于一对粒子对,如果我们需要这对粒子对最早发生碰撞,那么在这对粒子对前面的粒子对都不能发生碰撞,也就是将相应的运动在 里面禁掉。
开头说了,这是道期望 DP 题,考虑设计状态转移方程:
设 表示当前做到第 个粒子,并且第 个粒子向左/右移动时的概率,那么可以推出转移方程:
那么对于每一个被选中碰撞的粒子,最后算完概率之后再算期望就好了。
但是呢很遗憾,我们每更换一对粒子对,都需要重新做一遍 DP,复杂度是 的,那么要怎么优化呢?
这里采用线段树优化。
对于线段树节点 ,设其维护 ,那么我们需要维护一个 ,表示其左端点与右端点走的方向为 时在当前时间前所有粒子都不碰撞的概率。
当我们在更换一对粒子对的时候,我们需要对这对粒子对做出限制:这对粒子对所走的方向不能走。
因此我们在 上面打上标记之后,自底向上更新线段树的值,具体而言就是找到第一个以当前粒子对为分界线的节点,向上维护。
那么此时根节点就维护了所有粒子对均不碰撞的概率。
这里做一个容斥:第 对粒子碰撞的概率 = 前 对粒子均不碰撞的概率 - 前 对粒子均不碰撞的概率。
这样,我们就知道第 对粒子碰撞的概率了,然后算期望就好。
几个需要注意的细节代码里面已经有注释了。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:CF1286D LCC
Date:2021/6/15
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 1e5 + 10;
const LL P = 998244353;
int n, cnta;
LL x[MAXN], v[MAXN], pro[MAXN][2], ans, ys[MAXN], inv;
bool book[MAXN][2][2];
struct node
{
int u, d1, d2;
LL x, v;
}a[MAXN << 1];
struct node2
{
int l, r;
LL p[2][2];
}tree[MAXN << 2];
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
LL ksm(LL aa, LL bb, LL P)
{
LL ans = 1 % P;
for (; bb; bb >>= 1, aa = aa * aa % P)
if (bb & 1) ans = ans * aa % P;
return ans;
}
bool cmp(const node &fir, const node &sec) { return (fir.x * sec.v) < (sec.x * fir.v); }
//这里不推荐直接使用 double 除,可能会失精度。
void Update(int p)
{
int mid = (tree[p].l + tree[p].r) >> 1;
for (int firx = 0; firx < 2; ++firx)
for (int firy = 0; firy < 2; ++firy)
{
tree[p].p[firx][firy] = 0;
for (int secx = 0; secx < 2; ++secx)
for (int secy = 0; secy < 2; ++secy)
if (!book[mid][secx][secy]) ((tree[p].p[firx][firy] += tree[p << 1].p[firx][secx] * tree[p << 1 | 1].p[secy][firy] % P) >= P) ? (tree[p].p[firx][firy] %= P) : 0;
}
}
void build(int p, int l, int r)
{
tree[p].l = l, tree[p].r = r;
if (l == r) { tree[p].p[0][0] = pro[l][0]; tree[p].p[1][1] = pro[l][1]; return ; }
int mid = (l + r) >> 1; ys[mid] = p;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
Update(p);
}
int main()
{
n = Read(); inv = ksm(100, P - 2, P);//注意这里的 inv 一定要开 long long!
for (int i = 1; i <= n; ++i)
{
x[i] = Read(), v[i] = Read();
pro[i][1] = Read() * inv % P;
pro[i][0] = (1 - pro[i][1] + P) % P;
//小心,题目中是输入向右边走的概率
}
for (int i = 1; i < n; ++i)
{
++cnta; a[cnta] = (node){i, 1, 0, x[i + 1] - x[i], v[i + 1] + v[i]};
if (v[i] > v[i + 1]) { ++cnta; a[cnta] = (node){i, 1, 1, x[i + 1] - x[i], v[i] - v[i + 1]}; }
else if (v[i] < v[i + 1]) { ++cnta; a[cnta] = (node){i, 0, 0, x[i + 1] - x[i], v[i + 1] - v[i]}; }
}
std::sort(a + 1, a + cnta + 1, cmp); build(1, 1, n);
LL Last = 1;
for (int i = 1; i <= cnta; ++i)
{
book[a[i].u][a[i].d1][a[i].d2] = 1;
for (int j = ys[a[i].u]; j; j >>= 1) Update(j);
LL now = (tree[1].p[0][0] + tree[1].p[1][0] + tree[1].p[0][1] + tree[1].p[1][1]) % P;
((ans += a[i].x * ksm(a[i].v, P - 2, P) % P * (Last - now + P) % P) >= P) ? (ans %= P) : 0;
//第 i 次碰撞的概率 = 前 i - 1 次不碰撞的概率 - 前 i 次不碰撞的概率
//注意随时取模,可能你 WA 了就是在某个你不注意的地方没有取模
Last = now;
}
printf("%lld\n", ans); return 0;
}
Summary:
好题!
本题考验到了以下算法:
- 朴素 DP 方程的建立(更加具体的,期望 DP)
- 线段树优化 DP,这其中需要维护一些特别的很妙的东西。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具