草庐IT

Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解)

CCSU_梅子酒 2025-05-20 原文

题外话:抑郁场,开局一小时只出A,死活想不来B,最后因为D题出锅ura才保住可怜的分。但咱本来就写不到D

B - Long Legs(数论)

本题题解法一学自同样抑郁的知乎作者 幽血魅影的题解,有讲解原理。
法二来着知乎巨佬 cup-pyy(大佬说《不难发现》呜呜)

题意

三种操作:

  1. 向上走 m m m
  2. 向右走 m m m
  3. 给自己一次走的步数加 1 1 1,即使得 m = m + 1 m = m + 1 m=m+1

问从 ( 0 , 0 ) (0,0) (0,0) 走到 ( a , b ) (a, b) (a,b)的最小操作次数,值得注意的是操作三不可逆。

解析

假设我们最终一步的大小增长到 m m m, 那么在这个过程中我能以 [ 1 , m ] [1, m] [1,m] (当步数增长到该数时)之间的任何数字向上或向右迈出 k k k 步。
贪心的考虑,使得最大的步数 m m m 迈的越多对我们来说更优。
设终点为 ( a , b ) (a,b) (a,b) 我们先向上迈出 a m o d    m a \mod m amodm 步, 再向右迈出 b m o d    m b \mod m bmodm 步,此时剩下的向上向下还需要走的步数肯定都能整除 m m m

通俗的来讲操作流程就是,先将步数增加到 k = min(a % m, b % m),向取模小的那个方向走一步,再将步数增加到 k = max(a % m, b % m),向取模大的方向走一步,此时两个方向剩余步数都能整除 m m m,将步数增加到 m m m 直接走即可。

于是答案有 a n s = ⌈ a / m ⌉ + ⌈ b / m ⌉ + m − 1 ans = \lceil a / m \rceil + \lceil b / m \rceil + m - 1 ans=a/m+b/m+m1
设最终的 m m m 为 未知数 x x x ,观察方程 f ( x ) = a + b x + x − 1 f(x) = \frac {a + b} x + x - 1 f(x)=xa+b+x1不难发现其中的 a + b x + x \frac {a + b} x + x xa+b+x 是双勾函数, 而 1 1 1 是常数,函数在 x = a + b x = \sqrt{a + b} x=a+b 时取到最小值。
但是原式中有向上取整的影响,我们需要在 极值 a + b \sqrt{a + b} a+b 的周围寻找真正的极值点。

法二:考虑到数据范围,甚至可以枚举最终的 m m m 以取最小值。

代码

solve1() 为法一,solve2() 为法二,都能 AC。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

void solve1(){
    int a, b;
    cin >> a >> b;
    int x = sqrt(a + b);
    int ans = a + b;
    for(int i = max(1, x - 100); i <= x + 100; i ++){
        ans = min(ans, (a + i - 1) / i + (b + i - 1) / i + i - 1);
    }
    cout << ans << "\n";
}

void solve2(){
    int a, b;
    cin >> a >> b;
    int ans = a + b;
    for(int i = 1; i <= 1000000; i ++){
        ans = min(ans, (a + i - 1) / i + (b + i - 1) / i + i - 1);
    }
    cout << ans << "\n";
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    int t;
    cin >> t;
    while(t --){
        solve1();
        // solve2();
    }
    return 0;
}

E - Chain Chips

第一次见 DDP (动态动态规划)。

题意

给定 n n n 个节点的无向图和 n − 1 n - 1 n1 条边,每条边有权值 w i w_i wi i i i号边连接 i i i i + 1 i + 1 i+1 号节点。
最初每个节点上有一个编号与节点编号相同的球,每次可以将一个球通过一条边移动到相邻的节点上,花费为边的权值,问使得每个节点上有一个球,且球的编号与节点编号不同的最小花费。
并且有 q q q 个询问,每次询问会修改一条边,询问修改后原问题的答案。(每次修改都会持续对问题影响,每次询问之间不独立)

解析

博主自己做时,没看懂每次操作对接下来都有影响,还以为每次操作都是独立的,写了个 DP。
在这里先讲解一下如果每次操作独立的解法(因为没有样例可能有bug,但思路应该没问题的)

每次操作独立

我们考虑最大的花费:将所有球向右移动一位,将最后一个球移动到 1 1 1 号节点上,这样所有的边都被使用了两次。

