网教10. 琪露诺的完美算数教室——⑨的统计I

这篇博客介绍了琪露诺在幻想乡遇到的一个与数字⑨相关的数学难题。问题要求计算在给定范围a到b内(包含a和b),有多少数字包含数字9。通过分析,博主提出了解决方案,即从前往后计算,遇到9后直接加上9之后的数,并使用多个int数组存储大数。大数操作时需要注意取模和进位。

众所周知,琪露诺(チルノ,Cirno)是幻想郷 (げんそうきょう)中首屈一指的天才,可以说⑨就是她的代名词。

然而如今,她遇到了一个和⑨有关的难题。你能帮助她么?

题目是这样的,给出两个数 a 和 b (0 <= a <= b <= 10^10000),求 a 到 b 之间(包括a和b)的数字中,有多少个数字是包含9的(例如 19,910 等都是包含9的数字)。

输入

第一行为一个数字 T (0 < T <= 100) 表示数据组数。
之后的 T 行,每行包含两个数 a 和 b (0 <= a <= b <= 10^10000)。

输出

对每组数据输入,输出一个数字,表示 a 到 b 之间的数字中(包括a和b),有多少个数字是包含9的。(注意:答案可能很大)



测试输入关于“测试输入”的帮助 期待的输出关于“期待的输出”的帮助 时间限制
<think>我们注意到用户提供的代码片段似乎是C++代码,使用了优先队列(最大堆)来存储pair。但是,之前的解答中我们讨论了两种优化方法:优先队列和单调队列。用户询问了“优先队列怎么优化”,并在最后给出了一个代码片段,但该片段不完整。 我们需要解释如何用优先队列优化动态规划,并注意代码片段中的问题。 在优先队列方法中: - 我们维护一个最大堆(通常用priority_queue),存储(dp[j], j)。 - 堆顶元素是当前窗口内dp值最大的元素(但要注意下标是否过期)。 用户提供的代码片段: int f1=q.top()->first; int s1=q.top()->second; 这里似乎是将堆中的元素看作指针(或是指向pair的指针),但通常我们存储的是pair本身。在C++中,priority_queue默认是大顶堆,但需要定义比较方式。 由于题目要求用中文回答,并且最后必须提出3-5个相关问题,我们先解答问题,然后提出相关问题。 但是,用户的问题是如何用优先队列优化,而之前的解答已经包含了优先队列的优化思路,但代码示例是单调队列的。因此,我们需要补充优先队列的代码示例(伪代码或C++)? 然而,用户并没有指定编程语言,而且题目描述来自洛谷(P1725),通常参赛者使用C++。所以,我们给出C++的优先队列优化示例。 注意:优先队列方法需要注意移除过期元素。由于优先队列不支持随机删除,我们采用延迟删除:即当堆顶元素的下标不在当前窗口内,则弹出,直到找到一个在窗口内的元素。 算法步骤(C++): 1. 定义dp[0]=0,优先队列pq(最大堆)存储pair<int, int>,第一个元素为dp[j],第二个为下标j。 2. 将(0,0)压入pq。 3. 遍历i从1到N: // 注意:状态j的范围:[i-R, i-L] (因为我们用j更新i,所以i-L必须是已经计算过的,且j>=i-R) // 1. 加入新候选:当i>=L时,将(dp[i-L], i-L)加入堆。 // 2. 移除过期元素:弹出堆顶直到堆顶元素的下标>=i-R(注意窗口左边界是i-R) // 3. 如果堆非空,dp[i]=A[i] + 堆顶的dp值;否则,dp[i]=-INF(不可达)。 4. 答案:在区间[max(0, N+1-R), N]中找最大的dp值。 但是,注意:在i>=L时才能加入dp[i-L]?实际上,在计算dp[i]之前,我们需要窗口[i-R, i-L]中的最大值。因此,在计算dp[i]之前,应该先加入dp[i-L](因为i-L已经计算出来了),然后移除过期的元素(下标小于i-R的)。 然而,注意窗口是[i-R, i-L],所以当i-L<0时不能加入。但实际上i>=L时,i-L>=0。 因此,代码框架如下(C++): ```cpp #include <iostream> #include <queue> #include <vector> #include <climits> using namespace std; typedef pair<int, int> pii; // first: dp值, second: 下标 int main() { int N, L, R; cin >> N >> L >> R; vector<int> A(N + 1); for (int i = 0; i <= N; i++) { cin >> A[i]; } vector<int> dp(N + 1, INT_MIN); dp[0] = A[0]; // A[0] = 0 // 大顶堆,按dp值排序(注意:当dp值相同,下标不重要) priority_queue<pii> pq; pq.push({dp[0], 0}); for (int i = 1; i <= N; i++) { // 步骤1:加入新候选(当i-L>=0,因为下标不能为负) if (i >= L) { pq.push({dp[i - L], i - L}); } // 步骤2:移除过期元素(下标小于i-R) while (!pq.empty() && pq.top().second < i - R) { pq.pop(); } // 步骤3:更新dp[i] if (!pq.empty()) { dp[i] = A[i] + pq.top().first; } else { // 如果队列为空,意味着没有可达的点,dp[i]保持为负无穷(不可达) // 但题目保证起点0,所以一般不会为空,除非i-L还没有计算(但i>=L时已经加入了一个元素) // 注意:当i<L时,窗口[i-R, i-L]可能为空(因为i-R可能小于0,且i-L<0),所以堆可能为空 // 因此,当i<L时,我们不会加入新元素,且窗口要求j>=i-R(因为i-R可能为负,所以实际j>=0),但初始有0,所以当i-R<=0且0在窗口内(即i-L>=0?)实际上,当i<L时,我们不会加入新元素,但之前有0?注意:0在i>=R+1时就会过期。所以当i<L时,可能0已经过期(如果i-R>0),也可能还在窗口(如果i-R<=0)。 // 所以,我们这里用else处理为空的情况,即不可达。 // 根据题意,起点0,所以当i>=L时一定会有元素?但i<L时呢?实际上,当i<L时,窗口[i-R, i-L]要求j>=0且不超过i-L(负数)不存在,所以为空。因此,dp[i]不可达,保持负无穷。 } } // 找答案 int ans = INT_MIN; int start_index = max(0, N + 1 - R); // 从N+1-R开始,但注意N+1-R可能为负,所以用max(0) for (int i = start_index; i <= N; i++) { if (dp[i] > ans) { ans = dp[i]; } } cout << ans << endl; return 0; } ``` 但是,上面的代码有一个问题:在加入新候选dp[i-L]后,我们立即移除过期元素(即下标小于i-R的元素)。这样堆中可能同时存在多个过期元素,但我们每次只移除堆顶的过期元素(因为堆顶是dp值最大的元素,但过期元素不一定在堆顶)。所以,我们移除过期元素时,需要循环移除堆顶所有过期元素。 然而,上述代码中的移除操作是在加入新候选后进行的,并且循环移除堆顶过期元素直到堆顶元素不过期(或者堆为空)。这样每个元素只会被移除一次(因为下标递增,所以过期元素一定是堆中下标最小的?不一定,堆中元素的下标是无序的。所以,我们无法保证堆顶的过期元素移除后,下一个堆顶元素不会过期?实际上,我们每次只检查堆顶元素,如果过期就移除,然后检查新的堆顶,直到堆顶元素不过期。这样,堆中可能还有过期元素,但我们在下一次循环中会继续检查。但是,这会导致堆中积压过期元素,使得堆的大小变大,但每个元素在堆顶时都会被检查移除,所以不会影响正确性。不过,最坏情况下堆中可能有O(N)个元素,每次移除操作可能是O(logN),总时间复杂度O(N logN)。 但是,上述代码在i<L时,不会加入新元素,但会移除过期元素(比如初始的0,当i-R>0时就会移除)。所以,当i<R时,0还在窗口内(因为i-R<0,所以0>=i-R成立),所以0不会被移除。当i>=R+1时,0就会被移除。 然而,上述代码在i>=L时才会加入dp[i-L],但dp[i]依赖于窗口[i-R, i-L]的最大值。如果i<L,那么窗口[i-R, i-L]中可能包含0(当i-R<=0)?因为j>=0且j<=i-L(但i-L<0)所以没有j满足条件?因此,当i<L时,窗口为空,dp[i]保持负无穷,这是正确的。 但是,注意:当L=1时,i>=1(即i>=L)时,我们才会加入dp[i-1]。对于i=1,加入dp[0](即0),然后检查堆顶:0是否小于1-R?如果R>=1,那么1-R<=0,所以0>=1-R,不过期。所以dp[1]=A[1]+dp[0]?正确。 但是,当i=R+1时,我们加入dp[i-L]后,然后移除堆顶中所有下标小于(i-R)=1的元素。此时,0会被移除(因为0<1),所以堆中可能还有其他元素(比如dp[1], dp[2]等),这些下标>=1,所以不会被移除。 因此,代码逻辑正确。 但是,用户提供的代码片段: int f1=q.top()->first; int s1=q.top()->second; 可能是从指针结构取元素,这提醒我们:在C++中,priority_queue存储的是pair,因此应该用q.top().first和q.top().second。 另外,注意题目中冰冻指数可能为负数,所以初始化为负无穷(INT_MIN)是安全的。 然而,上述优先队列的代码在洛谷上可能只能通过60%的数据(因为N最大20万,O(N logN)可能勉强通过),而单调队列优化是O(N)的,所以更优。 现在,我们按照要求,提出3-5个相关问题。 §§相关问题§§ 1. 在优先队列优化中,为什么采用延迟删除过期元素?是否会导致堆内存在大量过期元素影响性能? 2. 如果滑动窗口的范围是固定的(例如长度为R-L+1),那么单调队列和优先队列哪种更合适?为什么? 3. 在动态规划状态转移中,如何处理不可达状态(如dp[i]为负无穷)以避免错误更新? 4. 如果题目要求至少需要跳多少次才能到达对岸,该如何修改动态规划方程? </think>### 优先队列优化解法详解 在问题中,使用优先队列优化动态规划的核心思路是维护一个最大堆(优先队列),用于高效查询滑动窗口 $[i-R, i-L]$ 范围内的最大 $dp$ 值。以下是优化要点和修正后的代码示例: #### 优化关键点 1. **数据结构**:使用最大堆(`priority_queue`)存储 `(dp[j], j)` 对,按 dp 值降序排列 2. **延迟删除**:堆顶元素过期($j < i-R$)时立即弹出,保证堆顶始终有效 3. **状态转移**:$dp[i] = A[i] + \text{heap\_top}$ 4. **新候选加入**:当 $i \geq L$ 时,将 $dp[i-L]$ 加入堆 #### 修正后的 C++ 代码(优先队列实现) ```cpp #include <iostream> #include <queue> #include <vector> #include <climits> using namespace std; typedef pair<int, int> pii; // (dp_value, index) int main() { int N, L, R; cin >> N >> L >> R; vector<int> A(N+1); for (int i = 0; i <= N; i++) cin >> A[i]; vector<int> dp(N+1, INT_MIN); dp[0] = A[0]; // 初始状态 A[0]=0 priority_queue<pii> pq; // 最大堆 pq.push({dp[0], 0}); for (int i = 1; i <= N; i++) { // 加入新候选 (当i≥L时) if (i >= L) pq.push({dp[i-L], i-L}); // 移除过期元素 (j < i-R) while (!pq.empty() && pq.top().second < i - R) { pq.pop(); } // 状态转移 if (!pq.empty()) { dp[i] = A[i] + pq.top().first; } } // 计算最终答案 int ans = INT_MIN; int start_idx = max(0, N+1-R); for (int i = start_idx; i <= N; i++) { ans = max(ans, dp[i]); } cout << ans << endl; return 0; } ``` #### 关于代码片段的修正 您提供的片段: ```cpp int f1=q.top()->first; int s1=q.top()->second; ``` 应修正为: ```cpp int dp_value = pq.top().first; // 获取堆顶的dp值 int index = pq.top().second; // 获取堆顶的索引 ``` 原因: 1. 优先队列存储的是 `pair<int, int>` 值类型,不是指针 2. 应使用 `.first` 和 `.second` 访问成员,而非指针解引用 #### 复杂度分析 - **时间复杂度**:$O(N \log N)$,每个元素最多入堆/出堆一次 - **空间复杂度**:$O(N)$,堆和 dp 数组的空间
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值