CSDN周赛66期图文题解 - 路灯亮度 & 池塘水量

2023-11-18

本期非编程题考察更多是对原书的阅读理解,可能还是因为自己理解不够,翻了半天书,还是错了两道。失之我命,不多废话。

本期编程题比较符合我的胃口,有陷阱,有技巧,窃以为是最近不少期里比较有意思的中等难度的题目了。美中不足的是两道题都没有给出数据范围,从而在判断时间复杂度及选择算法上存在一定的迷惑性。

第一题 路灯亮度

有一条长度为n米的街道,上面分布着一些亮度不等的路灯。每离开某盏路灯1米,该位置受该盏路灯影响的亮度就比该盏路灯的原始亮度减少1个单位,而某个特定位置的最终亮度则等于所有影响该位置的路灯亮度的最大值,而亮度的最小值为0。

输入描述:第一行输入两个正整数n和m分别表示街道长度和路灯数量,从第二行开始的m行每行两个正整数p和l分别表示路灯的位置和光亮程度(1<=p<=n)。再随后的q行每行都有一个正整数i,表示一个位置。

输出描述:输出q行表示所有的i对应的位置的亮度值

输入样例:

10 2
1 5
4 3
1
3
10

输出样例:

5
3
0

解析

题意不难理解,就是如下图这样,一条横坐标(街道)上在某些坐标点有 “路灯”,使得左右的坐标点都被不同程度地 “照亮”。因为某些坐标点可能同时被多个 “路灯” 照到(比如下图中 a 点和 b 点),所以问题就是给出 “路灯” 的坐标以及 q 个坐标点,让你找出这 q 个坐标点能被照到的最大的亮度。

一般类似这种题目,都有两种解答方式,如果查询次数较少,我们没有必要知道 “街道” 上每个点的亮度,不需要准备查询数组,只需要查询的时候,再去找出答案好了;反之,如果查询次数很多,则会把算法的大部分时间用在实现查询数组上,这样后面的查询将以 O(1) 的方式(每次查询都是常数级)完成。

本题数据有点水(所以第一种解法得以实现),但又不是那么水(见解法二)。

解法一

题目没有给出 q 的数据范围,在用例中也没有给出 q 的大小,所以我们无法判断 q 到底有多大。在实际测试中,可以发现 q 的范围并不大。所以我们可以不用准备查询数组,而是当需要查询某坐标位置的时候,再去穷举 p 个“路灯”,查看在每个 “路灯” 的 “照射” 下,要查询的坐标点的亮度是多少,再找出其中的最大值即可。

而每个 “路灯” 对某个坐标点的 “照射亮度”,根据题意(随着距离向两侧递减),存在很简单的数学公式:路灯亮度 - 路灯与坐标点的距离,而且这个值最小为 0。

所以使用 python 一句代码就可计算得到答案。

print(max(0, max(l - abs(p - q) for p, l in lights)))

这种方法其实也是穷举,可以清楚地看到,每次查询的时间复杂度为 O(p),p 为 “路灯” 的数量。而如果有 q 次查询,整体复杂度为 O(p*q) 。

解法二 

如上所述,当查询次数不多(q 不大)时,上面的方法完全没问题。但是如果 p 和 q 都非常大,上面的方法就存在超时的风险。所以,当查询次数很多时,通常我们会先创建查询数组。以题目为例,由一条有 n 个坐标点的 “街道”,我们可以创建一个长度为 n 的数组。然后根据每个 “路灯” 的亮度,找出每个坐标点的最大 “亮度”。这样当后面查询的时候,只要从这个数组里取出相应的答案即可。

这种方法的难点在于查询数组的建立。如何降低这个过程的复杂度,是这种方法能否成功的关键。理论上通过暴力方法也可以实现,即每当发现一个 “路灯”,就把所有的坐标点的 “亮度” 都更新一遍,时间复杂度为 O(n*p) 。本题数据倒还没有水到这种程度,不出意外的超时。

实际上,大多数类似的题目,尤其是线性数组,都存在 O(n) 复杂度的方法创建查询数组,本题也不例外。通常的办法是使用动态规划,假设 “街道” 为 dp 数组,则可得转移方程如下:

从左往右:dp[i] = max(dp[i], max(dp[i-1]-1, light[i]))

从右往左:dp[j] = max(dp[j], max(dp[j+1]-1, light[j]))

