2025“钉耙编程”中国大学生算法设计暑期联赛(3)(2025杭电多校 3)——1009线段染色
1009线段染色
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被染)。
-
情况分类:
- 点3被染(概率 0.5):此时两条线段都已满足条件。
- 点3未被染(概率 0.5):此时必须点2被染(概率 0.5)且点4被染(概率 0.5),概率为 0.5×0.5×0.5=0.125。
-
总概率 =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) 就是一个滑动窗口求和,双指针维护即可。
整个算法:
- 对每个线段 [l,r],标记
bestL[r] = max(bestL[r], l)。 - 扫一遍算出 F[i]=max(F[i−1], bestL[i−1])。
- 滑动窗口跑 DP。
- 答案 = ∑i≥maxLdpi⋅∏k=i+1nqk。
时间复杂度 O(n+m),不需要任何复杂数据结构。
唯一需要特判的是 qk=0(即 ak=10,点 k 必然被染色)的情况——此时前缀积为零,不能除。解决方式是在这些点处"重置"前缀积的基准。
什么时候"更强数据结构"真的能降低思维?
你说的这种思想确实有适用的场景,比如:
| 场景 | “聪明做法”(思维重) | “数据结构做法”(思维轻) |
|---|---|---|
| 斜率优化 DP | 手维护凸包 + 单调性分析 | 李超线段树直接维护 |
| 树上路径问题 | 点分治 / 各种分讨 | LCT / 树链剖分 |
| 区间历史操作 | 复杂的势能分析 | 吉司机线段树(Segment Tree Beats) |
| 带修 DP | 重新推导转移 | 线段树维护转移矩阵(动态 DP) |
| 但这些场景的共同点是:瓶颈在"优化/维护"而非"建模"。如果一道题的瓶颈在于"如何定义状态和转移",那数据结构是无能为力的。 | ||
| 本题恰好属于后者。 |
如果这道题加了修改操作……
如果题目变成"每次修改一个 ai,查询当前的概率",那就可以用动态 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
现在的问题是怎么快速算这个求和。你有两条路:
- 路线 A(数据结构):观察一下,当 i 递增到 i+1 时,已有的每个 dpj⋅∏qk 项会发生什么变化?只是被乘上了一个 qi。所以你需要一个支持区间乘、单点修改、区间求和的数据结构。
- 路线 B(代数化简):试试令 f(j)=dpj/∏k=1jqk,代入转移式看看会发生什么。结合 F(i) 的单调性,你会发现一个非常简洁的结构。
提示五(特殊情况)
如果某个点 k 满足 ak=10(即 qk=0,必然被染色),你的"除以前缀积"会出问题。想想这种点在物理意义上意味着什么,以及它如何把问题切成独立的段。
这 AI 提示二搁这乱写的,这数轴上的每一个点都是染色点,哪来的不染色点啊?那属于搞笑来了,卧槽。
你先别急,我觉得这里可能有个术语上的误解,我重新把关键点说清楚。
题目说的是:“整点 i 被染色的成功率为 pi”。也就是说,每个点进行染色操作后,有两种结果:
- 染色成功(概率 pi)
- 染色失败(概率 qi=1−pi)
比如样例六,a=[1,1,1,0],第四个点 a4=0,意味着 p4=0,第四个点必定染色失败。而线段 [4,4] 要求第四个点染色成功,所以答案为 0。
所以 n 个点中,并非每个点都会染色成功。最终染色成功的点是一个随机子集。我之前说的"染色点"指的就是"染色成功的点",表述上确实不够清晰,这里道歉。
重新用明确的语言给提示:
提示一
考虑最终的随机结果:n 个点中,有些染色成功,有些染色失败。把所有染色成功的点从左到右记为 c1<c2<⋯<ct。那么"所有线段都被满足"等价于:每条线段 [ls,rs] 内至少包含一个染色成功的点。
反过来想:如果两个相邻的染色成功点 ck 和 ck+1 之间有一段空白 (ck,ck+1),这段空白里的点全部染色失败。如果某条线段完整地落在这个空白里,那它就没有任何染色成功的点——这条线段就不满足条件。
提示二
基于提示一,可以设计 DP:设 dpi 表示"位置 i 染色成功,且所有右端点 ≤i 的线段都被满足"的概率贡献。转移时枚举上一个染色成功的位置 j,中间 j+1,…,i−1 全部染色失败。
提示三
转移的合法条件:(j,i) 这段空白里不能完整包含任何线段。定义:
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]:
- 你的代码算 dp1=1/2,dp2=1/2×dp1=1/4
- 正确的是 dp2=p2×(dp0×q1+dp1×1)=1/2×(1/2+1/2)=1/2
差别就在于 dp0×q1 这项:从虚拟起点 0 到位置 2,中间位置 1 必须染色失败,要乘 q1。
问题二:没有虚拟起点 dp0=1
位置 i 可以是第一个染色成功的点,前面全部失败。这对应从一个虚拟的位置 0 转移过来,dp0=1。你的二分从l=1开始,完全没考虑 j=0 这种情况。
问题三:答案不是 dpN
最右边的染色成功点不一定是 N。比如 N=4,可能点 3 染色成功、点 4 染色失败,只要所有线段都被覆盖了就行。正确答案是:
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 代码
AC
https://acm.hdu.edu.cn/contest/view-code?cid=1174&rid=28065
https://vjudge.net/solution/67894251
1 | /** |
2025“钉耙编程”中国大学生算法设计暑期联赛(3)
1008—01环
赛时最后靠AI找到了一个hack数据
1 | hack: |
就是这个其他时候,交换都是单向的,只有一个选择,但是第一个,交换有两个选择,就都试一下。
1 | ll cal(string &S,char ch){ |
1 | // Problem: 01环 |
1002小抹爱锻炼
https://acm.hdu.edu.cn/contest/view-code?cid=1174&rid=817
赛时一发就过了。队友的思路,我的代码。
1 | // Problem: 小抹爱锻炼 |
1012核心共振
切比雪夫距离与曼哈顿距离转化+前缀和+队友的公式。
因为有个地方溢出了,WA了一发,改了那个地方就A了。
1 | // Problem: 核心共振 |
1007性质不同的数字
其实还好,没有想象中那么神秘,那么这道题目其实是在问不同的区间覆盖情况有几种,那么实际上我们就差分(异或差分)+离散化就行了。
注意这里不能简单+1(下方的代码是对的)(简单的+1就是查lr[i].SE的idx,然后加的地方直接就是idx+1,这个是不行的,我们不能默认这两个区间是相交的)
1 | mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count()); |
1 | // Problem: 性质不同的数字 |
1004三带一
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: 三带一 |
1009线段染色
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 |
模版1
2025牛客多校训练4(死胡同,交换变选数贪心)
Problem F. For the treasury!
把这个问题看成选K个数字,然后把这K个数字全部移到第一个位置,然后把多减的代价加回来。
1 | // Problem: For the Treasury! |
Problem B. Blind Alley(看到这种砍掉一个方向,要想到dp)
赛时提交有问题是因为并没有考虑这个动态的过程,或许这种题目还是要首先搞清楚这个模拟,把这个模拟的本质,模拟的策略给搞清楚。模拟的策略就是说,至少目前为止,我走到这个点,一定是可以到达
直接模拟这个玩家,其实通过题意理解,只要这个玩家所走到的点最终能够走到视野的尽头,那么这个玩家就认为他能够走到走到终点。
那么前面有一个反向bfs,得到所有我们能到达终点的点。(赛时解法中已经想到了)。
那么这个far数组,或者说dp数组,可以通过比较朴素的前缀dp得到。
1 | vector<vector<ll> > dp(N+5,vll(M+5)); |
然后模拟这个玩家。
1 | while(!q.empty()){ |
https://ac.nowcoder.com/acm/contest/view-submission?submissionId=78459227
1 | // Problem: Blind Alley |
Problem I. I, Box
那么我个人认为难点在于这个题目需要输出推动序列,其实还是有点难的,难度在于这个模拟,以及代码实现上。
1 |
Problem G. Ghost in the Parentheses
首先,将( 转为1,)转为-1,始终有合法匹配(序列是合法的)就是前缀和始终为正。
通过该条件,我们能找出所有的不可交换位置(指的是你不能拿 )和这个$( $ 换 )。
通过如下程序,可以计算出来这个 )所能选的右括号有rem=N/2−fix 个,但是这样之间计算概率只能通过第一个样例,那怎么办那?遇事不决,就上dp(当然我也不会,和grok学的,这个dp)
1 | ll fix=0; |
1 |