思路讲解
2025“钉耙编程”中国大学生算法设计暑期联赛(3)(2025杭电多校 3)——1009线段染色
和这道题目有点相似,难点都在于这个容斥上面,那么解决都很简单,就是上dp。
AC代码
1 |
2025“钉耙编程”中国大学生算法设计暑期联赛(3)(2025杭电多校 3)——1009线段染色
和这道题目有点相似,难点都在于这个容斥上面,那么解决都很简单,就是上dp。
1 |
https://acm.hdu.edu.cn/contest/problem?cid=1174&pid=1009
https://vjudge.net/problem/HDU-8011
又是沟槽的概率题目。
感觉不是我能碰瓷的题目,之后有兴趣了再来看吧。
https://grok.com/chat/dc6a06d2-50d6-4bb8-a88b-87a1c553307d
1 | #include <bits/stdc++.h> |
题意简述
在一个长度为 n 的数轴上(包含整点 1∼n),给出了 m 条线段,第 i 条线段覆盖区间 [li,ri]。
针对数轴上的每个整点 i,进行一次独立的染色操作,染色的成功率为 pi=10ai。
定义一条线段被染色,当且仅当该线段覆盖的整点中至少有一个被染色。
求所有给定的 m 条线段都被染色的概率。
输入格式
第一行包含一个整数 t,表示测试数据组数。
对于每组数据:
第一行两个整数 n,m,分别表示数轴长度和线段数量。
第二行 n 个整数 a1,a2,…,an,表示每个点的染色概率参数(pi=ai/10)。
接下来 m 行,每行两个整数 li,ri,表示一条线段的左右端点。
输出格式
输出一个整数,表示概率对 109+7 取模后的结果。
数据范围
1≤t≤5×104
1≤n≤2×105,0≤m≤2×105
∑n,∑m≤2.5×106
0≤ai≤10
样例
1 | 6 |
样例输出
1 | 625000005 |
样例解释
第一组数据:
n=4,所有点染色概率均为 0.5。
线段为 [2,3] 和 [3,4]。
要使两条线段都被染色,需满足:(点2被染 或 点3被染) 且 (点3被染 或 点4被染)。
情况分类:
总概率 =0.5+0.125=0.625=85。
85(mod109+7)=625000005。
第二组数据:
线段为 [2,2] 和 [3,4]。
线段1仅包含点2,必须点2被染(概率 0.5)。
线段2包含点3, 4,需至少一个被染,概率为 1−(1−0.5)×(1−0.5)=0.75。
两者独立,总概率 =0.5×0.75=0.375=83。
我们重新回来看这道题目啊,重新回来看一下这道题目。
这是一个很好的问题,我来认真分析一下。
原因是:这道题的思维瓶颈在 DP 的建模,而不在 DP 的优化。数据结构能帮你优化已有的转移(比如把 O(n2) 变成 O(nlogn)),但不能替代"发现 DP 怎么定义"这一步。
这道题的核心洞察只有一个:
枚举"上一个被染色的位置 j",那么 (j,i) 之间不能完整包含任何线段。
一旦有了这个洞察,就能定义:
dpi=pi⋅j=F(i)∑i−1dpj⋅k=j+1∏i−1qk
其中 F(i)=max{ls:rs<i},是所有右端点在 i 左边的线段中,最大的左端点。
关键:F(i) 天然单调不减(随着 i 增大,参与取 max 的线段只多不少),所以转移的求和下界单调右移——这是一个滑动窗口。
数据结构(线段树/BIT)的作用是:如果你没注意到"前缀积可以提出来、然后滑动窗口"这件事,你可以用线段树做"区间乘 qi + 单点插入 + 区间求和"来暴力维护转移,省去这一步推导。
但"枚举上一个染色位置"这一步,没有任何数据结构能替你想。这就像斜率优化 DP 里——你可以用李超线段树替代手动维护凸包(降低了"维护凸包"的思维),但"发现 DP 具有斜率优化结构"这一步,李超树帮不了你。
这道题甚至不需要线段树。令 f(j)=dpj/∏k=1jqk,代入得:
dpi=pi⋅k=1∏i−1qk⋅j=F(i)∑i−1f(j)
F(i) 单调不减,所以 ∑f(j) 就是一个滑动窗口求和,双指针维护即可。
整个算法:
bestL[r] = max(bestL[r], l)。你说的这种思想确实有适用的场景,比如:
| 场景 | “聪明做法”(思维重) | “数据结构做法”(思维轻) |
|---|---|---|
| 斜率优化 DP | 手维护凸包 + 单调性分析 | 李超线段树直接维护 |
| 树上路径问题 | 点分治 / 各种分讨 | LCT / 树链剖分 |
| 区间历史操作 | 复杂的势能分析 | 吉司机线段树(Segment Tree Beats) |
| 带修 DP | 重新推导转移 | 线段树维护转移矩阵(动态 DP) |
| 但这些场景的共同点是:瓶颈在"优化/维护"而非"建模"。如果一道题的瓶颈在于"如何定义状态和转移",那数据结构是无能为力的。 | ||
| 本题恰好属于后者。 |
总结:你的直觉(用强数据结构换思维难度)是对的策略,但这道题的瓶颈不在数据结构能解决的层面。好消息是,一旦你掌握了"枚举上一个染色位置 + F(i) 单调 → 滑动窗口"这个 pattern,以后类似的一维覆盖概率题都可以秒。这是经验/模板的力量,比数据结构更靠谱。
好,我给你几层递进的提示,你自己想到哪一层算哪一层。
提示一(最基本的观察)
如果你把所有被染色的点从左到右排成一列 c1<c2<⋯<ct,那么"所有线段都被满足"这个条件,本质上是在约束相邻染色点之间的空隙。想想看:两个相邻染色点 ck 和 ck+1 之间的"空白区域"(ck,ck+1) 不能怎么样?
提示二(DP 状态)
如果你意识到了"空隙不能完整包含任何线段",那么自然可以设计 DP:从左到右枚举"下一个被染色的点 i",上一个被染色的点是 j。转移的合法性只取决于 j 的值——j 必须 ≥ 某个下界。这个下界是什么?它跟哪些线段有关?
提示三(下界的单调性)
定义 F(i) 为"所有右端点严格小于 i 的线段中,左端点的最大值"。想一想:为什么 F(i) 关于 i 是单调不减的?这个单调性意味着什么?
提示四(优化方向)
如果你已经写出了转移式:
dpi=pi⋅j=F(i)∑i−1dpj⋅k=j+1∏i−1qk
现在的问题是怎么快速算这个求和。你有两条路:
这 AI 提示二搁这乱写的,这数轴上的每一个点都是染色点,哪来的不染色点啊?那属于搞笑来了,卧槽。
你先别急,我觉得这里可能有个术语上的误解,我重新把关键点说清楚。
题目说的是:“整点 i 被染色的成功率为 pi”。也就是说,每个点进行染色操作后,有两种结果:
F(i)=max{ls:rs<i}
即"右端点在 i 左边的线段中,左端点最大的那个"。那么 j 必须满足 j≥F(i)。
F(i) 是单调不减的——想想为什么。
提示四
写出转移式后,观察求和的结构。F(i) 单调不减意味着求和的左界在右移,这是一个滑动窗口。结合前缀积的技巧,可以做到 O(n+m)。或者用线段树维护"区间乘 + 区间求和"做到 O(nlogn),不需要想前缀积除法的细节。
OK,我大概看懂了这个 AI 提示3了。这 AI 提示2属于是哈哈哈省略的有些过多了。啊,如果你要看 AI 提示的话,可以直接看这个 AI 提示3。嗯。