我们从较小的情况考虑:

  1. 两个节点那么就是 1 1 1 号球到节点 2 2 2 2 2 2 号球到节点 1 1 1 a n s = w 1 ∗ 2 ans = w_1 * 2 ans=w12
  2. 三个节点情况也很简单 1 1 1->节点 3 3 3 2 2 2->节点 1 1 1 3 3 3->节点 2 2 2 a n s = w 1 ∗ 2 + w 2 ∗ 2 ans = w_1 * 2 + w_2 * 2 ans=w12+w22
  3. 四个节点我们可以将其分成两部分 [ 1 , 2 ] , [ 3 , 4 ] [1,2],[3,4] [1,2],[3,4],即两个情况 1 1 1 。这时我们发现 节点 [ 2 , 3 ] [2,3] [2,3] 之间的边我们没有使用到。
  4. 最后再考虑一下五个节点的情况,我们可以将其分成 [ 1 , 2 , 3 ] + [ 4 , 5 ] [1,2,3] + [4,5] [1,2,3]+[4,5] 或者 [ 1 , 2 ] + [ 3 , 4 , 5 ] [1,2] + [3,4,5] [1,2]+[3,4,5],即情况 1 1 1 + + + 情况 2 2 2

此时我们就可以发现,可以将某一段区间不选不让球通过,以此来减少我们的花费。

就可以想到维护一个前缀DP 和 后缀DP,因为和本题关系不到了,具体看代码吧,也可以跳过直接看正解。

这是操作独立的代码不是本题的代码!

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 2e5 + 10;
const ll inf = 1e18;

ll f1[N], f2[N]; //f1:前i个完成交换的最小花费, f2:从n ~ i完成交换的最小花费
int n, a[N];

void init(){
    f1[0] = 0, f1[1] = inf, f1[2] = 2 * a[1];
    for(int i = 3; i <= n; i ++){
        f1[i] = inf;
        ll sum = a[i - 1];
        for(int j = i - 2; j >= i - 3; sum += a[j], j --){
            f1[i] = min(f1[i], f1[j] + sum * 2);
        }
    }

    f2[n + 1] = 0, f2[n] = inf, f2[n - 1] = 2 * a[n - 1];
    for(int i = n - 2; i >= 1; i --){
        f2[i] = inf;
        ll sum = a[i];
        for(int j = i + 2; j <= i + 3; sum += a[j - 1], j ++){
            f2[i] = min(f2[i], f2[j] + sum * 2);
        }
    }
    cout << "f1[n] = " << f1[n] << " f2[n] = " << f2[n] << "\n";
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n;
    for(int i = 1; i < n; i ++){
        cin >> a[i];
    }

    init();
    
    int m;
    cin >> m;
    while(m --){
        int idx, val;
        cin >> idx >> val;
        ll ans = f1[idx] + f2[idx + 1];
        ans = min(ans, f1[idx - 1] + f2[idx + 2] + val * 2);
        if(idx - 2 >= 0)
            ans = min(ans, f1[idx - 2] + a[idx - 1] * 2 + val * 2 + f2[idx + 2]);
        if(idx + 3 <= n + 1)
            ans = min(ans, f1[idx - 1] + val * 2 + a[idx + 1] * 2 + f2[idx + 3]);
        cout << ans << "\n";
    }
    return 0;
}

操作之间不独立

总结一下上述规律以及结论:
关于 n n n 个节点,无论数量为多少,总能将其分成若干个大小为 2 2 2 3 3 3 的区间使其错排, 使得其中某些边可以不被使用,以此减少花费。

推广一下,我们可以使得任意长度的一段错排花费代价为边权和的两倍(一条边被使用肯定要使用两次,本节点过去 + 其他节点过来),而上述的分成长度为 2 2 2 3 3 3 的是贪心的使得更多边可以不被使用。

重点:
对于一段节点 [ l , r ] [l, r] [l,r],其中的边权之和为 s s s,那么可以用 2 s 2s 2s 的代价将其错排,排列完后边 [ l − 1 , l ] [l-1, l] [l1,l] [ r , r + 1 ] [r,r+1] [r,r+1] 就可以不再使用,那么题目就可以抽象为给定连续的 n − 1 n - 1 n1 段,其中有些段可以不选但不能有连续的超过一段不选求最小值权值和。

我们就要考虑 DDP 线段树维护。具体见代码

先将最重要的线段树结构体定义和 线段之间合并 拿出来讲解。

struct node{
    int l, r;
    ll vl, vr, vall, vno; // 最小花费
    // vl:选[l, l + 1],不选[r - 1, r] 左选右不选
    // vr:选[r - 1, r],不选[l, l + 1] 右选左不选
    // vall: 左右都选
    // vno: 左右都不选
}tr[N * 4];