由于 “路灯” 的 “亮度” 是向两边递减,所以需要从两边两个方向各遍历一次——当然也可以同时进行,只要注意好边界问题。(由于 Python 中的 max 函数操作长列表时依然比较耗时,Python 选手直接套用依然可能超时,改成 if 语句手动判断即可)

这样,只需要 O(n) 的时间复杂度,就可以更新完成查询数组。如果后面有 q 次查询的话,整体复杂度将是 O(n+q) 。

综上所述,两种方法的优劣取决于 q 值的大小。鉴于本题并未给出 q 的范围,经实测,两种方法皆可。(从测试数据来看,貌似用例中的 p 和 q 的值都比较小)

本题不难,加上已经详细解释思路及解题步骤,代码故此省略。

第二题 池塘水量

某地有n个池塘,编号为i的池塘最大容积为i(从1开始编号),一开始都没有水。每天的天气状况决定了所有水池同时的水量变化,如果某天下了容积为v的雨表示所有池塘都会增加容积v的水,当然池塘满了的话水就会溢出而流走,不会影响到次日以后。如果某天天气炎热而蒸发了容积为v的水表示所有池塘都会减少容积v的水,当然池塘最多把所有水蒸发干而变成容积为0,也不会影响到次日以后。

输入描述:第一行输入两个正整数n和m分别表示池塘数量n和天气的天数m,接下来的m行每行一个数表示每天天气导致的水容积的变化,正数表示增加、负数表示减少,0表示没有变化。

输出描述:m行,每行表示当天池塘的总水容积。

输入样例:

5 3
1
3
-2

输出样例:

5
14
5

解析

看懂题目之后,我想大部分选手想到的都是暴力模拟的解法吧?甚至会觉得此题怎会如此简单?而当进行完暴力模拟的尝试后,就会发现原来测试数据里暗藏玄机 —— 总有那么几个点 T(timeout)了。

实际上,本题正确的解法是单调栈。这么说可能并不准确,因为单调栈实际上只是一种数据结构,真正能够优化算法的是借助单调栈实现的数形结合

暴力模拟的方法就不多说了,相信大家也已经都尝试过了吧,时间复杂度为 O(n*m)。(使用 Python 能够通过的用例相较编译型语言会更少,因为 Python 的数组 / 列表在实现灵活度的同时牺牲了性能,当处理大数组 / 长列表时,如果不使用 Numpy 等第三方模块,速度要比 C 慢上十倍都不止。)

暴力模拟只能通过部分用例,让我们来看看单调栈+数形结合该怎么做。

根据题意,我们可以在纸上画出类似下面的 “池塘” 模拟图。

但是不知为什么(可能是直觉),我在读完题后,画出的模拟图是下面这个样子:所有 “池塘” 的底部都在同一水平。其实不难证明,这两张图在本题的各种情况下都等价。由于我后面的分析都是基于此图,大家可以先跟着我的思路,待充分理解后再带入上面的图形里。(也许是先入为主,窃以为这样的图更有利于分析)

观察此图,它的底(“池塘” 总个数)与高(最深的 “池塘” 深度)相等,所以在几何数学里可以将其看做一个等腰直角三角形。

先考虑最基本的情形,当部分 “池塘” 因 “落雨” 而注水时,该图形变成下面的样子:

要计算所有 “池塘” 积水的容积,相当于计算图中蓝色部分的面积 S。而这部分面积可以像下面这样拆分成两部分,从而得到 S=S_{0}+S_{1} :

根据题意可知,h_{1} 等于降水 v。很显然,S_{0} 也是一个等腰直角三角形,所以 w_{1}=n-h_{1} 。

S_{1} 是平行四边形,所以它的面积 S_{1}=h_{1}*w_{1} 。而 S_{0} 的面积计算稍微有点特殊,这里不能使用等腰直角三角形的计算公式 h_{1}*h_{1}/2 。因为实际上,从微积分的角度来看,等腰直角三角形可以看成是 h_{1} 个从 0 到 h_{1} 排列的一组数字的集合,从而它的面积计算公式等价于高斯求和的形式 h_{1}*(0+h_{1})/2 。但在本题里,因为这个 “三角形” 实际上是从 1 到 h_{1} 的排列,所以它的面积应该是 h_{1}*(1+h_{1})/2 。