应该是如上图这个意思啊,我们先尝试一下能不能实现一下。因为这个转移合法性是具有单调性的。就是说,如果说这个 LR 不符合要求的话,那么更小的 L 是更加不可能符合要求的。那么这个还是非常容易就可以找到的。
啊,然后你按照这个思路写了一个 DP 程序以后,现在问题就是答案到底是什么?我们会发现答案不是 DP[N] 啊,答案不是 DP[N] 那怎么答案从何而来?答案是什么?现在的问题是这个。
你的代码有三个问题,核心都是同一件事的不同表现。
我们最大的问题是少考虑了虚拟起点0。说白了,它可以从 0 转移过来。还有一个地方就是它转移过来的中间那一段,它不能够被染色。
问题一:转移少乘了一坨东西
你写的转移是:
1 | dp[i] = p_i × Σ dp[j] (j 从 F(i) 到 i-1) |
但正确的转移是:
dpi=pi×j=F(i)∑i−1(dpj×k=j+1∏i−1qk)
那个 ∏qk 是什么意思?dpj 说的是"j 是上一个染色成功的点",那 j+1,j+2,…,i−1 这些点就必须全部染色失败,概率是 ∏k=j+1i−1qk。你把这个因子整个漏了。(我给 AI 的这个式子加了个括号,避免你理解错了。)
拿样例一来说,n=4,全部 p=q=1/2,线段 [2,3] 和 [3,4]:
l=1 开始,完全没考虑 j=0 这种情况。ans=i≥maxL∑dpi×k=i+1∏Nqk
这三个问题本质是一回事:你的 DP 把"中间点必须染色失败"这个概率因子完全忽略了。一旦补上这个因子,BIT 里就不能直接存 dpj 了(因为 ∏qk 依赖于查询时的 i),所以要么改存 f(j)=dpj/前缀积,要么换成带懒标记的线段树做区间乘。这就是为什么改动面积不可避免地大。
我们最大的问题是少考虑了虚拟起点0。说白了,它可以从 0 转移过来。还有一个地方就是它转移过来的中间那一段,它不能够被染色。因此需要乘上一个不被染色因数。说白了就是计算一下中间那一段它的这个乘积嘛。
dpi=pi×j=F(i)∑i−1(dpj×k=j+1∏i−1qk)
后面的这一坨乘积其实不好求啊,不是很好求,因为对于每一个 DP【 j 】来说,它的这个乘积的项数是不一样的。哎,不过我们可以采用类似于维护区间和区间修改的树状数组一样。这个可能说的有点抽象。
我们使用 iPad 画图来说明一下我们的这个意思。