void pushup(int p){
    node &ls = tr[p << 1], &rs = tr[p << 1 | 1];
    /* 所有选择都基于:可以有边不选,但不能有连续的两条边及以上不选 */
    tr[p].vl = min({
        ls.vl + rs.vl,
        ls.vall + rs.vl,
        ls.vall + rs.vno
    });

    tr[p].vr = min({
        ls.vr + rs.vr,
        ls.vr + rs.vall,
        ls.vno + rs.vall
    });

    tr[p].vall = min({
        ls.vl + rs.vall,
        ls.vall + rs.vr,
        ls.vall + rs.vall
    });

    tr[p].vno = min({
        ls.vr + rs.vl,
        ls.vr + rs.vno,
        ls.vno + rs.vl
    });
}

剩下的就是基本的线段树,看代码和注释就行了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 2e5 + 10;
const ll inf = 1e18;

int n, a[N];

struct node{
    int l, r;
    ll vl, vr, vall, vno; // 最小花费
    // vl:选[l, l + 1],不选[r - 1, r] 左选右不选
    // vr:选[r - 1, r],不选[l, l + 1] 右选左不选
    // vall: 左右都选
    // vno: 左右都不选
}tr[N * 4];

void pushup(int p){
    node &ls = tr[p << 1], &rs = tr[p << 1 | 1];
    /* 所有选择都基于:可以有边不选,但不能有连续的两条边及以上不选 */
    tr[p].vl = min({
        ls.vl + rs.vl,
        ls.vall + rs.vl,
        ls.vall + rs.vno
    });

    tr[p].vr = min({
        ls.vr + rs.vr,
        ls.vr + rs.vall,
        ls.vno + rs.vall
    });

    tr[p].vall = min({
        ls.vl + rs.vall,
        ls.vall + rs.vr,
        ls.vall + rs.vall
    });

    tr[p].vno = min({
        ls.vr + rs.vl,
        ls.vr + rs.vno,
        ls.vno + rs.vl
    });
}

void build(int p, int l, int r){
    tr[p] = {l, r, inf, inf, inf, inf};
    if(l == r){
        tr[p].vno = 0;
        tr[p].vall = a[l];
        if(l == 1 || l == n - 1) tr[p].vno = inf; // 左右两端的边必须选,因为1号节点和n号节点只有一条边连接
        return ;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(p);
}

void update(int p, int loc, int k){
    if(tr[p].l == tr[p].r){
        tr[p].vall = k;
        return ;
    }
    int mid = (tr[p].l + tr[p].r) >> 1;
    if(loc <= mid) update(p << 1, loc, k);
    else update(p << 1 | 1, loc, k);
    pushup(p);
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n;
    for(int i = 1; i < n; i ++) cin >> a[i];   

    build(1, 1, n - 1); // n 个点 n - 1 条边
    
    int m;
    cin >> m;
    for(int i = 1; i <= m; i ++){
        int u, val;
        cin >> u >> val;
        update(1, u, val);
        cout << min({tr[1].vl, tr[1].vr, tr[1].vall, tr[1].vno}) * 2 << "\n";
    }
    return 0; 
}

有关Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解)的更多相关文章

  1. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  2. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  3. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  4. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  5. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  6. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  7. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  8. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

  9. ElasticSearch之 ik分词器详解 - 2

    IK分词器本文分为简介、安装、使用三个角度进行讲解。简介倒排索引众所周知,ES是一个及其强大的搜索引擎,那么它为什么搜索效率极高呢,当然和他的存储方式脱离不了关系,ES采取的是倒排索引,就是反向索引;常见索引结构几乎都是通过key找value,例如Map;倒排索引的优势就是有效利用Value,将多个含有相同Value的值存储至同一位置。分词器为了配合倒排索引,分词器也就诞生了,只有合理的利用Value,才会让倒排索引更加高效,如果一整个Value不进行任何操作直接进行存储,那么Value和key毫无区别。分词器Analyzer通常会对Value进行操作:一、字符过滤,过滤掉html标签;二、分

  10. Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解) - 2

    题外话:抑郁场,开局一小时只出A,死活想不来B,最后因为D题出锅ura才保住可怜的分。但咱本来就写不到DB-LongLegs(数论)本题题解法一学自同样抑郁的知乎作者幽血魅影的题解,有讲解原理。法二来着知乎巨佬cup-pyy(大佬说《不难发现》呜呜)题意三种操作:向上走mmm步向右走mmm步给自己一次走的步数加111,即使得m=m+1m=m+1m=m+1问从(0,0)(0,0)(0,0)走到(a,b)(a,b)(a,b)的最小操作次数,值得注意的是操作三不可逆。解析假设我们最终一步的大小增长到mmm,那么在这个过程中我能以[1,m][1,m][1,m](当步数增长到该数时)之间的任何数字向上或

随机推荐