从而得到总面积计算公式 S = h_{1}*(1+h_{1})/2 + h_{1}*w_{1},也就是所有 “池塘积水” 的容积。

于是,就单次 “降雨” 来看,我们已经通过数形结合的方法,将暴力模拟的 O(n) 线性复杂度的算法优化成了 O(1) 的常数阶算法。接下来,我们来分析一下更复杂的情形。

第二天,假设 “池塘” 的水被 “蒸发” 时,所有 “池塘” 水位同时下降,我们将会得到下面的图形(左边的 “池塘” 干涸了):

这时我们注意到,蓝色部分的面积仍然等于 h_{1}*(1+h_{1})/2 + h_{1}*w_{1},只不过 h_{1} 的数值发生了变化,而 w_{1} 却并没有改变——这就是我们为什么要在上一步里多增加一个变量,而不是直接使用 n-h_{1} 的原因。所以,我们只要将原先的 h_{1} 减去这一天 “蒸发”的水量 v,得到新的 h_{1},依然能通过上述公式一步计算出当前所有 “池塘积水” 的容积。

假设第三天又发生了 “降雨”,却又回不到最初的水量,这时产生的图形长这个样子:

重点来了,这时产生了一个新的平行四边形,它的面积 S_{2} = h_{2}*w_{2} 。很显然 h_{2} 等于这天的降水量 v ,但 w_{2} 等于多少呢?我们放大来看:

w_{2} 应该等于前一天平面没有水的长度减去 h_{2} ,而回到前一天的图形,可以看到,这个长度实际上等于前一天 “蒸发” 的水量 v 。

并且这个高度为 v 的小等腰直角三角形,与大的高度为 n 的等腰直角三角形(由所有 “池塘” 组成)是等比关系。

我们暂停一下,从头捋一下至今为止用到的变量,用 v_{1},v_{2},v_{3} 表示三天的 “降雨” / “蒸发” 的体积:

  • 最开始第 0 天,所有 “池塘” 为空,“三角形” 的底和高都是 n
  • 第一天 “降雨”,h_{1}=v_{1}w_{1}=n-h_{1}
  • 第二天 “蒸发”,h_{1}=h_{1}-v_{2}w_{1} 不变
  • 第三天 “降雨”,h_{1}=h_{1}+v_{3}w_{1} 不变,h_{2}=v_{3}w_{2}=v_{2}-h_{2}

由于第二天和最开始第 0 天的情形是等比关系,所以我们要在第 0 天记录 n,第二天记录 v_{2},这样在第三天的时候就可以通过公式一步计算出总面积 S = S_{0} + \sum_{i=1}^{k}S_{i} ,其中 k 等于左边平行四边形的个数。

如果后面的 “降雨” 如此反复,将会形成下面的图形:

重点,更复杂的情况来了。很显然 “降雨” / “蒸发” 不可能这么规律,当 “降雨” 超过最左边的平行四边形的边长 w_{i} 时,它将会被 “淹没”,而当 “蒸发” 的容积大于最左边平行四边形的高度 h_{i} 时,它将会 “消失”:

轮到我们的单调栈出场了。由于左边的平行四边形的高度是单调递减的,在 “降雨” 和 “蒸发” 的过程中最左边的平行四边形还会 “增加” 或 “减少”,对于这种情况,我们可以维护一个单调栈。很显然,栈里要维护的变量有两个,h_{i} 和 w_{i},可以用列表表示 [h_{i},w_{i}] 。还记得我们前面提到过要在第 0 天和第二天记录 n 和 v_{2} 吗?这就相当于完全无水和部分水面 “干涸” 的时候,栈顶有一个高度为 0 的元素 [0,n] 或 [0,v_{2}],因为高度为 0,它的存在并不会影响计算结果,反而使得栈的维护更加容易。

本题数形结合的分析过程到这里就结束了,相信聪明的你一点就通。