说白了就是在线段树中的 j 存储不同的逆元值,存储的 DP j 乘以不同的逆元值,进而得到乘以相同的值,达到乘以不同值的这个效果。觉得我这个图还是画的比较清楚的。

1 | vector<ll> pre_mul(N+2,1); |
AC
https://acm.hdu.edu.cn/contest/view-code?cid=1174&rid=28065
https://vjudge.net/solution/67894251
1 | /** |
赛时最后靠AI找到了一个hack数据
1 | hack: |
就是这个其他时候,交换都是单向的,只有一个选择,但是第一个,交换有两个选择,就都试一下。
1 | ll cal(string &S,char ch){ |
1 | // Problem: 01环 |
https://acm.hdu.edu.cn/contest/view-code?cid=1174&rid=817
赛时一发就过了。队友的思路,我的代码。
1 | // Problem: 小抹爱锻炼 |
切比雪夫距离与曼哈顿距离转化+前缀和+队友的公式。
因为有个地方溢出了,WA了一发,改了那个地方就A了。
1 | // Problem: 核心共振 |
其实还好,没有想象中那么神秘,那么这道题目其实是在问不同的区间覆盖情况有几种,那么实际上我们就差分(异或差分)+离散化就行了。
注意这里不能简单+1(下方的代码是对的)(简单的+1就是查lr[i].SE的idx,然后加的地方直接就是idx+1,这个是不行的,我们不能默认这两个区间是相交的)
1 | mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count()); |
1 | // Problem: 性质不同的数字 |
sum为牌数总和,ans为答案,rem为剩余牌数。
rem=sum−3×ans 那么剩余的牌的数量一定大于 rem≥ans
ti≤rem−(ai−3ti) ⇒ ai−rem≤2ti
那么第一个式子没什么难的,剩余的这个rem‘B’肯定要比你的三元组多呀。然后移项一下,这个式子摇身一变,变为了ti的下界了,这个好像也挺简单的~~(就是赛时想不到)~~。
然后向上取整除法的写法需要特判<0的情况,因为众所周知,C++的/是向0取整的,
1 | inline ll chu(ll x){ |
check函数的写法,这个low怎么说呢,也没啥物理意义,就是纯数学推出来的式子,你就说是不是下界吧()。
1 | auto check=[&](ll ans)->bool{ |
https://acm.hdu.edu.cn/contest/view-code?cid=1174&rid=22887
1 | // Problem: 三带一 |
https://acm.hdu.edu.cn/contest/problem?cid=1174&pid=1009
又是沟槽的概率题目。
感觉不是我能碰瓷的题目,之后有兴趣了再来看吧。
https://grok.com/chat/dc6a06d2-50d6-4bb8-a88b-87a1c553307d
1 | #include <bits/stdc++.h> |
1 |