综合上述所有分析,总结栈的维护过程,并用代码实现:

  • “降雨” 时,
    • 最左边(栈顶)的平行四边形的 w_{i} 会发生变化,当 “降雨” 的容积 v 大于栈顶的 w_{i} 时,栈顶元素 [h_{i},w_{i}] 出栈,然后循环检查新栈顶的 w_{i} ;
    • 在此过程中,依次将弹出的栈顶的 w_{i} 减去 v 以抵消变化,得到最后的水位以更新栈顶的 w_{i} ;
    • 最后一步,再把栈里所有元素的 h_{i} 都加上此次 “降雨” 的容积 v 。完成更新。
    • 如果在第一步里栈内所有元素都弹出,说明所有 “池塘” 都满了,入栈 [n, 0]
  • “蒸发” 时,
    • 最左边(栈顶)的平行四边形的 h_{i} 会发生变化,当 “蒸发” 的容积 v 大于栈顶的 h_{i} 时,栈顶元素 [h_{i},w_{i}] 出栈,然后循环检查新栈顶的 h_{i} ;
    • 在此过程中,依次累加弹出的栈顶的 w_{i} 以得到 “干涸” 的水面宽度,得到最后的宽度 \sum w+v_{i},入栈 [0, \sum w+ v_{i}]
    • 最后一步,再把栈里所有元素的 h_{i} 都减去此次 “蒸发” 的容积 v 。完成更新。
    • 如果在第一步里栈内所有元素都弹出,说明所有 “池塘” 都干涸了,入栈 [0,n]

而计算每一天的所有 “池塘积水” 的容积,使用公式即可得到:

S = S_{0} + \sum_{i=1}^{k}S_{i}

带入变量:

S = h_{1}*(1+h_{1})/2 + \sum_{i=1}^{k}h_{i}*w_{i}

代码(部分)
n, m = map(int, input().split())
h = [[0, n]] # 初始化栈,所有池塘都干涸的情况
for _ in range(m):
    v = int(input())
    if v >= 0: # 水位上升
        temp = v
        while h and h[-1][1] < temp:
            temp -= h.pop()[1]
        if not h: h.append([n, 0]) # 所有池塘都满了
        else:
            h[-1][1] -= temp # 更新栈顶 “水面宽度”
            for i in h: i[0] += v # 更新栈内所有 “平行四边形” 的 “高度”
    else:
        # 请参考题解试着自己补全水位下降的代码

    s = h[0][0]*(1+h[0][0])//2 + sum(i[0]*i[1] for i in h)
    print(s)

该算法的时间复杂度为 O(m*k),其中 m 是给定的天数,k 是栈内元素的数量。说实话,这个 k 我不太好判断,理论上,当每天的 “降雨” / “蒸发” 极度变态时(形成锯齿状), k=min(n, m) 。但我用本题所有用例实测发现,k 的值不超过 7,相当于常数了。或可说明平均情况下(或单就本题而言),复杂度为 O(m) 。

本题或许还存在别的解法,不过相信思路大同小异,画图出来,一切就了然了。


码字不易,画图更不易,如果觉得上述题解对你有所帮助,希望留下你的点赞评论与鼓励。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CSDN周赛66期图文题解 - 路灯亮度 & 池塘水量 的相关文章

随机推荐

  • 匿名信V1.4.5.1版本更新“数据大屏”功能

    匿名信V1 4 5 1版本更新 数据大屏 功能 源码下载 匿名信h5源码 万策云盘 匿名信安装教程 匿名信v1 4 4源码下载 安装教程 匿名信 廖万里的博客 本文链接 匿名信V1 4 5 1版本更新 数据大屏 功能 匿名信 廖万里的博客
  • java中long最大值源码表示_通过JDK源码角度分析Long类详解

    概况 Java的Long类主要的作用就是对基本类型long进行封装 提供了一些处理long类型的方法 比如long到String类型的转换方法或String类型到long类型的转换方法 当然也包含与其他类型之间的转换方法 除此之外还有一些位
  • MySQL简述1

    MySQL是什么 MySQL优点 MySQL的四种分类 数据库的三大范式 多表查询 左连接 右连接 内连接 交叉连接 显式 隐式 子查询 事物 特性 原子性 一致性 隔离性 持久性 并发问题 脏读 读未提交 不可重复读 读已提交 幻读 可重
  • 机器学习之高斯过程

    高斯过程 高斯过程 Gaussian Process 高斯分布 置信区间 随机过程 高斯分布的特点 核函数 白噪声处理 实战 高斯过程 Gaussian Process 在机器学习领域里 高斯过程是一种假设训练数据来自无限空间 并且各特征都
  • Fiddler 微信小程序抓图教程(非常详细)从零基础入门到精通,看完这一篇就够了

    前言 本篇文章主要给大家详细讲解如何用Fiddler爬取微信小程序的图片 内容图文并茂 流程非常简单 我们开始吧 目录 一 获取软件并打开 二 点击工具设置相关代理 三 如何抓图 四 答疑 五 总结 一 获取软件并打开 1 通过百度网盘下载
  • 因果推断----do演算

    do演算 合法 的do表达式变换 规则1 如果我们观察到变量W与Y无关 其前提可能是以其他变量Z为条件 那么Y的概率分布就不会随W而改变 即 P Y d
  • vue3+elementPlus-浏览器告警解决error.ts:14 ElementPlusError: [ElPagination] 你使用了一些已被废弃的用法,请参考 el-pagination

    问题 在使用elementuiPlus的分页器组件的时候 发现会有如下图警告 检查代码
  • 微信小程序父组件向子组件传参,子组件样式无效问题处理

    微信小程序父组件向子组件传参 子组件样式无效问题处理 父组件代码 引入 json usingComponents evaluate evaluate evaluate wxml
  • dp 1.4协议_浅析关于HDMI接口与DP接口

    显示器现在主流已经为HDMI接口与DP接口 那么这些接口都有什么区别 以下表格会大致做个区分 建议优先使用DP接口 HDMI2 1接口目前仅发布协议 尚未大规模商用在高清电视机上有部分应用 Mini DP接口版本为DP1 2 HDMI2 1
  • libcurl库安装心得

    一 libcurl简介 libcurl是一个跨平台的网络协议库 支持http https ftp gopher telnet dict file 和ldap 协议 libcurl同样支持HTTPS证书授权 HTTP POST HTTP PU
  • JSON工具类

    在实际开发中通服都是使用JSON格式数据 那么如何跟JSON打交道呢 下面就写一些JSON的常用转换工具 以及JSON数据提取 目录 阿里的FastJSON JSONObject类 JSON类 JSONArray JSONPath Json
  • 分子对接教程

    TCGA GEO 文献阅读 数据库 理论知识 R语言 Bioconductor 服务器与Linux 接前文 分子对接教程 1 软件安装准备 分子对接教程 2 选择合适的蛋白受体 分子对接教程 3 配体分子文件格式转换 分子对接教程 4 蛋白
  • QT 中文版信息提示框

    引言 在QT设计UI程序过程中 整套系统都是中文版本 然而信息提示默认只有中文 难免有点小纠结 这里针对QMessageBox稍微做了一点点改进 使其支持完美的中文提示框 调用方式非常简单 只需要将QMessageBox调用地方 改为QSh
  • 专家PID

    专家PID 专家控制 专家控制是模拟人类专家控制的方式 它具有大量的专门知识和经验 和专家控制一样不需要知道对象的模型的情况下 对系统进行控制 专家控制的基本结构 和人类专家控制一样 知识库越是丰富 推理机越是精确 控制效果也就越好 不同的
  • 数据结构C++ 栈Stack求值算法

    来自邓俊辉老师的数据结构 C 版 第95页 readNumber函数 可读整数和小数 注意 下列代码是直接用C 内部写好的stack实现的 而不是书中给出的stack模板 发现更简洁的readNumber函数 float readNumbe
  • 使用Vue解决跨域问题

    如果你是一个Web前端工程师 那么跨域这个问题肯定是绕不开的 1 创建 vue config js 设置 devServer 属性 module exports devServer webpack dev server配置 host loc
  • ECS共享型s6和ECS突发性能型t6的区别选择哪个好?

    WP建站 一个专注于wordpress学习的 关注他 2 人赞同了该文章 这两个类型的阿里云ecs服务器的话 一般在这两个中二选一的话我们建议优先选择ECS共享型s6 我们简单的来说说他们的一些区别和特点吧 首先我们要知道的是他们都是独立的
  • 线性代数-----行列式的性质

    行列式的性质 设 D a 11
  • cosmos测试网络结点搭建完整流程

    第一步 下载golang并安装 配置环境变量 wget https dl google com go go1 13 8 linux amd64 tar gz tar C usr local xzf go VERSION OS ARCH ta
  • CSDN周赛66期图文题解 - 路灯亮度 & 池塘水量

    本期非编程题考察更多是对原书的阅读理解 可能还是因为自己理解不够 翻了半天书 还是错了两道 失之我命 不多废话 本期编程题比较符合我的胃口 有陷阱 有技巧 窃以为是最近不少期里比较有意思的中等难度的题目了 美中不足的是两道题都没有给出数据范