The Burrows-Wheeler Transform 块排序压缩算法

前言 最近发现了一个非常有意思的算法:The Burrows-Wheeler Transform(块排序压缩算法),所以稍微花点时间简单记录一下 说是一个压缩算法,实际上这个算法主要做的事情是把数据重排序,实际上并没有减少数据的长度(通常反而因为增加了行尾标识而增长了) 但是它完成了一个非常有意思的效果:在几乎不增加字符串长度的情况下,把一个字符串的重复的部分尽可能聚合到一块了 而 Gzip 压缩算法基于Deflate算法,通过结合LZ77算法(查找并替换重复字符串)和霍夫曼编码,通过将接近的字符串聚合到一块,能够有效增加压缩率 算法操作方式 我做了一个简单的演示工具,我们就用经典的 banana 作为示例 在原串后添加结束结束符号$,且此符号认为是最小的字符 banana\$ 生成字符串的全部循环序列 banana\$ anana\$b nana\$ba ana\$ban na\$bana a\$banan \$banana 将这几个字符串排序 \$banana a\$banan ana\$ban anana\$b banana\$ na\$bana nana\$ba 取出最后一列字符串 \$banana a\$banan ana\$ban anana\$b banana\$ na\$bana nana\$ba 得到结果 annb\$aa 下面将进行还原操作 annb\$aa 将结果排成一列 a n n b \$ a a 排序 \$ a a a b n n 在当前的列之前添加 BWT 结果 a\$ na na ba \$b an an 再次排序 \$b a\$ an an ba na na 重复上述步骤 a\$b na\$ nan ban \$ba ana ana 再次排序 \$ba a\$b ana ana ban na\$ nan 重复上述步骤 a\$ba na\$b nana bana \$ban ana\$ anan 再次排序 \$ban a\$ba ana\$ anan bana na\$b nana 重复上述步骤 a\$ban na\$ba nana\$ banan \$bana ana\$b anana 再次排序 \$bana a\$ban ana\$b anana banan na\$ba nana\$ 重复上述步骤 a\$bana na\$ban nana\$b banana \$banan ana\$ba anana\$ 再次排序 \$banan a\$bana ana\$ba anana\$ banana na\$ban nana\$b 重复上述步骤 a\$banan na\$bana nana\$ba banana\$ \$banana ana\$ban anana\$b 再次排序 \$banana a\$banan ana\$ban anana\$b banana\$ na\$bana nana\$ba 回到最初的矩阵 \$banana a\$banan ana\$ban anana\$b banana\$ na\$bana nana\$ba 上一步 下一步 .demo-button { padding: 6px 14px; border: 1px solid #ccc; background: #fff; cursor: pointer; border-radius: 4px; } .demo-button:disabled { opacity: .4; cursor: not-allowed; } .demo-block { display: grid; place-items: center; } .demo-button-block { display: grid; grid-template-columns: 1fr auto 30px auto 1fr; } .demo-window { height: 200px; font-family: monospace; border: 1px solid gray; padding: 5px 30px 5px 30px; margin: 5px 0 5px 0; } .red { color: red; } .green { color: green; } .gray { color: gray; } let i = 0, v = document.getElementById('v'), c = v.children, prev = document.getElementById('prev'), next = document.getElementById('next'); let u = () => { [...c].forEach((d, n) => d.hidden = n !== i); prev.disabled = i === 0; next.disabled = i === c.length - 1; }; let p = () => { if (i > 0) i--; u() }; let n = () => { if (i 算法原理 觉得这个算法有意思的地方,可能并不是它的实用价值。这里有一个非常有意思:为什么这样排序了几次之后,就会回到最初的矩阵 这里蕴藏了一个非常有意思的字符串排序逻辑。 通常情况下,我们会使用字符串从第一个字符开始比较,如果相同则比下一个字符 而在这个问题下,假定所有字符串长度相同,那其实完全可以从最后一位比起,然后逐次比较新增加的字符,可以实现类似桶排序的方式,达成最终排序结果 由于后排序的结果会覆盖先排序的结果,使得实际上达成了“从第一个字符开始比较,如果相同则比下一个字符”的效果

2026/1/1
articleCard.readMore

Summer Pockets 动画完结

无论怎么说,恭喜最终是完结了。作为一个原作党,对于剧情什么的,也没有什么特别能够想表达的内容,毕竟剧本已经放在那边,给制作组能够自由发挥的空间也很少。 不过原作的文本量巨大,虽然已经作为半年番的方式播出,但个人感觉还是不够长,很多原作中的剧情并没有很好的表现出来,很多感情的递进都没有很恰当的表达,这也是导致我看番的时候有一种比较割裂又自然能够衔接上的点。这里说几个个人很遗憾的细节场景,如果有兴趣的话建议还是玩一下原作 羽未回忆自己的原来生活的片段 原文大概是这样的 晚上,我做了两人份的饭 一份是给我的 一份是给爸爸的 我将他们分装在盘子里,然后盖上了保鲜膜 一个人吃完饭,洗好自己的胖子,做完作业…… 然后去洗个澡,就准备睡觉了 我钻进被子里闭上了眼睛 没过多久,玄关就传来了开门的声音 是爸爸他回来了 但是他什么都没有说。我能听到的,就只有他那在地上拖着的,疲惫的脚步声 这是我司空见惯的事情 餐桌上传来了塑料袋被放下的声音 那里面,装着的一定是他在便利店里买来的酒和下酒菜 这也是我司空见惯的事情 所以,我就直接这样睡了过去 早上醒来的时候,家里就只剩下我一个人了 我走进厨房 发现之前分装在盘子里的饭还是还是原模原样地被放在那里 所以,我就将它放进了微波炉 等待了两分钟 这样,早饭就做好了 这还是我司空见惯的事情 还有一些别的场景,我也打算后面再补充一些,个人感觉这一幕被压缩了太多时间,这里感觉更适合花费更长的时间去表达这种生活,就像 CLANNAD 中因为渚离开后,男主那种颓废状态,似乎就是一个很好的参考

2025/10/4
articleCard.readMore

香港银行开卡记-1

本文内嵌了 Google Map,请考虑当前的网络环境是否支持查看 背景 前段时间前往了香港一趟,主要是因为我妈想要换手机了,所以只能打算把我现在的手机给她,然后我自己再去买个新的。考虑到现在 Apple Intelligence 也没有打算在国内上线,所以打算去一趟香港买手机,顺便办下卡 由于自己早就有了别的港卡,所以这次去香港主要是想要办一张中国银行的卡。虽然中资银行的服务不咋滴,同时背景也不好,但是给的汇率确实令人欣喜 安排 考虑到中国银行周六只上半天班,周日不上班的情况,所以如果要办理的话,推荐周五或者其他工作日去办理,大概办理一次的时间要 1-2 个小时 我的方案是周四晚就飞到深圳,然后在福田站附近先睡一晚,第二天一大早起来赶火车去火车站,福田站有一条直达的火车线路,虽然一般地图会告诉你需要 1 小时左右,但是实际上过去大概只需要 15 分钟,这里预估的时间应该都是加上了过海关之类的时间 如果打算的是 walk-in,由于开户需要很早过去排队,所以推荐尽可能早的火车班次,然后直接去银行门口排队等开门,特别是西九龙这块的银行,比如中港城的 这家银行一天只给 25 个开户名额,所以基本上只要超过 9 点到,就赶不上了。这里推荐一家我去开的分行,在九龙城区 这家在西九龙下火车后,直接做地铁就可以直达,非常快,而且人比较少,一般 14 点 walk-in 都能进去开户。特别的一点是,这里附近 100米就有一家图书馆,香港的图书馆提供打印服务,所以如果缺少什么文件的话,就可以来这里打印,虽然开门有点晚 当然也可以考虑走预约开户,地址是 这里,但是预约其实挺难预约到好的分行,基本上提前一周进去开,还是不一定能抢到西九龙的预约名额。所以如果真的要推荐的话,可以先看看能不能预约上,即使是北区也凑合,然后再 walk-in 这家试试能成就行,不能成再去北区 注意事项 中银香港开户是需要准备纸质材料的,但是最终他们不收材料,只会拿你的材料复印一份存档。我这边推荐带上以上材料 身份证(原件,无需复印,必须) 港澳通行证/护照(原件,无需复印,必须) 过关小票(原件,无需复印,必须) 最近的信用卡账单,要求上面必须要有一个居住地址,作为地址证明(如果没有的话,那就用身份证上的地址作为居住地址吧) 国内的信用卡现在都是用电子账单,不一定有居住地址,很多银行是支持开带有居住地址的信用卡账单的,可以问下客服 因为国内的信用卡账单填写的实际上是通讯地址,也不怎么检查地址的真实性,所以实际上可以通过国内信用卡地址来把你银行登记的居住地址改为任意地址 如果给不出信用卡账单之类的东西,那就乖乖填身份证上的地址吧,记得跟柜员说你的通讯地址和居住地址不同,这样后续寄送材料的时候,就可以问问是否会寄送到通讯地址而不是居住地址 因为很多寄送的卡片都是平邮,如果你的居住地址比较偏僻,可能会导致丢件,所以推荐还是通讯地址填写一个比较可靠一点的地方 公司内提供的收入证明,用来证明你的收入资产,开户会方便很多 最近半年的银行卡流水,同上,也是为了财产证明,毕竟国外的银行让你来开户都是希望从你身上赚点钱,而不是就白白让你开个户然后里面一分钱都不留 找一个过去的证券账户的结单,一般不推荐最近的结单,可以早半年甚至一年的结单 如果希望开通中银香港的美元证券账户的话,需要地址证明是英文的,但好像大家都不会去用他们的美元证券账户,所以好像也没啥用 另外请务必保证手机号在香港能够正常收到短信,否则开户的手机号你只能临时找别人的手机号顶替了

2024/11/26
articleCard.readMore

【转载】Rust 中常见的有关生命周期的误解

1. 介绍 Rust学习中最难的部分就是⽣命周期,很多⽣命周期规则衍⽣的复杂情况并没有在 TRPL 中得到介绍,因此本⽂的⽬的是想帮助Rust程序员打通⽣命周期的问题,同时也希望能为 modern C++ 程序员带来⼀些思考和启发。 本⽂中使⽤的术语可能并不那么官⽅,因此下⾯列出了⼀个表格,记录使⽤的短语及其想表达的含义。 短语意义 T1)所有可能类型的集合 或 2)上述集合中的某一个具体类型 所有权类型某些非引用类型,其自身拥有所有权 例如 i32, String, Vec 等 1)借用类型 或 2) 引用类型引用类型,不考虑可变性 例如 &i32, &mut i32 1)可变引用 或 2) 独占引用独占可变引用,即 &mut T 1) 不可变引用 或 2) 共享引用可共享不可变引用,即 &T 2. 误解 简单来讲,⼀个变量的⽣命周期是指⼀段时期,在这段时期内,该变量所指向的内存地址中的数据是有效的,这段时期是由编译器静态分析得出的,有效性由编译器保证。接下来我将探讨这些常⻅误解的细节。 1) T 只包含所有权类型 这更像是对泛型的误解⽽⾮对⽣命周期的误解,但在 Rust 中,泛型与⽣命周期的关系是如此紧密,以⾄于不可能只讨论其中⼀个⽽忽视另外⼀个。当我刚开始学习 Rust 时,我知道 i32 , &i32 , 和 &mut i32 是不同的类型,同时我也知泛型 T 表示所有可能类型的集合。然⽽,尽管能分别理解这两个概念,但我却没能将⼆者结合起来。在当时我这位 Rust 初学者的眼⾥,泛型是这样运作的: 类型T&T&mut T 例子i32&i32&mut i32 其中 T 包全体所有权类型;&T 包括全体不可变引⽤; &mut T 包括全体可变引⽤;T, &T, 和 &mut T 是不相交的有限集。简洁明了,符合直觉,却完全错误。事实上泛型是这样运作的: 类型T&T&mut T 例子i32, &i32, &mut i32, &&i32, &mut &mut i32&i32, &&i32, &&mut i32&mut i32, &mut &mut i32, &mut &i32 T , &T , 和 &mut T 都是⽆限集,因为你可以借⽤⼀个类型⽆限次。T 是 &T 和 &mut T 的超集。&T 和 &mut T 是不相交的集合. 下⾯有⼀些例⼦来验证这些概念: 1 2 3 4 5 6 7 trait Trait {} impl<T> Trait for T {} impl<T> Trait for &T {} // 编译错误 impl<T> Trait for &mut T {} // 编译错误 上述代码不能编译通过: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 error[E0119]: conflicting implementations of trait `Trait` for type `&_`: --> src/lib.rs:5:1 | 3 | impl<T> Trait for T {} | ------------------- first implementation here 4 | 5 | impl<T> Trait for &T {} | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_` error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`: --> src/lib.rs:7:1 | 3 | impl<T> Trait for T {} | ------------------- first implementation here ... 7 | impl<T> Trait for &mut T {} | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _` 编译器不允许我们为 &T 和 &mut T 实现 Trait ,因为这与我们为 T 实现的 Trait 发⽣了冲突,⽽ T 已经包括了 &T 和 &mut T. 因为 &T 和 &mut T 是不相交的,所以下⾯的代码可以通过编译: 1 2 3 4 5 trait Trait {} impl<T> Trait for &T {} // 编译通过 impl<T> Trait for &mut T {} // 编译通过 关键点回顾 T 是 &T 和 &mut T 的超集 &T 和 &mut T 是不相交的集合 2) 如果 T: 'static 那么 T 直到程序结束为⽌都⼀定是有效的 错误的推论 T: 'static 应该视为 “ T 有着 'static ⽣命周期” &'static T 和 T: 'static 是⼀回事 若 T: 'static 则 T ⼀定是不可变的 若 T: 'static 则 T 只能在编译期创建 让⼤多数 Rust 初学者第⼀次接触 'static ⽣命周期注解的代码示例⼤概是这样的: 1 2 3 fn main() { let str_literal: &'static str = "字符串字⾯量"; } 他们被告知说 "字符串字⾯量" 是被硬编码到编译出来的⼆进制⽂件当中去的,并在运⾏时被加载到只读内存中,所以它不可变且在程序的整个运⾏期间都有效,这也使其⽣命周期为 'static. 在了解到 Rust 使⽤ static 来定义静态变量这⼀语法后,这⼀观点还会被进⼀步加强。 1 2 3 4 5 6 7 8 9 10 11 static BYTES: [u8; 3] = [1, 2, 3]; static mut MUT_BYTES: [u8; 3] = [1, 2, 3]; fn main() { MUT_BYTES[0] = 99; // 编译错误,修改静态变量是 unsafe 的 unsafe { MUT_BYTES[0] = 99; assert_eq!(99, MUT_BYTES[0]); } } 关于静态变量 它们只能在编译期创建 它们应当是不可变的,修改静态变量是 unsafe 的 它们在整个程序运⾏期间有效 静态变量的默认⽣命周期很有可能是 'static, 对吧?所以可以合理推测 'static ⽣命周期也要遵循同样的规则,对吧? 确实,但 持有 'static ⽣命周期注解的类型和⼀个满⾜ 'static 约束 的类型是不⼀样的。后者可以于运⾏时被动态分配,能被安全⾃由地修改,也可以被 drop, 还能存活任意的时⻓。区分 &'static T 和 T: 'static 是⾮常重要的⼀点。&'static T 是⼀个指向 T 的不可变引⽤,其中 T 可以被安全地⽆期限地持有,甚⾄可以直到程序结束。这只有在 T ⾃身不可变且保证 在引⽤创建后 不会被 move 时才有可能。T 并不需要在编译时创建。我们可以以内存泄漏为代价,在运⾏时动态创建随机数据,并返回其 'static 引⽤,⽐如: 1 2 3 4 5 6 7 use rand; // 在运⾏时⽣成随机 &'static str fn rand_str_generator() -> &'static str { let rand_string = rand::random::<u64>().to_string(); Box::leak(rand_string.into_boxed_str()) } T: 'static 是指 T 可以被安全地⽆期限地持有,甚⾄可以直到程序结束。 T: 'static 在包括了全部 &'static T 的同时,还包括了全部所有权类型, ⽐如 String, Vec 等等。 数据的所有者保证,只要⾃身还持有数据的所有权,数据就不会失效,因此所有者能够安全地⽆期限地持有其数据,甚⾄可以直到程序结束。 T: 'static 应当视为 “ T 满⾜ 'static ⽣命周期约束” ⽽⾮ “ T 有着 'static ⽣命周期”。 ⼀个程序可以帮助阐述这些概念: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 fn drop_static<T: 'static>(t: T) { std::mem::drop(t); } fn main() { let mut strings: Vec<String> = Vec::new(); for _ in 0..10 { if rand::random() { // 所有字符串都是随机⽣成的 // 并且在运⾏时动态分配 let string = rand::random::<u64>().to_string(); // strings获取了string的所有权 strings.push(string); } } // 这些字符串是所有权类型,所以他们满⾜ 'static ⽣命周期约束 for mut string in strings { // 这些字符串是可变的 string.push_str("a mutation"); // 这些字符串都可以被 drop drop_static(string); // 编译通过 } // 这些字符串在程序结束之前就已经全部失效了 println!("i am the end of the program"); } 关键点回顾 T: 'static 应当视为 “ T 满⾜ 'static ⽣命周期约束” 若 T: 'static 则 T 可以是⼀个有 'static ⽣命周期的引⽤类型 或 是⼀个所有权类型 因为 T: 'static 包括了所有权类型,所以 T 可以在运⾏时动态分配 不需要在整个程序运⾏期间都有效 可以安全,⾃由地修改 可以在运⾏时被动态的 drop 可以有不同⻓度的⽣命周期 3) &'a T 和 T: 'a 是⼀回事 这个误解是前⼀个误解的泛化版本。&'a T 要求并隐含了 T: 'a ,因为如果 T 本身不能在 'a 范围内保证有效,那么其引⽤也不能在 ‘a 范围内保证有效。例如,Rust 编译器不会运⾏构造⼀个 &'static Ref<'a, T> ,因为如果 Ref 只在 'a 范围内有效,我们就不能给它 'static ⽣命周期。T: 'a 包括了全体 &'a T ,但反之不成⽴。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 只接受带有 'a ⽣命周期注解的引⽤类型 fn t_ref<'a, T: 'a>(t: &'a T) {} // 接受满⾜ 'a ⽣命周期约束的任何类型 fn t_bound<'a, T: 'a>(t: T) {} // 内部含有引⽤的所有权类型 struct Ref<'a, T: 'a>(&'a T); fn main() { let string = String::from("string"); t_bound(&string); // 编译通过 t_bound(Ref(&string)); // 编译通过 t_bound(&Ref(&string)); // 编译通过 t_ref(&string); // 编译通过 t_ref(Ref(&string)); // 编译失败,期望得到引⽤,实际得到 struct t_ref(&Ref(&string)); // 编译通过 // 满⾜ 'static 约束的字符串变量可以转换为 'a 约束 t_bound(string); // 编译通过 } 关键点回顾 T: 'a ⽐ &'a T 更泛化,更灵活 T: 'a 接受所有权类型,内部含有引⽤的所有权类型,和引⽤ &'a T 只接受引⽤ 若 T: 'static 则 T: 'a 因为对于所有 'a 都有 'static >= 'a 4) 我的代码⾥不含泛型也不含⽣命周期注解 错误的推论 避免使⽤泛型和⽣命周期注解是可能的 这个让⼈爽到的误解之所以能存在,要得益于 Rust 的⽣命周期省略规则,这个规则能允许你在函数 定义以及 impl 块中省略掉显式的⽣命周期注解,⽽由借⽤检查器来根据以下规则对⽣命周期进⾏ 隐式推导。 第⼀条规则是每⼀个是引⽤的参数都有它⾃⼰的⽣命周期参数 第⼆条规则是如果只有⼀个输⼊⽣命周期参数,那么它被赋予所有输出⽣命周期参数 第三条规则是如果是有多个输⼊⽣命周期参数的⽅法,⽽其中⼀个参数是 &self 或 &mut self, 那么所有输出⽣命周期参数被赋予 self 的⽣命周期。 其他情况下,⽣命周期必须有明确的注解 这⾥有不少值得讲的东⻄,让我们来看⼀些例⼦: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 显式标注的⽅案 fn get_str<'a>() -> &'a str; // 泛型版本 fn get_str() -> &'static str; // 'static 版本 // ⾮法,多个输⼊,不能确定返回值的⽣命周期 fn overlap(s: &str, t: &str) -> &str; // 显式标注(但仍有部分标注被省略)的⽅案 fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // 返回值的⽣命周期不⻓于 s fn overlap<'a>(s: &str, t: &'a str) -> &'a str; // 返回值的⽣命周期不⻓于 t fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // 返回值的⽣命周期不⻓于 s 且不⻓于 t fn overlap(s: &str, t: &str) -> &'static str; // 返回值的⽣命周期可以⻓于 s 或者 t fn overlap<'a>(s: &str, t: &str) -> &'a str; // 返回值的⽣命周期与输⼊⽆关 // 展开后 fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'a str; fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'b str; fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'static str; fn overlap<'a, 'b, 'c>(s: &'a str, t: &'b str) -> &'c str; // 展开前 fn compare(&self, s: &str) -> &str; // 展开后 fn compare<'a, 'b>(&'a self, &'b str) -> &'a str; 如果你写过 结构体⽅法 接收参数中有引⽤的函数 返回值是引⽤的函数 泛型函数 trait object(后⾯将讨论) 闭包(后⾯将讨论) 那么对于上⾯这些,你的代码中都有被省略的泛型⽣命周期注解。 关键点回顾 ⼏乎所有的 Rust 代码都是泛型代码,并且到处都带有被省略掉的泛型⽣命周期注解 5) 如果编译通过了,那么我标注的⽣命周期就是正确的 错误的推论 Rust 对函数的⽣命周期省略规则总是对的 Rust 的借⽤检查器总是正确的,⽆论是技巧上还是语义上 Rust ⽐我更懂我程序的语义 让⼀个 Rust 程序通过编译但语义上不正确是有可能的。来看看这个例⼦: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct ByteIter<'a> { remainder: &'a [u8] } impl<'a> ByteIter<'a> { fn next(&mut self) -> Option<&u8> { if self.remainder.is_empty() { None } else { let byte = &self.remainder[0]; self.remainder = &self.remainder[1..]; Some(byte) } } } fn main() { let mut bytes = ByteIter { remainder: b"1" }; assert_eq!(Some(&b'1'), bytes.next()); assert_eq!(None, bytes.next()); } ByteIter 是⼀个 byte 切⽚上的迭代器,简洁起⻅,我这⾥省略了 Iterator trait 的具体实现。这看起来没什么问题,但如果我们想同时检查多个 byte 呢? 1 2 3 4 5 6 7 8 fn main() { let mut bytes = ByteIter { remainder: b"1123" }; let byte_1 = bytes.next(); let byte_2 = bytes.next(); if byte_1 == byte_2 { // ⼀些代码 } } Compiler Error: 1 2 3 4 5 6 7 8 9 error[E0499]: cannot borrow `bytes` as mutable more than once at a time --> src/main.rs:20:18 | 19 | let byte_1 = bytes.next(); | ----- first mutable borrow occurs here 20 | let byte_2 = bytes.next(); | ^^^^^ second mutable borrow occurs here 21 | if byte_1 == byte_2 { | ------ first borrow later used here 如果你说可以通过逐 byte 拷⻉来避免编译错误,那么确实。当迭代⼀个 byte 数组上时,我们的确可以通过拷⻉每个 byte 来达成⽬的。但是如果我想要将 ByteIter 改写成⼀个泛型的切⽚迭代器,使得我们能够对任意 &'a [T] 进⾏迭代,⽽此时如果有⼀个 T,其 copy 和 clone 的代价⼗分昂贵,那么我们该怎么避免这种昂贵的操作呢?哦,我想我们不能,毕竟代码都通过编译了,那么⽣命周期注解肯定也是对的,对吧?错,事实上现有的⽣命周期就是 bug 的源头!这个错误的⽣命周期被省略掉了以⾄于难以被发现。现在让我们展开这些被省略掉的⽣命周期来暴露出这个问题。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct ByteIter<'a> { remainder: &'a [u8] } impl<'a> ByteIter<'a> { fn next<'b>(&'b mut self) -> Option<&'b u8> { if self.remainder.is_empty() { None } else { let byte = &self.remainder[0]; self.remainder = &self.remainder[1..]; Some(byte) } } } 感觉好像没啥⽤,我还是搞不清楚问题出在哪。这⾥有个 Rust 专家才知道的⼩技巧:给你的⽣命周期注解起⼀个更有含义的名字,让我们试⼀下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct ByteIter<'remainder> { remainder: &'remainder [u8] } impl<'remainder> ByteIter<'remainder> { fn next<'mut_self>(&'mut_self mut self) -> Option<&'mut_self u8> { if self.remainder.is_empty() { None } else { let byte = &self.remainder[0]; self.remainder = &self.remainder[1..]; Some(byte) } } } 每个返回的 byte 都被标注为 'mut_self, 但是显然这些 byte 都源于 'remainder! 让我们来修复⼀下这段代码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct ByteIter<'remainder> { remainder: &'remainder [u8] } impl<'remainder> ByteIter<'remainder> { fn next(&mut self) -> Option<&'remainder u8> { if self.remainder.is_empty() { None } else { let byte = &self.remainder[0]; self.remainder = &self.remainder[1..]; Some(byte) } } } fn main() { let mut bytes = ByteIter { remainder: b"1123" }; let byte_1 = bytes.next(); let byte_2 = bytes.next(); std::mem::drop(bytes); // 我们现在甚⾄可以把这个迭代器给 drop 掉! if byte_1 == byte_2 { // 编译通过 // ⼀些代码 } } 现在我们再回过头来看看我们上⼀版的实现,就能看出它是错的了,那么为什么 Rust 会编译通过呢?答案很简单:因为这是内存安全的。Rust 借⽤检查器对⽣命周期注解的要求只到能静态验证程序的内存安全为⽌。即便⽣命周期注解有语义上的错误,Rust 也能让程序编译通过,哪怕这样做为程序带来不必要的限制。这⼉有⼀个和之前相反的例⼦:在这个例⼦中,Rust ⽣命周期省略规则标注的⽣命周期是语义正确的,但是我们却在⽆意间使⽤了不必要的显式注解,导致写出了⼀个限制极其严格的⽅法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #[derive(Debug)] struct NumRef<'a>(&'a i32); impl<'a> NumRef<'a> { // 我定义的泛型结构体以 'a 为参数,这意味着我也需要给⽅法的参数 // 标注为 'a ⽣命周期,对吗?(答案:错) fn some_method(&'a mut self) {} } fn main() { let mut num_ref = NumRef(&5); num_ref.some_method(); // 可变借⽤ num_ref 直⾄其⽣命周期结束 num_ref.some_method(); // 编译错误 println!("{:?}", num_ref); // 同样编译错误 } 如果我们有⼀个带 'a 泛型参数的结构体,我们⼏乎不可能去写⼀个带 &'a mut self 参数的⽅法。因为这相当于告诉 Rust “这个⽅法将独占借⽤该对象,直到对象⽣命周期结束”。实际上,这意味着 Rust 的借⽤检查器只会允许在该对象上调⽤⾄多⼀次 some_method, 此后该对象将⼀直被独占借⽤并会因此变得不再可⽤。这种⽤例极其罕⻅,但是因为这种代码能够通过编译,所以那些对⽣命周期还感到困惑的初学者们很容易写出这种 bug. 修复这种 bug 的⽅式是去除掉不必要的显式⽣命周期注解,让 Rust ⽣命周期省略规则来处理它: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)] struct NumRef<'a>(&'a i32); impl<'a> NumRef<'a> { // 不再给 mut self 添加 'a 注解 fn some_method(&mut self) {} // 上⼀⾏去掉语法糖后: fn some_method_desugared<'b>(&'b mut self){} } fn main() { let mut num_ref = NumRef(&5); num_ref.some_method(); num_ref.some_method(); // 编译通过 println!("{:?}", num_ref); // 编译通过 } 关键点回顾 Rust 对函数的⽣命周期省略规则并不保证在任何情况下都正确 在程序的语义⽅⾯,Rust 并不⽐你懂 可以试试给你的⽣命周期注解起⼀个有意义的名字 试着记住你在哪⾥添加了显式⽣命周期注解,以及为什么要加 6) Boxed Trait Object对象不含⽣命周期注解 之前我们讨论了 Rust 对函数 的⽣命周期省略规则。Rust 对 trait 对象也存在⽣命周期省略规则,它 们是: 如果 trait 对象被⽤作泛型类型的⼀个类型参数,那么 trait 对象的⽣命周期约束会依据该类型参数 的定义进⾏推导 若该类型参数有唯⼀的⽣命周期约束,则将这个约束赋给 trait 对象 若该类型参数不⽌⼀个⽣命周期约束,则 trait 对象的⽣命周期约束需要显式标注 如果上⾯不成⽴,也就是说该类型参数没有⽣命周期约束,那么 若 trait 定义时有且仅有⼀个⽣命周期约束,则将这个约束赋给 trait 对象 若 trait 定义时⽣命周期约束中存在⼀个 'static , 则将 'static 赋给 trait 对象 若 trait 定义时没有⽣命周期约束,则当 trait 对象是表达式的⼀部分时,⽣命周期从表达式中推导⽽出,否则赋予 'static 以上这些听起来特别复杂,但是可以简单地总结为⼀句话“⼀个 trait 对象的⽣命周期约束从上下⽂推导⽽出。”看下⾯这些例⼦后,我们会看到⽣命周期约束的推导其实很符合直觉,因此我们没必要去记忆上⾯的规则: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type T2 = Box<dyn Trait + 'static>; // 展开前 impl dyn Trait {} // 展开后 impl dyn Trait + 'static {} // 展开前 type T3<'a> = &'a dyn Trait; // 展开后,&'a T 要求 T: 'a, 所以推导为 'a type T4<'a> = &'a (dyn Trait + 'a); // 展开前 type T5<'a> = Ref<'a, dyn Trait>; // 展开后,Ref<'a, T> 要求 T: 'a, 所以推导为 'a type T6<'a> = Ref<'a, dyn Trait + 'a>; trait GenericTrait<'a>: 'a {} // 展开前 type T7<'a> = Box<dyn GenericTrait<'a>>; // 展开后 type T8<'a> = Box<dyn GenericTrait<'a> + 'a>; // 展开前 impl<'a> dyn GenericTrait<'a> {} // 展开后 impl<'a> dyn GenericTrait<'a> + 'a {} ⼀个实现了 trait 的具体类型可以被引⽤,因此它们也会有⽣命周期约束,同样其对应的 trait 对象也有⽣命周期约束。你也可以直接对引⽤实现 trait, 引⽤显然是有⽣命周期约束的: 1 2 3 4 5 6 7 8 trait Trait {} struct Struct {} struct Ref<'a, T>(&'a T); impl Trait for Struct {} impl Trait for &Struct {} // 直接为引⽤类型实现 Trait impl<'a, T> Trait for Ref<'a, T> {} // 为包含引⽤的类型实现 Trait 总之,这个知识点值得反复理解,新⼿在重构⼀个使⽤ trait 对象的函数到⼀个泛型的函数或者反过来时,常常会因为这个知识点⽽感到困惑。来看看这个示例程序: 1 2 3 4 5 6 7 8 9 10 11 12 13 use std::fmt::Display; fn dynamic_thread_print(t: Box<dyn Display + Send>) { std::thread::spawn(move || { println!("{}", t); }).join(); } fn static_thread_print<T: Display + Send>(t: T) { std::thread::spawn(move || { println!("{}", t); }).join(); } 这⾥编译器报错: 1 2 3 4 5 6 7 8 9 10 11 12 13 error[E0310]: the parameter type `T` may not live long enough --> src/lib.rs:10:5 | 9 | fn static_thread_print<T: Display + Send>(t: T) { | -- help: consider adding an explicit lifetime bound...: `T: 'static +` 10 | std::thread::spawn(move || { | ^^^^^^^^^^^^^^^^^^ | note: ...so that the type `[closure@src/lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds --> src/lib.rs:10:5 | 10 | std::thread::spawn(move || { | ^^^^^^^^^^^^^^^^^^ 很好,编译器告诉了我们怎样修复这个问题,让我们修复⼀下。 1 2 3 4 5 6 7 8 9 10 11 12 13 use std::fmt::Display; fn dynamic_thread_print(t: Box<dyn Display + Send>) { std::thread::spawn(move || { println!("{}", t); }).join(); } fn static_thread_print<T: Display + Send + 'static>(t: T) { std::thread::spawn(move || { println!("{}", t); }).join(); } 现在它编译通过了,但是这两个函数对⽐起来看起来挺奇怪的,为什么第⼆个函数要求 T 满⾜ 'static 约束⽽第⼀个函数不⽤呢?这是个刁钻的问题。事实上,通过⽣命周期省略规则,Rust ⾃动在第⼀个函数⾥推导并添加了⼀个 'static 约束,所以其实两个函数都含有 'static 约束。Rust 编译器实际看到的是这个样⼦的: 1 2 3 4 5 6 7 8 9 10 11 12 13 use std::fmt::Display; fn dynamic_thread_print(t: Box<dyn Display + Send + 'static>) { std::thread::spawn(move || { println!("{}", t); }).join(); } fn static_thread_print<T: Display + Send + 'static>(t: T) { std::thread::spawn(move || { println!("{}", t); }).join(); } 关键点回顾 所有 trait 对象都含有⾃动推导的⽣命周期 7) 编译器的报错信息会告诉我怎样修复我的程序 错误的推论 Rust 对 trait 对象的⽣命周期省略规则总是正确的 Rust ⽐我更懂我程序的语义 这个误解是前两个误解的结合,来看⼀个例⼦: 1 2 3 4 5 use std::fmt::Display; fn box_displayable<T: Display>(t: T) -> Box<dyn Display> { Box::new(t) } 报错如下 1 2 3 4 5 6 7 8 9 10 11 12 13 error[E0310]: the parameter type `T` may not live long enough --> src/lib.rs:4:5 | 3 | fn box_displayable<T: Display>(t: T) -> Box<dyn Display> { | -- help: consider adding an explicit lifetime bound...: `T: 'static +` 4 | Box::new(t) | ^^^^^^^^^^^ | note: ...so that the type `T` will meet its required lifetime bounds --> src/lib.rs:4:5 | 4 | Box::new(t) | ^^^^^^^^^^^ 好,让我们按照编译器的提示进⾏修复。这⾥我们先忽略⼀个事实:返回值中装箱的 trait 对象有⼀个⾃动推导的 'static 约束,⽽编译器是基于这个没有显式说明的事实给出的修复建议。 1 2 3 4 5 use std::fmt::Display; fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> { Box::new(t) } 现在可以编译通过了,但这真的是我们想要的吗?可能是,也可能不是,编译器并没有提到其他修复⽅案,但下⾯这个也是⼀个合适的修复⽅案。 1 2 3 4 5 use std::fmt::Display; fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> { Box::new(t) } 这个函数所能接受的实际参数⽐前⼀个函数多了不少!这个函数是不是更好?确实,但不⼀定必要,这取决于我们对程序的要求与约束。上⾯这个例⼦有点抽象,所以让我们看⼀个更简单明了的例⼦: 1 2 3 fn return_first(a: &str, b: &str) -> &str { a } 报错 1 2 3 4 5 6 7 8 9 10 11 12 error[E0106]: missing lifetime specifier --> src/lib.rs:1:38 1 | fn return_first(a: &str, b: &str) -> &str { | ---- ---- ^ expected named lifetime parameter = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b` help: consider introducing a named lifetime parameter | | | 1 | fn return_first<'a>(a: &'a str, b: &'a str) -> &'a str { | ^^^^ ^^^^^^^ ^^^^^^^ ^^^ 这个错误信息推荐我们给所有输⼊输出都标注上同样的⽣命周期注解。如果我们这么做了,那么程序将通过编译,但是这样写出的函数过度限制了返回类型。我们真正想要的是这个: 1 2 3 fn return_first<'a>(a: &'a str, b: &str) -> &'a str { a } 关键点回顾 Rust 对 trait 对象的⽣命周期省略规则并不保证在任何情况下都正确 在程序的语义⽅⾯,Rust 并不⽐你懂 Rust 编译错误的提示信息所提出的修复⽅案并不⼀定能满⾜你对程序的需求 8) ⽣命周期可以在运⾏时动态变⻓或变短 错误的推论 容器类可以在运⾏时交换其内部的引⽤,从⽽改变⾃身的⽣命周期 Rust 借⽤检查器能进⾏⾼级的控制流分析 这个编译不通过: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct Has<'lifetime> { lifetime: &'lifetime str, } fn main() { let long = String::from("long"); let mut has = Has { lifetime: &long }; assert_eq!(has.lifetime, "long"); { let short = String::from("short"); // “转换到” 短的⽣命周期 has.lifetime = &short; assert_eq!(has.lifetime, "short"); // “转换回” ⻓的⽣命周期(实际是并不是) has.lifetime = &long; assert_eq!(has.lifetime, "long"); // `short` 变量在这⾥ drop } // 编译失败, `short` 在 drop 后仍旧处于 “借⽤” 状态 assert_eq!(has.lifetime, "long"); } 报错: 1 2 3 4 5 6 7 8 9 10 error[E0597]: `short` does not live long enough --> src/main.rs:11:24 | 11 | has.lifetime = &short; | ^^^^^^ borrowed value does not live long enough ... 15 | } | - `short` dropped here while still borrowed 16 | assert_eq!(has.lifetime, "long"); | --------------------------------- borrow later used here 下⾯这个还是报错,报错信息也和上⾯⼀样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct Has<'lifetime> { lifetime: &'lifetime str, } fn main() { let long = String::from("long"); let mut has = Has { lifetime: &long }; assert_eq!(has.lifetime, "long"); // 这个代码块逻辑上永远不会被执⾏ if false { let short = String::from("short"); // “转换到” 短的⽣命周期 has.lifetime = &short; assert_eq!(has.lifetime, "short"); // “转换回” ⻓的⽣命周期(实际是并不是) has.lifetime = &long; assert_eq!(has.lifetime, "long"); // `short` 变量在这⾥ drop } // 还是编译失败, `short` 在 drop 后仍旧处于 “借⽤” 状态 assert_eq!(has.lifetime, "long"); } ⽣命周期必须在编译时被静态确定,⽽且 Rust 借⽤检查器只会做基本的控制流分析,所以它假设每个 if-else 块和 match 块的每个分⽀都能被执⾏,然后选出⼀个最短的⽣命周期赋给块中的变量。⼀旦⼀个变量被⼀个⽣命周期约束了,那么它将 永远 被这个⽣命周期所约束。⼀个变量的⽣命周期只能缩短,⽽且所有的缩短时机都在编译时确定。 9) 将独占引⽤(&mut)降级为共享引⽤(&)是 safe 的 错误的推论 通过重借⽤引⽤内部的数据,能抹掉其原有的⽣命周期,然后赋⼀个新的上去 你可以将⼀个独占引⽤作为参数传给⼀个接收共享引⽤的函数,因为 Rust 将隐式地重借⽤独占引⽤内部的数据,⽣成⼀个共享引⽤: 1 2 3 4 5 6 7 fn takes_shared_ref(n: &i32) {} fn main() { let mut a = 10; takes_shared_ref(&mut a); // 编译通过 takes_shared_ref(&*(&mut a)); // 上⾯那⾏去掉语法糖 } 这在直觉上是合理的,因为将⼀个独占引⽤转换为共享引⽤显然是⽆害的,对吗?令⼈讶异的是,这并不对,下⾯的这段程序不能通过编译: 1 2 3 4 5 6 fn main() { let mut a = 10; let b: &i32 = &*(&mut a); // 重借⽤为不可变引⽤ let c: &i32 = &a; dbg!(b, c); // 编译失败 } 报错如下: 1 2 3 4 5 6 7 8 9 error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable --> src/main.rs:4:19 | 3 | let b: &i32 = &*(&mut a); | -------- mutable borrow occurs here 4 | let c: &i32 = &a; | ^^ immutable borrow occurs here 5 | dbg!(b, c); | - mutable borrow later used here 代码⾥确实有⼀个独占引⽤,但是它⽴即重借⽤变成了⼀个共享引⽤,然后⾃身就被 drop 掉了。但是为什么 Rust 好像把这个重借⽤出来的共享引⽤看作是有⼀个独占的⽣命周期呢?上⾯这个例⼦中,允许独占引⽤直接降级为共享引⽤是没有问题的,但是这个允许确实会导致潜在的内存安全问题。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use std::sync::Mutex; struct Struct { mutex: Mutex<String> } impl Struct { // 将 self 的独占引⽤降级为 str 的共享引⽤ fn get_string(&mut self) -> &str { self.mutex.get_mut().unwrap() } fn mutate_string(&self) { // 如果 Rust 允许独占引⽤降级为共享引⽤,那么下⾯这⼀⾏代码执⾏后, // 所有通过 get_string ⽅法返回的 &str 都将变为⾮法引⽤ *self.mutex.lock().unwrap() = "surprise!".to_owned(); } } fn main() { let mut s = Struct { mutex: Mutex::new("string".to_owned()) }; let str_ref = s.get_string(); // 独占引⽤降级为共享引⽤ s.mutate_string(); // str_ref 失效,变成⾮法引⽤,现在是⼀个悬垂指针 dbg!(str_ref); // 当然,实际上会编译错误 } 这⾥的关键点在于,你在重借⽤⼀个独占引⽤为共享引⽤时,就已经落⼊了⼀个陷阱:为了保证重借⽤得到的共享引⽤在其⽣命周期内有效,被重借⽤的独占引⽤也必须保证在这段时期有效,这延⻓了独占引⽤的⽣命周期!哪怕独占引⽤⾃身已经被 drop 掉了,但独占引⽤的⽣命周期却⼀直延续到共享引⽤的⽣命周期结束。使⽤重借⽤得到的共享引⽤是很难受的,因为它明明是⼀个共享引⽤但是却不能和其他共享引⽤共存。重借⽤得到的共享引⽤有着独占引⽤和共享引⽤的缺点,却没有⼆者的优点。我认为重借⽤⼀个独占引⽤为共享引⽤的⾏为应当被视为 Rust 的⼀种反模式。知道这种反模式是很重要的,当你看到这样的代码时,你就能轻易地发现错误了 1 2 3 4 5 6 7 8 9 10 11 12 // 将独占引⽤降级为共享引⽤ fn some_function<T>(some_arg: &mut T) -> &T; struct Struct; impl Struct { // 将独占的 self 引⽤降级为共享的 self 引⽤ fn some_method(&mut self) -> &self; // 将独占的 self 引⽤降级为共享的 T 引⽤ fn other_method(&mut self) -> &T; } 尽管你可以在函数和⽅法的声明⾥避免重借⽤,但是由于 Rust 会⾃动做隐式重借⽤,所以很容易⽆意识地遇到这种情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::collections::HashMap; type PlayerID = i32; #[derive(Debug, Default)] struct Player { score: i32, } fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) { // 从 server 中得到 player, 如果不存在就创建⼀个默认的 player 并得到这个新创建的。 let player_a: &Player = server.entry(player_a).or_default(); let player_b: &Player = server.entry(player_b).or_default(); // 对得到的 player 做⼀些操作 dbg!(player_a, player_b); // 编译错误 } 上⾯这段代码会编译失败。这⾥ or_default() 会返回⼀个 &mut Player,但是由于我们添加了⼀个显式的类型标注,它会被隐式重借⽤成 &Player。⽽为了达成我们真正的⽬的,我们不得不这样做: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::collections::HashMap; type PlayerID = i32; #[derive(Debug, Default)] struct Player { score: i32, } fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) { // 因为编译器不允许这两个返回值共存,所有这⾥直接丢弃这两个 &mut Player server.entry(player_a).or_default(); server.entry(player_b).or_default(); // 再次获取 player, 这次我们直接拿到共享引⽤,避免隐式的重借⽤ let player_a = server.get(&player_a); let player_b = server.get(&player_b); // 对得到的 player 做⼀些操作 dbg!(player_a, player_b); // 现在能编译通过了 } 难⽤,⽽且很蠢,但这是我们为了内存安全这⼀信条所做出的牺牲。 关键点回顾 尽量避免重借⽤⼀个独占引⽤为共享引⽤,不然你会遇到很多麻烦 重借⽤⼀个独占引⽤并不会结束其⽣命周期,哪怕它⾃身已经被 drop 掉了 10) 对闭包的⽣命周期省略规则和函数⼀样 这更像是 Rust 的陷阱⽽⾮误解尽管闭包可以被当作是⼀个函数,但是并不遵循和函数同样的⽣命周期省略规则。 1 2 3 4 5 6 7 fn function(x: &i32) -> &i32 { x } fn main() { let closure = |x: &i32| x; } 报错: 1 2 3 4 5 6 7 8 error: lifetime may not live long enough --> src/main.rs:6:29 | 6 | let closure = |x: &i32| x; | - - ^ returning this value requires that `'1` must outlive `'2` | | | | | return type of closure is &'2 i32 | let's call the lifetime of this reference `'1` 去掉语法糖后,我们得到的是: 1 2 3 4 5 6 7 8 9 10 // 输⼊的⽣命周期应⽤到了输出上 fn function<'a>(x: &'a i32) -> &'a i32 { x } fn main() { // 输⼊和输出有它们⾃⼰各⾃的⽣命周期 let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x }; // 注意:上⼀⾏并不是合法的语句,但是我们需要它来描述我们⽬的 } 出现这种差异并没有什么好处。只是在闭包最初的实现中,使⽤的类型推断语义与函数不同,⽽现在将⼆者做⼀个统⼀将是⼀个 breaking change, 因此现在已经没法改了。那么我们怎么显式地标注⼀个闭包的类型呢?我们有以下⼏种⽅案: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fn main() { // 转换成 trait 对象,但这样是不定长的,所以会编译错误 let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x; // 可以分配到堆上作为代替方案,但是在这里堆分配感觉有点蠢 let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x); // 可以不⽤堆分配⽽直接创建⼀个 'static 引⽤ let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x; // 上⼀⾏去掉语法糖 :) let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x }; // 这看起来很完美,但可惜不符合语法 let identity: impl Fn(&i32) -> &i32 = |x: &i32| x; // 这个也⾏,但也不符合语法 let identity = for<'a> |x: &'a i32| -> &'a i32 { x }; // 但是 "impl trait" 可以作为函数的返回值类型 fn return_identity() -> impl Fn(&i32) -> &i32 { |x| x } let identity = return_identity(); // 上⼀个解决⽅案的泛化版本 fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T { f } let identity = annotate(|x: &i32| x); } 我想你应该注意到了,在上⾯的例⼦中,如果对闭包应⽤ trait 约束,闭包会和函数遵循同样的⽣命周期省略规则。这⾥没有什么现实的教训或⻅解,只是说明⼀下闭包是这样的。 关键点回顾 每个语⾔都有其陷阱 避免在闭包中使⽤⽣命周期! 11) 'static 引⽤总能被强制转换为 'a 引⽤ 我之前有过这样的代码: 1 2 fn get_str<'a>() -> &'a str; // 泛型版本 fn get_str() -> &'static str; // 'static 版本 两者之间是否有实际的差异。我⼀开始并不确定,但⼀番研究过后遗憾地发现,是的,这⼆者确实有差异。通常在使⽤值时,我们能⽤ 'static 引⽤直接代替⼀个 'a 引⽤,因为 Rust 会⾃动把 'static 引⽤强制转换为 'a 引⽤。直觉上这很合理,因为在⼀个对⽣命周期要求⽐较短的地⽅⽤⼀个⽣命周期⽐较⻓的引⽤绝不会导致任何内存安全问题。下⾯的这段代码通过编译,和预期⼀致: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use rand; fn generic_str_fn<'a>() -> &'a str { "str" } fn static_str_fn() -> &'static str { "str" } fn a_or_b<T>(a: T, b: T) -> T { if rand::random() { a } else { b } } fn main() { let some_string = "string".to_owned(); let some_str = &some_string[..]; let str_ref = a_or_b(some_str, generic_str_fn()); // 编译通过 let str_ref = a_or_b(some_str, static_str_fn()); // 编译通过 } 然⽽当引⽤作为函数类型签名的⼀部分时,强制类型转换并不⽣效。所以下⾯这段代码不能通过编译: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 use rand; fn generic_str_fn<'a>() -> &'a str { "str" } fn static_str_fn() -> &'static str { "str" } fn a_or_b_fn<T, F>(a: T, b_fn: F) -> T where F: Fn() -> T { if rand::random() { a } else { b_fn() } } fn main() { let some_string = "string".to_owned(); let some_str = &some_string[..]; let str_ref = a_or_b_fn(some_str, generic_str_fn); // 编译通过 let str_ref = a_or_b_fn(some_str, static_str_fn); // 编译错误 } 报错如下: 1 2 3 4 5 6 7 8 9 10 error[E0597]: `some_string` does not live long enough --> src/main.rs:23:21 | 23 | let some_str = &some_string[..]; | ^^^^^^^^^^^ borrowed value does not live long enough ... 25 | let str_ref = a_or_b_fn(some_str, static_str_fn); | ---------------------------------- argument requires that `some_string` is borrowed for `'static` 26 | } | - `some_string` dropped here while still borrowed 很难说这是不是 Rust 的⼀个陷阱,把 for<T> Fn() -> &'static T 强制转换为 for<'a, T> Fn() -> &'a T 并不是⼀个像把 &'static str 强制转换为 &'a str 这样简单直⽩的情况。前者是类型之间的转换,后者是值之间的转换。 关键点回顾 for <'a,T> fn()->&'a T 签名的函数⽐ for <T> fn()->&'static T 签名的函数要更灵活,并且泛⽤于更多场景 3. 总结 因为静态分析技术的局限以及 Rust 的保守性,可以看到 Rust 在内存安全做了很多看似很傻的实现(对于 C++ ⾼⼿来说实际上也很傻的)。对于恼羞成怒的 Rust 新⼿(包括作者本⼈),可以会⼀怒之下就 unsafe{} 就开始像写 C ⼀样写 Rust 了,但这显然违背了 Rust 这⻔语⾔的初衷。同时这⾥⾯提到的基于⽣命周期可能发⽣的内存安全问题,也能给 modern C++ 的程序员带来启发,并不是懂点指针和⾯向对象就能说⾃⼰会 C++ 的,⼀个资深的 C++ 程序员要时刻考虑内存安全问题。safe Rust 的好处就是,通过看似很傻的⽣命周期机制,尽可能的去帮程序员规避内存安全问题。 T 是 &T 和 &mut T 的超集 &T 和 &mut T 是不相交的集合 T: 'static 应当视为 “ T 满⾜ 'static ⽣命周期约束” 若 T: 'static 则 T 可以是⼀个有 'static ⽣命周期的引⽤类型 或 是⼀个所有权类型 因为 T: 'static 包括了所有权类型,所以 T 可以在运⾏时动态分配 不需要在整个程序运⾏期间都有效 可以安全,⾃由地修改 可以在运⾏时被动态的 drop 可以有不同⻓度的⽣命周期 T: 'a ⽐ &'a T 更泛化,更灵活 T: 'a 接受所有权类型,内部含有引⽤的所有权类型,和引⽤ &'a T 只接受引⽤ 若 T: 'static 则 T: 'a 因为对于所有 'a 都有 'static >= 'a ⼏乎所有的 Rust 代码都是泛型代码,并且到处都带有被省略掉的泛型⽣命周期注解e Rust ⽣命周期省略规则并不保证在任何情况下都正确 在程序的语义⽅⾯,Rust 并不⽐你懂 可以试试给你的⽣命周期注解起⼀个有意义的名字 试着记住你在哪⾥添加了显式⽣命周期注解,以及为什么要 所有 trait 对象都含有⾃动推导的⽣命周期 Rust 编译错误的提示信息所提出的修复⽅案并不⼀定能满⾜你对程序的需求 ⽣命周期在编译时被静态确定 ⽣命周期在运⾏时不能被改变 Rust 借⽤检查器假设所有代码路径都能被执⾏,所以总是选择尽可能短的⽣命周期赋给变量 尽量避免重借⽤⼀个独占引⽤为共享引⽤,不然你会遇到很多麻烦 重借⽤⼀个独占引⽤并不会结束其⽣命周期,哪怕它⾃身已经被 drop 掉了 每个语⾔都有其陷阱 for <'a,T> fn()->&'a T 签名的函数⽐ for <T> fn()->&'static T 签名的函数要更灵活,并且泛⽤于更多场合。

2024/10/8
articleCard.readMore

Codeforces Round 972 (Div. 2)

A. Simple Palindrome 大致题意 只允许使用 aeiou 构建一个字符串,使得其中的回文子序列尽可能少 思路 注意是回文子序列,所以比如 aeioua 这种,看起来一个回文串都没有,实际上 aea, aia, aoa 等等都是可以构造出来的 显然,如果存在间隔的方式,那么带来的回文串一定会更多,毕竟 aea 里面还可以再提取出一个 aa。所以应该保证尽可能不要出现间隔字母即可,同时联系的字母也要少 AC code 1 2 3 4 5 6 7 8 void solve() { int n; cin >> n; int cnt[5] = {0, 0, 0, 0, 0}; for (int i = 0; i < n; ++i) ++cnt[i % 5]; for (int i = 0; i < 5; ++i) for (int j = 0; j < cnt[i]; ++j) cout << "aiueo"[i]; cout << endl; } B2. The Strict Teacher 大致题意 有一排方格,其中一部分格子上有老师,老师希望抓住某个学生,且老师和学生每次都会停留在当前的格子或者走到相邻的格子,给出老师的初始位置,询问如果学生的初始位置在某个值时,需要多久才能抓到 思路 只需要考虑在所有老师最左边或者在最右边,或者在某两个中间这三种情况即可,简单题 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int n, m, q; cin >> n >> m >> q; vector<int> data(m); for (auto &item: data) cin >> item; sort(data.begin(), data.end()); while (q--) { int t; cin >> t; if (t < data[0]) cout << data[0] - 1 << endl; else if (t > data.back()) cout << n - data.back() << endl; else { auto iter = upper_bound(data.begin(), data.end(), t); int r = iter.operator*(); --iter; int l = iter.operator*(); cout << (r - l) / 2 << endl; } } } C. Lazy Narek 大致题意 有 $n$ 个字符串,每个字符串长度为 $m$,允许选择其中几个(或者一个都不选),并按照原来的顺序拼接起来, 然后再从拼接后的字符串提取出一个子序列,使得这个子序列恰好是多个连续的 narek 这个字符串 将此子序列的长度减去提取走子序列后的 narek 字母数量,得到分数,问分数最大是多少 思路 对于每一个字符串,当需要使用这个字符串的时候,当前状态只有 5 种可能的开始,即当前需要匹配 n/a/r/e/k 的时候,同时也只有 5 种结束状态 所以只需要枚举每一个字符串在不同的字母结束的时候的最优分数,然后做一下动态规划即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void solve() { signed _; cin >> _; for (signed tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; string str = "narek"; vector<string> data(n); for (auto &item: data) item.reserve(m); for (auto &item: data) cin >> item; vector<vector<int>> dp(n); for (int i = 0; i < n; ++i) { dp[i].resize(5, -1000000); for (int k = 0; k < 5; ++k) { int cur = k, soc = 0; for (auto &c: data[i]) { if (c == str[cur]) { ++cur; if (cur == 5) { soc += 5; cur = 0; } } else if (c == 'n' || c == 'a' || c == 'r' || c == 'e' || c == 'k') --soc; } if (k == 0) dp[i][cur] = max(dp[i][cur], soc); if (i > 0) { dp[i][k] = max(dp[i - 1][k], dp[i][k]); dp[i][cur] = max(dp[i - 1][k] + soc, dp[i][cur]); } } } int ans = 0; for (int j = 0; j < n; ++j) for (int i = 0; i < 5; ++i) ans = max(ans, dp[j][i] - i); cout << ans << endl; } }

2024/9/16
articleCard.readMore

《黑神话:悟空》游玩评测

开篇 首先还是恭喜悟空在发售后四天内完成了 1000万份的销售 在经历了 73 个小时的游玩之后,最终达成了九九八十一难的全成就,虽然没有完成二周目的探索,但是也算是经历了几乎全部的游戏内容 个人评价 石刻 首先是非常巧的一点,在今年 6 月底的时候,恰好去到了大足石刻现场,也拍了不少照片,这里放一张大足的千手观音照片 所以在看到黑神话第三章开始的大片的石雕的时候,也感受到了一种非常熟悉的感觉。和现场不同的一点是,现场的很多石刻,几乎没有太多的保护措施, 以至于漆画掉落严重,但是游戏中似乎都被比较好的进行重新上色了 可以说在建模方面(虽然大部分应该都是扫描的)确实做了不少功夫 购买之前 接着来说说游戏本身。我并不是第一时间购买游戏的玩家,相反,我直到 23 号晚上才购买下游戏,且直到 24 号才开始游玩,有两个方面 游戏本身被认为是是更接近于魂系游戏,而我个人并不是对这类游戏很感兴趣 国产游戏带来的失望太多,更何况是一个从 7 个人的小团队开始开发的游戏。个人并不希望无脑的对国产游戏而进行购买,反而是希望能够将自己的支持有意义 最后决定购买的核心因素,一方面确实没有出现与最初的预告片的效果差距较大的心理落差的讨论,另一方面则是整体的游戏难度和一般的魂系游戏而言,确实较为简单 游玩评测 画面与地图设计(9.5/10) 游戏的画面确实值得相当高的评分,无论是森林、沙地、雪山等等的画面确实值得评价为难以挑剔,在第三章的小西天土地将天命人带去湖上的过程,一路的雪景确实给我带来了不小的震撼 整体的地图设计也相对合理,虽然游戏没有给出地图,但是在游戏内容的指引还是相当清晰的,特别是在主要路线上安置了相当多的火盆火把用来提示路线 但是有个别地方还是缺少一些对于无地图的游戏的设计,比如在花果山的时候拿到筋斗云之后,反而不知道应该去哪里,特别是当我拿到筋斗云之后乱飞了一阵子,就彻底迷了路,我甚至都不知道自己在哪里 有人说可以跟着雷声走,起码能找到一个 BOSS,但是我当时所在的地方完全听不到雷声,同时更可怕的是,我无法降落,也无法传送,只能退出游戏重新进入的方式来回到土地庙。而回到土地庙之后,本来应该依靠打完 BOSS 后的剧情朝向来了解下一步应该去哪,变成了完全不知道接下来应该去哪了 如果说前 5 章的地图像是有过完整的地图设计,确实值得无地图的话,而第六章的地图可以说是完全没有设计,把所有的 BOSS 都堆积在一个圆形的大平台上一样 音乐音效(8.5/10) 音乐方面,可以很容易听出有不少的老板西游记电视剧里的音乐或者是变奏版本,这确实带来了不少好感,但是整体游玩过程中,音乐的体验似乎有点缺失,很难让你能分别出来,这个时候你应该注意安全,还是应该继续前进 在音乐方面一个非常典型的例子是《塞尔达传说荒野之息》中的沙漠地区,当 BOSS 吃到炸弹然后倒地的时候,背景音乐会立刻变奏来提醒你应该上前进攻了,或者 BOSS 瘫痪了。 故事情节(8.5/10) 如果这个游戏是一个纯粹的魂系游戏,那我会给故事情节打 9.5 分甚至更高,但是既然制作者都说这不是魂系游戏了,那我只能给出 8.5 的评分 也许可能是我很小的时候就没有再看《西游记》了,其中对很多很多的故事情节反而没有任何印象,难以和游戏情节搭上,比如猪八戒什么时候和蜘蛛精有过关系了?我好像没有印象,再比如牛魔王的女儿萍萍有什么什么故事情节,猪八戒又什么时候杀了她的家人?我好像也没有印象,反而让人更加困惑 甚至红孩儿什么时候变成了夜叉?我越来越疑惑,在一路疑惑中继续打下去,似乎也没有给出太多的答案 当然你也可以说我没有认真看过《西游记》不知道故事情节,但如果这不是一部粉丝向作品的话,那我只了解其中的大致情节似乎也情有可原 游戏性(9/10) 游戏性方面基本没有太多令人不舒服的地方,无论是技能加点还是四个技能 + 两个饰品的效果,也没有太多值得特别称赞的,所以还是可以给一个中规中矩的 9 分 最终评价 个人而言,我会给黑神话打 9.0/10 的分数,整体上也是一块非常出色的 3A 游戏,但是作为一个只需要 48 个小时就能在无跳剧情的情况下通关一周目全部支线任务和主线剧情,整体内容还是欠了一些,似乎地图和画风有了 268 的价位,但是内容却稍有不值这个价位。但是整体上还是属于值得购买的水平

2024/9/8
articleCard.readMore

Golang 踩坑 —— interface 为参数的时候传 nil 指针

问题 这两天踩了一个奇怪的坑,抽出核心逻辑可以得到这样一段代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type V interface { } type T struct { Vnext V } func NewT(t V) *T { return &T{next: t} } func TestName(t *testing.T) { var tmp *T = nil newT := NewT(tmp) if newT.next == nil { t.Log("newT.next is nil as expected.") } else { t.Errorf("newT.next should be nil, but got %v", newT.next) } } 此时,输出的内容是:newT.next should be nil, but got <nil> 是不是挺疑惑的,稍做修改,将 var tmp *T = nil 改成 var tmp V = nil 此时,运行得到的结果是:newT.next is nil as expected. 原因 最终在 Google Groups 上找到了相关说明: I’m trying to understand why a nil pointer when converted to an interface produces a non-nil value. Because different nil pointers can have different types, and the interface remembers the type of the (nil) pointer (that it is converted from): that remembering means that the interface value isn’t nil. Is this a bug? No. (It’s a mild confusion based on the overloading of nil to mean the zero value for pointers of any type and for interfaces — it’s not obvious from the text of a program that the nils are of different types.) Chris 简单来说就是为了满足类似 C++ 的 RTTI 的特性,因为转为 interface 必然会丢失掉原来的类型信息,需要保存下原来的类型 这就导致了一个具体的变量传递给一个 interface 参数的函数的时候,因为会丢失掉原始的类型,所以将其包装成一个特殊的 struct。我们可以用 unsafe 的方式来获取到相关的信息 因为样例的 interface 在 golang 中使用类似如下的结构进行存储 1 2 3 4 type eface struct { _type *_type data unsafe.Pointer } 所以我们可以使用如下方案提取具体的变量值: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type InterfaceStruct struct { pt uintptr pv uintptr } type V interface { } type T struct { Vnext V } func NewT(t V) *T { return &T{next: t} } func TestName(t *testing.T) { var tmp *T = nil newT := NewT(tmp) pointer := *(*InterfaceStruct)(unsafe.Pointer(&newT.next)) fmt.Println(pointer) } 就可以得到执行结果为 {4309258112 0} 也就是实际上 data 字段确实是 0,也就是 nil,但是其类型则存在一个 _type 的指针用来描述,所以在程序层面又不能说是 nil

2024/7/28
articleCard.readMore

用 magic 变量解决 UAF 问题

最近学到了一个很有意思的方法解决 UA(Use-After-Free) 的问题,示例代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class A { public: explicit A(int x): _magic(0x41), a(x) {} ~A() { _magic = 0xdead; } void print() const { assert(_magic == 0x41); cout << a << endl; } private: unsigned _magic; int a; }; int main() { auto a = new A(10); a->print(); delete a; a->print(); } 简单来说就是在定义类的时候,增加一个 magic 的变量,用于记录当前的变量是否已经被释放了 同时使用了 assert 在每一个方法内判断一下是否正在执行被释放的代码 如果被释放了(示例中的代码),此时就会提示 Assertion failed: (_magic == 0x41), function print, file main.cpp, line 16. 其中可以注意到使用了 0x41 作为 magic 的默认值,也是为了解决 CPP 没有 RTTI 的问题,因为其恰好是 A 这个字母

2024/7/10
articleCard.readMore

Codeforces Round 942 (Div. 2)

A. Contest Proposal 大致题意 有两个数组 $a, b$,已经从小到大排序好了,现在往 $a$ 数组最前面再塞入几个值,同时从最后面删除相同数量的值,使得 $\forall i \in [1, n], a_i \leq b_i$ 思路 简单题,由于数据量很小,甚至可以暴力 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int n; cin >> n; vector<int> a(n), b(n); for (auto &i: a) cin >> i; for (auto &i: b) cin >> i; int l = 0, r = 0, ans = 0; while (l < n && r < n) { if (a[l] <= b[r]) { ++l; ++r; } else { ++ans; ++r; } } cout << ans << endl; } B. Coin Games 大致题意 有两个人做游戏,有几个英镑再桌面上,有些正面朝上有些背面朝上。 每次操作,允许移走一个正面朝上的,然后连续选择两个剩下的影片进行翻转 问谁会操作到最后一次 思路 翻转两个硬币等于没有翻转 AC code 1 2 3 4 5 6 7 8 9 10 void solve() { int n; cin >> n; string str; str.resize(n); cin >> str; int cnt = 0; for (const auto& c: str) cnt += c == 'U'; cout << (cnt % 2 ? "YES" : "NO") << endl; } C. Permutation Counting 大致题意 有 $n$ 种卡片,每种都有一定数量,现在允许额外再增加 $k$ 张,使得这些卡片可以组成一个数组,数组种的存在的 $[1, n]$ 的排列的子串尽可能多,问可以有多少个 思路 只需要 $1, 2, 3, \dots, n, 1, 2, 3, \dots n$ 类似这样排列即可,通过二分找出每个数值都能到达的数量,然后排列起来 然后是剩下的那部分,比如多了一个 $1$,那么按照上面的排列方式,将 $1$ 放在最后面还能再多一次,即每有一个多出来的种类,就能增加一个子串 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int n, k; cin >> n >> k; vector<int> data(n); for (auto &i: data) cin >> i; auto check = [&](int x) { int use = 0; for (const auto &v: data) { if (v < x) use += x - v; if (use > k) break; } return use <= k; }; int l = 0, r = 1e15; while (l + 1 < r) { int mid = (l + r) >> 1; if (check(mid)) l = mid; else r = mid; } int ans = 0, use = k; for (const auto& v: data) { if (v > l) ++ans; else use -= l - v; } ans = min(n, use + ans); cout << ans + (l - 1) * n + 1 << endl; } D1. Reverse Card (Easy Version) 大致题意 给出 $n, m$,求满足条件的 $a, b$ 对 $1 \leq a \leq n, 1 \leq b \leq m$ $(a + b) \space mod \space b \times gcd(a, b) = 0$ 思路 假定 $a = x \times y, b = x \times z$,且 $gcd(y, z) = 1$ 则可以得到 $$ & (a + b) \space mod \space b \times gcd(a, b) = 0 \\\rightarrow & x \times y + x \times z = t \times (x \times z \times x) \\\rightarrow & y + z = t \times x \time z \\\rightarrow & 1 + \frac{y}{z} = t \times x \\$$ 容易得到,必然 $\frac{y}{z}$ 是整数,而 $gcd(y, z) = 1$,所以 $z = 1$,故 $1 + y = t \times x$ 所以很容易得到公式进行计算 AC code 1 2 3 4 5 6 7 8 9 10 11 #define int long long void solve() { int n, m, ans = 0; cin >> n >> m; for (int i = 1; i <= n && i <= m; ++i) { int my = n / i; ans += (my + 1) / i; } cout << ans - 1 << endl; } D2. Reverse Card (Hard Version) 大致题意 给出 $n, m$,求满足条件的 $a, b$ 对 $1 \leq a \leq n, 1 \leq b \leq m$ $b \times gcd(a, b) \space mod \space (a + b) = 0$ 思路 假定 $a = x \times y, b = x \times z$,且 $gcd(y, z) = 1$ 则可以得到 $$ & b \times gcd(a, b) \space mod \space (a + b) = 0 \\\rightarrow & x \times z \times x = t \times (x \times y + x \times z) \\\rightarrow & x \times z = t \times (y + z) \\\rightarrow & x \times = \frac{t}{z} \times (y + z)$$ 容易得到,必然 $\frac{y}{z}$ 是整数,而 $gcd(y, z) = 1$,所以必然只能用 $t$ 来承接除过来的 $z$,即上述公式中的表达 所以可以根据公式得到,只需要找到合理的互质数 $y, z$,即可找出有多少个 $x$ 满足条件,因为 $t$ 可以是任意值,即 $x$ 是 $y + z$ 的倍数 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define int long long void solve() { int n, m, ans = 0; cin >> n >> m; if (n < 2 || m < 2) { cout << 0 << endl; return; } function<int(int, int)> gcd = [&](int a, int b) { return b == 0 ? a : gcd(b, a % b); }; for (int i = 1; i * i <= n; ++i) { for (int j = 1; j * j <= m; ++j) { if (gcd(i, j) != 1) continue; ans += min(n / i, m / j) / (i + j); } } cout << ans << endl; }

2024/5/5
articleCard.readMore

Codeforces Round 941 (Div. 2)

从本篇开始,代码仅包含核心逻辑部分,多组数据的逻辑也将不再包含 A. Card Exchange 大致题意 有开始的 $n$ 张牌,每张牌有点数,可能相同也可能不同 允许进行如下操作:选择 $k$ 张相同的牌,弃掉,然后再摸进来 $k-1$ 张任意点数的牌 问最少可以剩下多少张牌 思路 每次操作就可以整来 $k - 1$ 张的任意牌,所以只需要将任意牌单独计数,暴力循环找可以进行操作的点数,在包含任意牌的情况下,是否可以进行操作,能操作就操作 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k, cnt[101] = {}; cin >> n >> k; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++cnt[tmp]; } int tot = 0; while (true) { bool flag = true; for (int i = 1; i < 101; ++i) { if (cnt[i] + tot >= k) { if (k - cnt[i] % k <= tot) { tot -= k - cnt[i] % k; cnt[i] += k - cnt[i] % k; } flag = false; tot += (cnt[i] / k) * (k - 1); cnt[i] %= k; } } if (flag) break; } for (int i: cnt) tot += i; cout << tot << endl; } } B. Rectangle Filling 大致题意 有一个矩阵,每个位置有黑色和白色两种情况,允许进行如下操作 在矩阵里选择两个相同的颜色的节点,将这两个节点组成的矩阵内的所有颜色变成和这两个节点的颜色相同 问是否能够通过任意次数的操作,使得整个矩阵变成完全相同的颜色的矩阵 思路 容易得出如下结论: 若一个矩阵的一个对角顶点颜色相同,则必然可以通过一次操作完成 若一个矩阵的一条边的两端颜色相同,且对边存在一个点的颜色和这两端相同,则可以通过两次操作完成 其他情况均不可能 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<string> mp(n); for (auto &i: mp) { i.resize(m); cin >> i; } if (mp[0][0] == mp[n - 1][m - 1] || mp[n - 1][0] == mp[0][m - 1]) { cout << "YES" << endl; continue; } bool flag = false; if (mp[0][0] == mp[n - 1][0]) for (int i = 0; i < n; ++i) if (mp[0][0] == mp[i][m - 1]) flag = true; if (mp[0][m - 1] == mp[n - 1][m - 1]) for (int i = 0; i < n; ++i) if (mp[0][m - 1] == mp[i][0]) flag = true; if (mp[0][0] == mp[0][m - 1]) for (int i = 0; i < m; ++i) if (mp[0][0] == mp[n - 1][i]) flag = true; if (mp[n - 1][0] == mp[n - 1][m - 1]) for (int i = 0; i < m; ++i) if (mp[n - 1][0] == mp[0][i]) flag = true; cout << (flag ? "YES" : "NO") << endl; } } C. Everything Nim 大致题意 有 $n$ 个石头堆,Alice 和 Bob 玩游戏,轮流进行取石子, 每次取的时候,必须选择一个 $k$,满足现在有石头的石头堆中,石头数量最少的那个堆也有 $k$ 个石头 然后在每一堆里都同时取走 $k$ 个石头 谁最后没办法取石头,谁就输了,问最后谁赢了 思路 首先,每次从所有堆里进行删除,可以等价将所有的石头堆排序后,变成差值堆,且必须从第一个石头堆开始取 那么问题就变成了:有一组石头堆,必须从第一个石头堆里开始取,每次可以取任意个数,问最后谁会取最后一次 这就很简单了,因为如果这个石头堆是 $1$ 个,那么大家都没得选,就是交换一下先后手,但是如果不是一个,那么就必然此时操作的人,可以是否要先手还是后手了, 因为此时他可以选择取到只剩下一个或者一个都不剩下,就可以实现交换先后手 所以核心是谁拿到了第一个先后手交换权,谁就能操纵整个游戏 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; sort(data.begin(), data.end()); if (data[0] != 1) { cout << "Alice" << endl; return; } int cnt = 1; for (int i = 1; i < n; ++i) if (data[i] - data[i - 1]) { if (data[i] - data[i - 1] == 1) ++cnt; else { ++cnt; break; } } cout << (cnt % 2 ? "Alice" : "Bob") << endl; } D. Missing Subsequence Sum 大致题意 需要构造一个数列,满足 对于 $k$,无法从数组中找到任何一个子序列,使得序列之和等于 $k$ 对于 $i \in [1, n] \space and \space i \neq k$,必定从数组中找到任何一个子序列,使得序列之和等于 $k$ 思路 如果不考虑不能构造 $k$ 的情况,其实相当简单,即都是 $2^x$ 即可,即二进制上考虑 接下来是考虑如何构造不出 $k$。首先假定一下 $k > 1$,后面再单独讨论 $1$ 的情况 那么必然可以分为两个部分 $[1, k - 1]$ 和 $[k + 1, n]$ 我们很容易得到这样一个结论:假如可以组成 $1$ 和 $k - 1$ 的话,如果组合一下必然可以得到 $k$,如果要组合不出来, 必定 $k - 1$ 中包含了 $1$ 的必要元素,同理,$k - 2$ 也可以如此推理,这样可以得到非常多的关系链 但是这些关系链重要吗,并不,因为太多太乱太无意义了,但是反而可以得到这样一个结论:如果所有小于 $k$ 的值之和是超过 $k$ 的,那么很容易能够找到一个组合,可以得到 $k$ 所以只需要背着这条结论走即可,即刚好将比 $k$ 小的所有值加起来都比 $k$ 小即可,这样就可以构造出 $[1, k - 1]$ 的全部值了, 然后再加入一个 $k + 1$ 即可完成后面部分 不过稍微可能需要注意的需要再加入一个值,因为 $k$ 本身被排除在外了,所以这可能会导致原来依赖 $k$ 的值无法组成, 例如,如果 $k = 4$,那么 $6$ 就是一个很难组成的值,因为唯一包含第 $3$ 个比特位是 $1$ 的值就被干掉了,所以需要补充一个来避免这个问题 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int n, k, tot = 0, nxt = 1; cin >> n >> k; vector<int> ans; ans.reserve(25); bool flag = true; while (tot < n) { if (flag && tot + nxt >= k) { ans.push_back(k - tot - 1); ans.push_back(k + 1); ans.push_back(k + (nxt << 1)); flag = false; } else ans.push_back(nxt); tot += nxt; nxt <<= 1; } cout << ans.size() << endl; for (int i = 0; i < ans.size(); ++i) cout << ans[i] << " \n"[i == ans.size() - 1]; }

2024/5/3
articleCard.readMore

Codeforces Round 940 (Div. 2) and CodeCraft-23

A. Stickogon 大致题意 有 $n$ 根木棍,问最多可以构成多少个等边的多边形,要求每一条边只能用一根木棍 思路 构建成三角形就行,统计一下,每种边的数量整除 $3$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; map<int, int> mp; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++mp[tmp]; } int ans = 0; for (auto &[a, b]: mp) ans += b / 3; cout << ans << endl; } } B. A BIT of a Construction 大致题意 给出一个整数 $k$,要求构造一个数组,其长度为给出的 $n$,每一项都不是负数,且满足 $\sum^n_{i=1} a_i = k$ 问如何使得整个数组的 $a_1 | a_2 | \dots | a_n$ 的数中,比特位为 $1$ 数量最多 思路 由于求和为 $k$,且每一项都不是负数,那么必然可以得到所有值都比 $k$ 小,最大起码也是等于 而题目要求比特位为 $1$ 尽可能多,而尽可能多的值必然是 $2^x - 1$,所以找最大的 $x$ 使得 $2^x \leq k$,然后剩下的数值不重要了,补充满 $k$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; if (n == 1) { cout << k << endl; continue; } int a = 0; for (int i = 31; i >= 0; --i) { if (k >= (1 << i) - 1) { a = (1 << i) - 1; break; } } cout << a << ' ' << k - a; for (int i = 2; i < n; ++i) cout << ' ' << 0; cout << endl; } } C. How Does the Rook Move? 大致题意 有一个棋盘,现在黑白两方轮着下,其中玩家使用白棋,电脑使用黑棋 电脑下棋的位置固定是根据用户下的位置的相反位置,例如玩家这一步下了 $(i, j)$,那么电脑则会下 $(j, i)$。 如果 $i = j$,那么就跳过电脑的回合 现在下的每一个棋都是城堡(类似中国象棋中的车),需要保证任何一步下的位置都必定不会被出现相互吃的情况,且目前已经有几个位置已经下好了,问剩下的还有几种可能下法 思路 容易猜出这是一个递推的题,类似斐波那契数列。当然可以仔细来看 首先,已经下了几步这件事是无意义的,因为去掉已经下的那些行/列,就会回到一个普通的没有下的棋盘, 说白了就是个干扰项,只需要把给出的棋盘大小减去已经下过的位置的行数,就可以得到新的棋盘行数 同样的,不仅是已经下的位置,你现在下的位置也是如此,一旦下好了+电脑下好,再把下好的那几行/列删掉,就是一个新的空棋盘,所以这是一个递推 接下来是如何得到递推公式了,因为下哪一行都一样,删掉之后就是空白的,且根据要求,每一行必定有一个城堡,且求算的总数并不关系下的顺序,只看最后的样子, 那么我们可以只考虑第一行(因为第一行必定有一个城堡,可能是白的也可能是黑的) 如果第一行,我下了最左上角的位置,那么就会得到一个 $n - 1$ 的棋盘($i = j$,机器人没有地方下) 如果第一行,我下了不是第一个位置,那么机器人必定会下对角线位置,即得到一个 $n - 2$ 的棋盘。因为这样的位置有 $n - 1$ 个, 且第一行可能是黑的也可能是白的,所以递推公式就是 $a_n = a_{n-1} + 2 \times (n - 1) \times a_{n - 2}$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define int long long void solve() { vector<int> ans(3e5 + 10); ans[0] = 1; ans[1] = 1; constexpr int mod = 1e9 + 7; for (int i = 2; i < ans.size(); ++i) { ans[i] = (ans[i - 1] + (ans[i - 2] * (i - 1) * 2) % mod) % mod; } int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; set<int> st; for (int i = 0; i < k; ++i) { int u, v; cin >> u >> v; st.insert(u); st.insert(v); } cout << ans[n - st.size()] << endl; } } D. A BIT of an Inequality 大致题意 给出一个数组,要求找到一个元祖 $(x, y, z)$,满足 $0 \leq x \leq y \leq z \leq n, f(x, y) \oplus f(y, z) > f(x, z)$ 其中 $f(l, r) = a_l \oplus a_{l+1} \oplus a_{l+2} \oplus \dots \oplus a_{r}$ 问有多少个不同的元组 思路 很容易得到,$f(x, z) \oplus a_y = f(x, y) \oplus f(y, z)$,就是再异或上一边 $a_y$ 能让值变大 那么必然,原来 $a_y$ 中,最大的为 $1$ 那个比特位,$f(x, z)$ 为 $0$,毕竟如果是 $1$ 的话,肯定就变小了 接下来从这个角度分析,由于 $x \leq y \leq z$,所以 $f(x, z)$ 中一定已经异或过一次 $a_y$ 了, 而已知一个比特位 $a_y$ 是 $1$ 但是 $f(x, z)$ 是 $0$,那么必然在 $[x, z]$ 中,这个比特位为 $1$ 的,出现了偶数次,且至少 $2$ 次 所以只需要对于每一个可能的 $y$,找这个 $y$ 最大的为 $1$ 的那个比特位,为 $1$ 的次数恰好为偶数,且包含 $y$ 的区间数量即可。我采用了双向的奇偶标记 例如第三个例子,可以得到如下的表格 indexorigin2^2forwardback2^1forwardback2^0forwardback 071oddeven1oddeven1oddeven 130oddodd1evenodd1evenodd 271evenodd1oddeven1oddeven 320eveneven1evenodd0oddodd 410eveneven0eveneven1evenodd 这个表格的制作方式: 先计算出每个值的每个比特位,放在 $2^x$ 列上 单独计算每一列 forward,从上往下走,初始值为 even,如果当前的 $2^x$ 是 $1$,则将前一个 forward 翻转后填入,反之则超过来 然后单独计算每一列 back,从下往上走,初始值为 forward 最后的值,填入逻辑同上 然后统计某个位置,左边的 back 下不同类型的数量和右边的 forward 不同的类型数量,再做乘法即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; vector<bool> flag[32][2]; int cntR[32][2] = {}, cntL[32][2] = {}, ans = 0; bool cf[43] = {}; for (auto &i: flag) i[0].resize(n); for (auto &i: flag) i[1].resize(n); for (int v = 0; v < n; ++v) { for (int i = 0; i < 32; ++i) { if (data[v] & (1ll << i)) cf[i] = !cf[i]; flag[i][0][v] = cf[i]; ++cntR[i][cf[i]]; } } for (int v = n - 1; v >= 0; --v) { for (int i = 0; i < 32; ++i) { if (data[v] & (1ll << i)) cf[i] = !cf[i]; flag[i][1][v] = cf[i]; } } for (int v = 0; v < n; ++v) { for (int i = 0; i < 32; ++i) ++cntL[i][flag[i][1][v]]; for (int i = 31; i >= 0; --i) if (data[v] & (1ll << i)) { ans += cntL[i][0] * cntR[i][0] + cntL[i][1] * cntR[i][1]; break; } for (int i = 0; i < 32; ++i) --cntR[i][flag[i][0][v]]; } cout << ans << endl; } }

2024/4/27
articleCard.readMore

日本旅游杂记-东京篇

旅游计划前的闲聊 一直都在计划去日本旅游,不过因为各种原因搁置了,最近也打算趁着发年终奖了,计划去日本旅游一趟。 想着把旅游期间遇到的问题和计划也分享在这,可以作为其他想要去日本旅游的人的参考。当然,这是自由行的计划,如果是跟旅行团的话,还是乖乖听旅行团的安排吧 为什么考虑去日本旅游呢 我个人觉得旅行分为两类一类是人文旅游,一类是自然旅游 人文旅游需要去那些和自己日常生活习惯完全不同的国家地区,去体验不同的人文,需要注意减少遇到和自己来自同一个地区的人 自然旅游需要去体验自然风光,那必须人越少越好,无论是不是和你来自同一个地区 个人感觉目前自然旅游最成功的一次应该就是之前去甘南地区,也许以后有时间我可以来分享一下这段行程,而人文旅游最好的办法就是出国了 核污水问题。这导致去日本旅游的人变少了,简简单单,就看你信不信日本的核污水排放期间的安全程度 日本是一个不免签的国家。这也就意味着但凡是有能力去日本旅游的,必定拥有一定的经济实力,相对而言遇到的国人素养更高一些,当然也更少一些 日本的汇率在跌。这对去日本旅游而言可能会有一些便宜,当然这也是次要条件,毕竟现在日本也面临了输入型通货膨胀,实际成本也不能确定到底是涨没涨 旅行前的准备 这块内容主要写在旅行之前,我做出的准备,当然完整的文章会在旅行结束后再发布,但是这块内容我尽量不再去做修改,而是在后面进行补充说明 请务必注意,本段完全在旅行之前编写,不保证可信度,补充说明会在旅行结束后下面进行补充,请务必阅读完整 文书准备 护照 前往日本旅游需要一本有效期至少还有 6 个月的护照,这个难度应该不是很高,去出入境管理机构办理即可,我记得我的护照大概用了一周的时间寄到了, 但是还是要留一些时间,毕竟要求只是 7 个工作日办理完成 护照需要身份证就可以办理了,一寸照片都是要求现场拍摄的。 另外需要你办理所在地,你需要拥有此地区居住证或者是暂住证,并不需要户口本其实也可以办理的,至少我没带户口本去也能办 护照当然是越早办理越好,一般成年人的护照有效期都是 10 年,只要你的旅游计划是最近 9 年内的,那就没有问题 签证 日本目前还不是免签地区,所以还是需要申请签证的,不过日本的旅游签证办理还算简单 有关此类签证手续,需通过取得了日本国驻华使领馆(请确认各使领馆的管辖地区 )送签资格的指定旅行社进行申请。故申请人无法直接去使领馆办理及领取此类签证。 via. 日本国驻华大使馆文件 根据文件描述,旅游签证必须要由旅行社代为办理,不能直接去大使馆申请,必须去找一家旅行社去办理 为了方便起见,我还是直接选择一些旅游平台,比如飞猪、携程等进行办理,由于个人有一些奇怪的洁癖,各种互联网需求只选择一家平台且不比价,考虑到淘宝上就可以直接搜索到飞猪的商品,于是决定使用淘宝+飞猪的组合,包括后面全部的网络预订均选择了飞猪的服务 以上海的领事馆为例,在飞猪上搜索日本签证,就可以得到以下的费用情况 类型有效时间逗留时间价格一般年薪(税后)要求 单次旅游签证90天15天199100k 年收入 具有足够经济能力人士的多次旅游签证3年30天435200k 年收入 相当高收入人士的多次签证5年90天458.9500k 年收入 相关材料可以在 此处 拿到,当然具体要什么,还是得听旅游社的,比如年薪要求。不过如果是在校大学生的话,似乎办理单次签证比较轻松 Visit Japan Web 这个是可能很多人并不知道的,地址在这里 这是日本入境前需要进行申请的地方,理论上你不填写也可以坐飞机,只不过在入关的时候还是要求你进行填写,如果提前在这里进行报备的话,入关会很快。 当然目前本人还没有体验过提前填写的好处,毕竟还没有出发 我也是从 这里 学到的,可以参考一下这位姐姐制作的视频,也很有价值 现金 根据经验而言,差不多一顿饭吃好一点的话,两个人可能需要 5000JPY,约等于 230CNY/30USD/250HKD/1000NTD 的样子,按照三日行程,需要 30K JPY 的样子, 当然听说很多地方都支持 Suica 卡或者使用 visa/PayPal/Mastercard 所以可能并不需要兑换太多的现金,可能更多是在一些小店铺进行购买, 故大概计划准备 10000 到 20000 左右的 JPY 吧,总归能找到不用现金吃饭的地方的吧 交通 市区交通 交通选择了了 Suica 卡,也就是 这个 当然没有那么麻烦,因为是 iPhone 用户,所以你可以直接在手机的钱包应用里找到这张卡,在国内就可以直接办理,非常快, 路径是:钱包 $\rightarrow$ 添加卡 $\rightarrow$ 交通卡 $\rightarrow$ SUICA 很方便是不是,按照 iPhone 的公交卡机制,再开启快捷交通后,就可以直接用摄像头那块区域进行刷卡了,就和用实体卡一样……希望如此吧,也得等我试试 机场交通 另外需要额外注意的一个问题是,离东京最近的机场是羽田机场,但羽田机场通常是承载国内航班的,我们一般是飞到了成田国际机场,假如想从成田机场前往秋叶原,则有 55km…… 这个距离还是稍微有那么一点点的可怕,目前也有三种交通方式 来自 JR 的N’EX 东京往返车票 京成成田 SKY ACCESS(京成 Access 特急) Skyliner 天空快线 这里考虑到达新宿的时间,如果是在上野、浅草、银座之类的地方就下车的话,应该会更快一些 N’EXSKY ACCESSSkyliner time cost1h27m1h25m1h11m money cost5,000JPY2,960JPY5,580JPY transferNO1 time1 time first time07:3705:4106:20 last time21:4423:0823:00 综合上看,还是坐 SKY ACCESS 比较值,时间也比较充裕,除了慢一点需要转车以外,基本就没太大缺点了。 另外,搭乘「京成 Access 特急」是不需要预约的!只要在抵达成田机场后于现场的自动售票机、售票中心购票,有 IC 票卡的更可以直接哔哔进站! 旅游地点 做了一张东京地图,标注了核心的一些旅游景点 涩谷 旅游地点 涩谷 SKY 预定地址 可以俯瞰整个东京地区,可以看到富士山、晴空塔、东京塔等景点 费用 2,200JPY,但是现场直接买票要 2,500JPY 日落很好看,需要提前很早很早在网上进行订票,不然是订不到日落时分时间进去的,只有下午场。 虽然并不限制什么时候出来,但是在里面待一个下午等日落还是有点…… 原宿地区 指原宿车站到表参道站一路的店铺,包括来自竹下通出口的一带,有非常多的小吃和店家 涩谷十字路口 全球人流量最大的十字路口 美食 極味屋 澀谷PARCO店 日本〒150-0042 Tokyo, Shibuya City, Udagawacho, 15−1 渋谷パルコ地下 1階 美式牛扒屋,有松露炒饭,自助烤肉 日常营业时间:11:30–23:00,周末营业时间:11:00–22:30 人群约 2000JPY+ LUKE’S LOBSTER 表參道貓街店 三明治专卖店 Micasadeco & Cafe Jingumae 咖啡甜点店,有松饼等 晚上不营业 菜单 ANAKUMA CAFE - Harajuku 熊熊咖啡厅,很好玩的咖啡店 单杯价格在 1,500JPY 左右 菜单 上野 旅游地点 上野恩赐公园 樱花季会比较好,非樱花季就比较尴尬了,可以进去逛逛 晴空塔(东京天空树)官网 要上去的话,价格还是比较贵的,提前至少一天预定费用是 2,700JPY,如果是当天预定的话需要 3,100JPY,成本还是稍微有一些高 不过有和隔壁的墨田水族馆合并的门票,价格为 4,700 JPY,组合票在这里 购买 需要注意组合票必须先去天空树,逛完才能进入水族馆 墨田水族馆 官网 墨田地区的水族馆,单人 2,500JPY 价格不算很高,可以接受,而且是莉可莉丝的取景地,还是挺值得一去 浅草寺 有着悠久历史寺庙,可以去参拜一下 记得收集一下御朱印 Sumida Park/隅田公园 沿河的一条,樱花季会比较好看 可以看到对面的晴空塔,如果不去晴空塔的话,这里观景也不错 美食 Numazuko Kaisho 1,500JPY 自助海鲜 Bou-ya Uenorokuchometen(房家ホルモン館 上野六丁目店) 烤肉店 有各种稀有部位的烤牛肉,比如牛肠、牛舌、 预期两个人的成本是 5,000JPY 可乐是梅汁口味的 新宿 旅游地点 美食 Afuri Karakurenai Shinjuku Subnade(AFURI辛紅 新宿サブナード) 拉面店,沾面 菜单 人群价格在 1100JPY 左右,相对廉价 筑地市场 美食 (驻地不能边走边吃哦) とんぼや Tonboya 碳烤鮪魚,500JPY 一串 Onigiri Marutoyo 周末休息的一家店 炸饭团 Yamachō/Shouro 玉子烧,很火爆 Matcha Stand Maruni TOKYO TSUKIJI 咖啡抹茶店 官网 抹茶单杯 650JPY YONEMOTO Coffee Shop Tsukiji New Shop 很有名的老店,推荐黑糖蜜咖啡 Saito Suisan(斉藤水産㈱) 生吃牡蛎,1400JPY 一个 Soratsuki 甜点店,有奶昔等 月岛/佃岛 美食 KYUEI MELON PAN(月島久栄) 面包店 菠萝面包 200JPY Moheji Hanare(月島もんじゃ もへじ はなれ) 文字烧 丰州 旅游地点 Tokyo Toyosu Manyo club(東京豊洲 万葉倶楽部) 超级公共浴室,泡澡 八楼可以免费泡脚,记得自带毛巾 美食 下午两点打烊的街区 海鮮丼 大江戶 豐洲市場內店 菜单与营业时间 人均约 2500JPY 14:30 打烊 濱風茶房 抹茶店,有甜点,比如栗子蛋糕(1000JPY) 18:00 打烊 越後屋 助五郎 烤鱼串 19:00 打烊 稍微有点贵 秋叶原 美食 肉屋橫丁 烤肉自助,和牛吃到饱 一个人 6980JPY 麻布台 旅游地点 teamLab Borderless: MORI Building DIGITAL ART MUSEUM 官网 美术馆,整个房间都是屏幕的艺术馆,艺术内容会和你的互动进行变化 门票很贵,日常一个人 4000JPY,节假日或者临近的日期容易涨到 4400JPY 甚至 4800JPY Hills House 33F 高空视野,可以近距离看到东京铁塔,还是免费的景点 美食 Pelican café 一家卖吐司的店,评分有高有低,排队比较久 椀もなか 花一会 18:00 关门 各种冲泡的调料,适合买回去当礼物 住宿 住宿可以考虑根据游玩路线制定,日本的酒店的房间和床都比较小 旅行路线推荐 路线考虑是从新宿出发,回到新宿的,所以不一定适合,也可以根据上面的地点自己进行拟定 新宿-涩谷-台场-丰州-月岛-筑地市场 早上从新宿出发 先乘坐 JR 山手线,到达原宿站 从原宿站的竹下口出 沿着出口方向一直向东步行到竹下口红绿灯处,然后再向南走,一路走到新宿站 LUKE’S LOBSTER 表參道貓街店 Micasadeco & Cafe Jingumae ANAKUMA CAFE - Harajuku 中午到达新宿站 極味屋 澀谷PARCO店 涩谷十字路口 乘坐崎京線到达东京电讯 前往台场海滨公园 台场海滨公园 下午再向北步行至丰州 Tokyo Toyosu Manyo club 濱風茶房 越後屋 助五郎 然后再西北方向,经过月岛 Moheji Hanare(月島もんじゃ もへじ はなれ) KYUEI MELON PAN(月島久栄) 傍晚到达筑地市场 とんぼや Tonboya Onigiri Marutoyo Yamachō/Shouro Matcha Stand Maruni TOKYO TSUKIJI YONEMOTO Coffee Shop Tsukiji New Shop Saito Suisan(斉藤水産㈱) Soratsuki 晚上可以选择在银座附近逛街 新宿-上野-押上-麻布台 早上从新宿出发 乘坐 JR 山手线,到达上野 上野恩赐公园 然后乘坐东京地下铁的银座线再前往浅草 浅草寺 隅田公园 中午回到上野地区解决午餐 Bou-ya Uenorokuchometen(房家ホルモン館 上野六丁目店) 乘坐地下铁银座线转都营浅草线前往押上 墨田水族馆 傍晚前往麻布台,并解决晚饭 Hills House 33F 椀もなか 花一会 现金 按照 5000 一顿来算,三天大概需要 30k 的日元,可以在各大银行进行兑汇,但是好像很难预约,需要想要去旅游的可以多找找别的渠道 电器 日本的民用电压统一为 100V,但是在日本东部地区(比如东京)是采用 50Hz,而西部地区(京都、大阪等)是 60Hz 这是在二战之后,由美国负责帮忙重建关西地区,而关东地区则由英国帮忙重建,所以导致了日本同时并存两种频率的供电系统。 当然对于频率而言影响并不大,现在绝大部分电器都支持了 50Hz-60Hz,但是电压方面需要额外关注一下, 因为国内使用的是 220V 的高电压,部分相对廉价的设备通常仅支持 200V-220V,如果有不足的话需要额外购买一些 另外的,日本采用的也是和国内相同的接口形状,所以如果转换插头的电压范围是满足日本的电压的情况下,就不需要额外购买了 旅行后的回顾 在东京游玩了三天之后,再加上经历了一大堆繁琐的事情之后,终于有时间来补充剩下部分了,这里将会主要聊聊旅行准备相关的事情 Visit Japan Web 这是非常方便的东西,可以代替你在飞机上有人发放的纸质入境申请,包括后续不少流程可以直接使用, 同时还有免税 QR code,当然我还没有用上,听说需要单次消费超过 5000JPY 才能使用 现金 在日本支付方式比起国内要方便多了,绝大部分店铺都支持刷卡、现金、PayPal,相当一部分的店铺还支持了 Alipay, 另外值得注意的一点,日本的交通卡(即上面提到的 Suica 卡)也是支持作为日常的消费使用卡片,支持率通常比 Alipay 还要高, 所以如果你没有一张 visa 或者 master card,那么可以考虑在 Suica 卡中多充值一些金额。不过听说还有很多支持 Apple Pay,但是我自己还没有尝试过 另外,在个别的店铺,是只支持现金支付的,虽然是仅支持现金,但是也能保证给你开出一张打印的小票,除了浅草寺里是没有小票也不支持电子支付,必须现金 交通 三大轨道交通公司 东京的交通非常的便利,便利到有点过于便利了 很多时候我觉得共享单车是一种非常好的解决城市内最后一公里的交通问题,因为地铁的站点密度并不是很高,通常两个站之间的距离可以达到 1-2km, 加上现在的人们通常并不喜欢乘坐公交车,所以必然存在着最后一公里的问题 但是对于日本而言,如果你只考虑乘坐轨道交通,那么就压根不存在最后一公里的问题,因为站点密度实在太高了,通常两个站之间的直线距离只有 500m 举个例子,这个是都营地下铁的东京运营图,可以再拿国内城市的地铁图来比较一下密度,当然看起来和北京比,似乎也差不多,看起来可能略多一些 但是整个东京拥有三家和都营地下铁一样规模的轨道交通运营公司,且这几家的公司甚至不共用站点 而图里的仅仅只是一家公司的站点,可见东京的轨道交通的发达程度 东京的轨道交通主要分为两大类,三家公司。分别是 JR东日本、东京地下铁、都营地下铁,其中后面两家统称为地铁,前者则俗称为 JR。虽然都是轨道交通, 但是 JR 和地铁则完全不同,比较典型的特点是:地铁的站点看起来都比较小,比较隐藏,而 JR 站点都有很大的规模,类似国内的地铁站 其中 JR东日本的标识非常容易认,即一个大大的 JR 字母,而东京地下铁则是蓝色的 M 字母构型,都营地下铁则是一片绿色的银杏叶 比如下图,是都营地下铁的浅草桥站的入口, 因为当时是大晚上,且忙于赶路,所以没有拍照,但是我在 Google Maps 中找到了入口。 如果不明说下面的图中有地铁站入口,你能很快发现吗 仔细关注图片中央的深红色衣服的人附近的一个门口上面的标识 大部分较小的地铁站点都比较隐藏,不论是东京地下铁公司的站点还是都营地下铁的站点,似乎都隐藏在很多店铺之间 而 JR 站则一般比较大,比如秋叶原站的入口 当然 JR 和地铁之间还有很多区别,例如地铁上不能吃东西,但是 JR 可以,再比如地铁有不少站点是在地下的,而 JR 则全都是地上轨道交通。 但是从价格、速度、体验上,我确实没有感觉出太多的区别 乘坐细节注意 需要值的注意的是,东京轨道交通从地上进站一路到站台,地面上都会划有方向,大部分情况下是靠左,也就是你需要靠左侧走楼梯进站。 虽然实际上靠左还是靠右都是可以的,但是毕竟是作为访客,还是非常推荐和当地人一样,尽量靠左走 另外,大部分东京的地铁进站都是纯楼梯的,没有扶手电梯,所以对于不打算定居在一个酒店的人而言,可能要经历比较大的负担——拎着行李箱走很多很多的楼梯 要注意由于东京的地铁是多个公司运营的,公司之间的站台不共享,这也就导致你即使全程使用地铁,也需要不断的走楼梯进行进站出站,这还是挺折腾人的 另外值得一提的是日本的扶手电梯,一定要站在左边!一定要站在左边!一定要站在左边!日本的不成文规定:左侧是用来站着的,右侧是用来走的, 所以如果你不小心站到了右侧,务必要走上去,不要堵着后面的人了 另外非常建议的事情是,下载 Google Maps,在日本使用的时候,便利程度和高德完全不是一个 level 的软件了, 它会告诉精确的告诉你应该进入哪个站,到哪个站台,接下来有哪些时间的班车你可以坐,每辆班车预期从本站出发的时间以及到达的时间、目前的延误时间, 甚至在地铁上还会通知你下一站要下车了 旅游地点 有不少旅游地点需要注意 原宿地区:这里的店铺都是 11 点才开门,唯一的一家 10 点开门的是中式餐厅。另外如果你需要特意去购买某些东西,反而建议早一点去,有很多人会提前排队 筑地市场:这里的店家关门都比较早,似乎下午就找不到什么开着门的店铺了 Hills House 33F:这里的 33F 是要从 B1 楼的特殊电梯才能进入,仔细找一下,有一个特殊的入口 另外现在并非完全免费,需要你在 33F 的咖啡店里至少购买一杯咖啡才能进入,价格不算贵,但是需要额外加上 500JPY,不过即使这样,总价也不高 另外此处的咖啡店只支持非现金支付,一般在那个特殊的电梯前,会有保安向你确认 其他相关 日本的红绿灯稍微有一些不一样,日本的红绿灯有这种特殊的计时,其是通过还有几个两点来表示总共还有多久, 比如总共 6 个光电,亮了 2 个,意味着当前颜色的灯还有 1/3 的时间,完全没有精确的时间,只有大概的比例 另外就是日本对智能马桶的执着有点过分,你甚至可以在地铁站的公共厕所内发现智能马桶……

2024/4/23
articleCard.readMore

Codeforces Round 939 (Div. 2)

A. Nene’s Game 大致题意 有一排士兵,按照 $1,2,3,4 \dots n$ 的顺序喊,每次喊道 $a_i$ 的位置就踢出队伍,问最后剩下几个人 思路 只需要关注第一个被踢出去的人就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int k, q, s; cin >> k >> q; cin >> s; for (int i = 1; i < k; ++i) { int tmp; cin >> tmp; } for (int i = 0; i < q; ++i) { int n; cin >> n; cout << min(n, s - 1) << " \n"[i == q - 1]; } } } B. Nene and the Card Game 大致题意 有一组牌,每种数字只出现在两张牌上 现在将这组牌打散后分给两个人,并进行游戏。游戏的每一轮,当前的出牌手需要出一张牌,如果这张牌上的数值的另外一张已经在场上了,那么就会获得 1 份 现在给你其中一个人的手牌,问最多可以得到多少分 思路 两边的牌是映射的,所以先手出一张,后手跟一张,这样是刚刚好的,所以先手只能赚到那些两张牌都在自己手里的分数 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; set<int> st; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; st.insert(tmp); } cout << n - st.size() << endl; } } C. Nene’s Magical Matrix 大致题意 有一个矩阵,现在允许每次往一行或者一列上覆盖写 $1, 2, 3 \dots n$ 的某一个排列,问最终的整个矩阵的求和是多少 思路 每个位置都能变成它的横坐标和纵坐标里的较大者,简单模拟即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; int ans = 0; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) ans += max(i, j) + 1; cout << ans << ' ' << 2 * n << endl; for (int i = n - 1; i >= 0; --i) { cout << 1 << ' ' << i + 1; for (int j = 0; j < n; ++j) cout << ' ' << j + 1; cout << endl; cout << 2 << ' ' << i + 1; for (int j = 0; j < n; ++j) cout << ' ' << j + 1; cout << endl; } } } D. Nene and the Mex Operator 大致题意 有一个初始数组,允许你每次选择一个子串,将其的每一个值变成这个子串的 $MEX$,问可以让整个数组的所有位置之和最大是多少 思路 注意这个数组最大只能是 18 个数值,所以可以随意暴力 容易得到,最终一定可以把选择的区间都变成和当前区间长度相同的那个值,比如区间长度为 $3$,那么最终这个区间可以变成三个 $3$ 首先通过 dp 计算出哪些区间要进行上面的操作,然后再递归构建即可 比如我通过一定手段能够构建 $0, 1, 2, 3, 4, x$,就能通过一次 $MEX$ 得到 $5, 5, 5, 5, 5, 5$, 这个时候如果我再去尝试构建 $0, 1, 2, 3, 4$ 就可以重复之前的构建过程了,重复构建前五个值,相当于递归两次即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void solve() { int n; cin >> n; vector<int> data(n), dp(n), dr(n); for (auto &i: data) cin >> i; dp[0] = data[0] >= 1 ? data[0] : 1; dr[0] = data[0] >= 1 ? -1 : 0; for (int i = 1; i < n; ++i) { dp[i] = dp[i - 1] + data[i]; dr[i] = -1; for (int j = i; j >= 1; --j) if (dp[j - 1] + (i - j + 1) * (i - j + 1) > dp[i]) { dp[i] = dp[j - 1] + (i - j + 1) * (i - j + 1); dr[i] = j; } if ((i + 1) * (i + 1) > dp[i]) { dr[i] = 0; dp[i] = (i + 1) * (i + 1); } } vector<pair<int, int>> ans; ans.reserve(2e5); auto zero = [&](int l, int r) { bool flag = true; for (int i = l; i <= r; ++i) if (data[i] == 0) flag = false; if (flag) ans.emplace_back(l, r); else { ans.emplace_back(l, r); ans.emplace_back(l, r); } for (int i = l; i <= r; ++i) data[i] = 0; }; function<void(int, int)> dfs = [&](int l, int r) { if (l == r) { if (data[l] == 0) return; ans.emplace_back(l, l); return; } dfs(l, r - 1); ans.emplace_back(l, r); ans.emplace_back(l, r - 1); data[r] = r - l; dfs(l, r - 1); }; auto f = [&](int l, int r) { zero(l, r); dfs(l, r); ans.emplace_back(l, r); }; vector<pair<int, int>> lr; for (int i = n - 1; i >= 0; --i) { if (dr[i] == -1) continue; f(dr[i], i); i = dr[i]; } cout << dp[n - 1] << ' ' << ans.size() << endl; for (auto &[a, b]: ans) cout << a + 1 << ' ' << b + 1 << endl; }

2024/4/21
articleCard.readMore

Codeforces Round 928 (Div. 4)

A. Vlad and the Best of Five 大致题意 给出五个字母,其中只有 A/B,问那个字母出现次数多 思路 简单题,统计一下就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { string str; str.resize(5); cin >> str; int cnt[2] = {}; for (auto &i: str) ++cnt[i - 'A']; cout << (cnt[0] > cnt[1] ? 'A' : 'B') << endl; } } B. Vlad and Shapes 大致题意 检查一个图案是不是正方形还是三角形 思路 三角形不好检查,检查正方形就行,即四个角落都是染色的即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<string> data(n); for (auto &i: data) i.resize(n), cin >> i; int d[4] = {0, n, 0, n}; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) { if (data[i][j] == '0') continue; d[0] = max(d[0], i); d[1] = min(d[1], i); d[2] = max(d[2], j); d[3] = min(d[3], j); } if (data[d[0]][d[2]] == '1' && data[d[1]][d[2]] == '1' && data[d[0]][d[3]] == '1' && data[d[1]][d[3]] == '1') cout << "SQUARE" << endl; else cout << "TRIANGLE" << endl; } } C. Vlad and a Sum of Sum of Digits 大致题意 计算 $1, n$ 之间的所有值,将其的每一个 10 进制的值相加后再相加得到的结果 思路 暴力即可,注意打表 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using namespace std; void solve() { vector<int> data(2e5 + 10, 0); for (int i = 1; i < data.size(); ++i) { data[i] = data[i - 1]; int t = i; while (t) { data[i] += t % 10; t /= 10; } } int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; cout << data[n] << endl; } } D. Vlad and Division 大致题意 给出 $n$ 个值,将其变成多个组,满足任意一个组内的任意两个值,满足他们两个值的任意比特位都不一样 思路 每个组里最多两个值,即必须是 $a_i ^ a_j = 0x7fffffff$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using namespace std; void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; map<int, int> st; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++st[tmp]; } int ans = 0; while (!st.empty()) { auto iter = st.begin(); int a = iter->first; if (iter->second == 1) st.erase(iter); else --iter->second; int b = (~a) ^ (1 << 31); iter = st.find(b); if (iter != st.end()) --iter->second; if (iter != st.end() && iter->second == 0) st.erase(iter); ++ans; } cout << ans << endl; } } E. Vlad and an Odd Ordering 大致题意 有 $1, 2, 3, \dots, n$ 个数,先从小到大取出所有的奇数排成一排, 然后再取出剩下值中的,满足是一个奇数乘上一个 $2$ 的值,从小到大排成一排 然后再取出剩下值中的,满足是一个奇数乘上一个 $3$ 的值,从小到大排成一排 依此类推,直到用完,问第 $k$ 个值是多少 思路 实际上没有 $3$ 的机会了,同样的也没有 $5$ 的机会了 其实就是二进制里,把最后一位是 $1$ 的取走,然后取倒数第二位是 $1$ 的,依此类推 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; int s = 1; while (true) { int t = (n + 1) / 2; if (k <= t) { break; } k -= t; n -= t; s <<= 1; } cout << (k * 2 - 1) * s << endl; } } F. Vlad and Avoiding X 大致题意 一个 $7 \times 7 的矩阵,开始的时候一些方格已经被染成黑色,要让中间不出现 X 形状的图案,问至少需要染白多少个方格$ 思路 暴力就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { vector<string> data(7); for (auto &i: data) i.resize(7); for (auto &i: data) cin >> i; constexpr int arr[3][2] = {0, 0, 1, -1, 1, 1}; int ans = 49; function<void(int)> dfs = [&](int s) { if (s >= ans) return; for (int i = 1; i < 6; ++i) for (int j = 1; j < 6; ++j) if (data[i - 1][j - 1] == 'B' && data[i - 1][j + 1] == 'B' && data[i + 1][j - 1] == 'B' && data[i + 1][j + 1] == 'B' && data[i][j] == 'B') { for (auto &ar: arr) { data[i + ar[0]][j + ar[1]] = 'W'; dfs(s + 1); data[i + ar[0]][j + ar[1]] = 'B'; } return; } ans = min(ans, s); }; dfs(0); cout << ans << endl; } } G. Vlad and Trouble at MIT 大致题意 有一棵树,有些节点上的人要播放音乐,有一些节点上的人要睡觉,有一些则无所谓 现在需要创建一些墙使得放英语的人不会吵到睡觉的人,音乐会随着树的边传播,墙只能创建在边上,问最少需要多少个墙 思路 树上搜索即可,如果当前节点是要播放音乐的,那么和它的所有要播放音乐或者无所谓的节点都可以连在一块,反之也一样。 但是如果是无所谓的人,那么就要看它的直接孩子节点中要播放音乐的多还是要睡觉的多了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<vector<int>> g(n); string str; str.resize(n); for (int i = 1; i < n; ++i) { int tmp; cin >> tmp; g[tmp - 1].push_back(i); } cin >> str; int ans = n - 1; function<int(int)> dfs = [&](int c) { if (g[c].empty()) { return str[c] == 'P' ? 1 : (str[c] == 'C' ? 2 : 0); } int cnt[3] = {}; for (const auto &n: g[c]) ++cnt[dfs(n)]; if (str[c] == 'P') { ans -= cnt[1] + cnt[2]; return 1; } else if (str[c] == 'S') { ans -= cnt[0] + cnt[2]; return 0; } else { if (cnt[0] > cnt[1]) { ans -= cnt[0] + cnt[2]; return 0; } else if (cnt[1] > cnt[0]) { ans -= cnt[1] + cnt[2]; return 1; } else { ans -= cnt[0] + cnt[2]; return 2; } } }; dfs(0); cout << ans << endl; } }

2024/4/20
articleCard.readMore

Codeforces Round 927 (Div. 3)

A. Thorns and Coins 大致题意 有一条路,有些地方有陷阱,有些地方有金币,每次只能向前走一步或者跳到第二步,在不踩到陷阱的情况下,最多可以收集多少金币 思路 最早出现连续两个陷阱的地方就是结束,统计前面的金币数量即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.resize(n); cin >> str; int cnt = 0; for (int i = 0; i < n; ++i) { cnt += str[i] == '@'; if (i > 0 && str[i] == '*' && str[i - 1] == '*') break; } cout << cnt << endl; } } B. Chaya Calendar 大致题意 有 $n$ 个预兆,每个预兆都有出现的周期 且如果出现了第一个预兆后出现了第二个预兆,且在这个第二个预兆后又出现了第三个预兆,以此类推,当出现最后一个预兆的时候,就是末日,问末日是哪一天 思路 不断找合法的倍数即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; int cur = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; int c = (cur + tmp) / tmp; cur = c * tmp; } cout << cur << endl; } } C. LR-remainders 大致题意 有一个数组,现在每次删除掉最左边和最右边的值,问此时剩余的所有值的乘积与 $m$ 的取模值,已知了删除顺序,问所有次的取模的值 思路 反向操作即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<int> data(n); for (auto &i: data) cin >> i; string str; str.resize(n); cin >> str; vector<int> order(n); int l = 0, r = n - 1; for (int i = 0; i < n; ++i) { if (str[i] == 'L') order[i] = data[l++]; else order[i] = data[r--]; } int cur = 1; for (int i = n - 1; i >= 0; --i) { cur = (cur * order[i]) % m; data[i] = cur; } for (int i = 0; i < n; ++i) cout << data[i] << " \n"[i == n - 1]; } } D. Card Game 大致题意 有一副扑克牌,其中有一个花色是王牌,王牌的牌大于其他花色的牌,相同花色的牌,则数值越大越大 现在有 $2 \times n$ 张牌,问是否恰好存在 $n$ 对牌,使得每一对都是可以比较出大小的值 思路 模拟即可,不是很难 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, sp; char s; cin >> n >> s; sp = (s == 'C' ? 0 : (s == 'D' ? 1 : (s == 'H' ? 2 : 3))); vector<int> data[4]; string str; str.resize(2); for (int i = 0; i < n * 2; ++i) { cin >> str; if (str[1] == 'C') data[0].push_back(str[0]); if (str[1] == 'D') data[1].push_back(str[0]); if (str[1] == 'H') data[2].push_back(str[0]); if (str[1] == 'S') data[3].push_back(str[0]); } sort(data[0].begin(), data[0].end(), greater<>()); sort(data[1].begin(), data[1].end(), greater<>()); sort(data[2].begin(), data[2].end(), greater<>()); sort(data[3].begin(), data[3].end(), greater<>()); vector<pair<pair<int, char>, pair<int, char>>> ans; ans.reserve(n); bool flag = true; for (int i = 0; i < 4; ++i) { if (i == sp) continue; for (int j = (int)data[i].size() - 1; j >= 1; j -= 2) ans.push_back({{i, data[i][j]}, {i, data[i][j - 1]}}); if (data[i].size() % 2 && !data[sp].empty()) { ans.push_back({{i, data[i][0]}, {sp, data[sp].back()}}); data[sp].pop_back(); } else if (data[i].size() % 2) flag = false; } for (int j = (int)data[sp].size() - 1; j >= 1; j -= 2) ans.push_back({{sp, data[sp][j]}, {sp, data[sp][j - 1]}}); if (!flag) cout << "IMPOSSIBLE" << endl; else { auto to_char = [](int i) { switch (i) { case 0: return 'C'; case 1: return 'D'; case 2: return 'H'; case 3: return 'S'; } return 'A'; }; for (const auto &[a, b]: ans) cout << a.second << to_char(a.first) << ' ' << b.second << to_char(b.first) << endl; } } } E. Final Countdown 大致题意 有一个倒计时,但是它每次减少的时候,需要的耗时与改变的值的数量相同,比如 $10 \rightarrow 9$ 需要 $2$ 秒 问当前的倒计时值实际需要多少秒 思路 类似不同的进制值,进制分别是 $1, 11, 111, 1111, 11111, 11111 \dots$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.resize(n); cin >> str; vector<int> ans(n + 1, 0); int tmp = 0; for (int i = 0; i < n; ++i) { ans[i + 1] = str[i] - '0' + tmp; tmp += str[i] - '0'; } for (int i = n; i > 0; --i) { ans[i - 1] += ans[i] / 10; ans[i] %= 10; } int start = 0; while (!ans[start]) ++start; for (int i = start; i <= n; ++i) cout << ans[i]; cout << endl; } } F. Feed Cats 大致题意 有 $m$ 只猫,每只猫可以出现在 $[l_i, r_i]$ 的范围内,每只猫只允许吃一次食物,吃多了就会死。 问现在在不同的格子上放食物(每次放的食物量足够任意数量的猫吃),在保证不会出现吃死的情况下,最多可以喂饱多少只小猫 思路 其实就是取几个点,使得他们之间没有跨越相同的区间,同时总跨越的区间足够多 简单 dp 一下即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<pair<int, int>> data(m), rdata; for (auto &[a, b]: data) cin >> a >> b; rdata = data; sort(data.begin(), data.end()); sort(rdata.begin(), rdata.end(), [&](const pair<int, int> &lhs, const pair<int, int> &rhs) { return lhs.second != rhs.second ? lhs.second < rhs.second : lhs.first < rhs.first; }); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; vector<int> ans(n + 1, 0); int l = 0, res = 0, r = 0, cnt = 0; for (int i = 1; i <= n; ++i) { while (l < m && data[l].first <= i) { q.push(data[l++]); ++cnt; } while (r < m && rdata[r].second < i) { ++r; cnt--; } while (!q.empty() && q.top().second < i) q.pop(); if (q.empty()) ans[i] = ans[i - 1]; else ans[i] = ans[q.top().first - 1] + cnt; ans[i] = max(ans[i], ans[i - 1]); res = max(res, ans[i]); } cout << res << endl; } } G. Moving Platforms 大致题意 有一个图,其中每一个点都有一个权值 $l_i$,一个步长 $s_i$。 每个单位时间后,每个点的权值变成 $l_i \leftarrow (l_i + s_i) \space mod \space H$ 如果两个点之间有边,且他们权值此时相同,则连通,否则边不可用,且走过一个边需要花费一个单位时间 问从第 1 个节点走到第 n 个节点需要多少时间 思路 容易得到,实际上每一条边的可通行时间是一个 $ax + b$ 函数的值,其中 $x \in [0, \inf)$,如果能够求出所有边的函数,那么就很容易了 接下来是如何计算边的 $a, b$ 了 首先 $a$ 很容易,因为都是累加,所以需要满足 $s_i \times t - s_j \times t \equiv 0 (mod \space H)$ 容易得到方程 $a \leftarrow \frac{lcm(s_i - s_j, H)}{s_i - s_j}$ 接下来是如何算 $b$ 了,即第一次需要走多少步,他们才会相同 容易得到 $l_i + t_1 \times s_i \equiv l_j + t_1 \times s_j (mod \space H)$ 变换可以得到 $t_1 \times (s_i - s_j) + t_2 \times H = l_j - l_i$ 这很显然可以使用扩展欧几里得去做,求算出 $t_1$ 是多少,再通过计算得到 $b$ 即可 然后正常的图上求最短路径即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { function<int(int, int, int &, int &)> exGcd = [&exGcd](int a, int b, int &x, int &y) { if (!b) { x = 1, y = 0; return a; } int ret = exGcd(b, a % b, y, x); y -= a / b * x; return ret; }; struct edge { int a, b, v, n; }; int n, m, H; cin >> n >> m >> H; vector<int> l(n), s(n); vector<edge> edges(m * 2); vector<int> head(n + 1, -1); for (auto &i: l) cin >> i; for (auto &i: s) cin >> i; for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; int a = s[u - 1] - s[v - 1], b = H, x, y; int g = exGcd(a, b, x, y); if (abs(l[v - 1] - l[u - 1]) % abs(g)) continue; a = abs(b / g); b = (a - (((l[u - 1] - l[v - 1]) / g * x) % a + a) % a) % a; edges[i << 1] = {a, b, v, head[u]}; edges[i << 1 | 1] = {a, b, u, head[v]}; head[u] = i << 1; head[v] = i << 1 | 1; } priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; vector<bool> visit(n + 1, false); q.emplace(0, 1); int ans = -1; while (!q.empty()) { auto [cost, cur] = q.top(); q.pop(); if (visit[cur]) continue; visit[cur] = true; if (cur == n) { ans = cost; break; } for (int e = head[cur]; ~e; e = edges[e].n) { int tmp = (cost - edges[e].b + edges[e].a - 1) / edges[e].a; int nc = tmp * edges[e].a + edges[e].b; q.emplace(nc + 1, edges[e].v); } } cout << ans << endl; } }

2024/4/14
articleCard.readMore

Codeforces Round 926 (Div. 2)

A. Sasha and the Beautiful Array 大致题意 有一个数组,现在允许你任意排序它,使得其所有的相邻对之差之和最小,问如何操作 思路 排序一下就行,这样就等于最大的那个值减去最小的那个 AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; sort(data.begin(), data.end()); cout << data.back() - data.front() << endl; } } B. Sasha and the Drawing 大致题意 有一个正方形,其上有 $4 \times n - 2$ 条对角线,现在要你染黑一些格子,使得这些对角线至少有 $x$ 个被覆盖,问最少染黑几个 思路 只要染黑第一行和最下面一行即可,显然,除了四个角落,其他几个点染了就是影响两条对角线 AC code 1 2 3 4 5 6 7 8 9 10 11 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; if (m <= 4 * n - 4) cout << (m + 1) / 2 << endl; else if (m == 4 * n - 3) cout << 2 * n - 1 << endl; else if (m == 4 * n - 2) cout << 2 * n << endl; } } C. Sasha and the Casino 大致题意 在赌场赌博,已知每次可以下注任意合理的钱 $y$,赢了就收回 $k \times y$,输了就没了,且最多连续输 $x$ 场,问是否赚到任意数量的钱 思路 根据赌徒原理做,要保证你每次下注的时候,如果赢了能把之前输的钱全都赚回来,且还要多赚一点 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int k, x, a; cin >> k >> x >> a; int ca = a, lose = 0; bool flag = true; for (int i = 0; i < x; ++i) { int cur = (lose + k - 1) / (k - 1); if (ca < cur) { flag = false; break; } ca -= cur; lose += cur; } if (ca * k <= a) flag = false; cout << (flag ? "YES" : "NO") << endl; } } D. Sasha and a Walk in the City 大致题意 有一棵树,现在要选择一定数量的节点染色,使得任意两个节点之间的路径最多只经过两个染黑节点,问如何操作 思路 树上 dp 即可,关注当前节点到所有下面的子节点中,染色数量最多的路径染色了多少个,可以枚举 1 个和 2 个的情况(0 个一定只有一种) AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; constexpr int mod = 998244353; vector<pair<int, int>> edges((n - 1) * 2); vector<int> head(n + 1, -1); for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; edges[i << 1] = {v, head[u]}; edges[i << 1 | 1] = {u, head[v]}; head[u] = i << 1; head[v] = i << 1 | 1; } function<pair<int, int>(int, int)> dfs = [&](int u, int p) { int a = 1, b = 0; for (int e = head[u]; ~e; e = edges[e].second) { if (edges[e].first == p) continue; auto [na, nb] = dfs(edges[e].first, u); a = (a * (1 + na)) % mod; b = (b + na + nb) % mod; } return make_pair(a, b); }; auto [a, b] = dfs(1, 0); cout << (a + b + 1) % mod << endl; } }

2024/4/13
articleCard.readMore

Codeforces Round 925 (Div. 3)

A. Recovering a Small String 大致题意 有一个字符串,长度固定为 $3$ 个字母,将其的每个字母对应的字母下标相加的值已知,问字典序最小的字符串是多少 思路 简单题,从后往前考虑即可,后面的尽可能大就是前面尽可能小 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> str(3); str[2] = min(26, n - 2); n -= str[2]; str[1] = min(26, n - 1); n -= str[1]; str[0] = n; cout << (char)(str[0] + 'a' - 1) << (char)(str[1] + 'a' - 1) << (char)(str[2] + 'a' - 1) << endl; } } B. Make Equal 大致题意 有 $n$ 个水壶,每次允许将前面的水壶里的一部分水倒入到后面的水壶,问是否可能使得所有水壶的水一样多 思路 记录一个中间值,从前往后遍历,超过平均值就把超出部分加到中间值上,反之则减去,只要中间值不出现负数即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; int tar = 0; for (const auto &i: data) tar += i; tar /= n; int last = 0; bool flag = true; for (const auto& i:data) { last += i - tar; if (last < 0) flag = false; } cout << (flag ? "YES" : "No") << endl; } } C. Make Equal Again 大致题意 有一段数组,允许最多选择一段区间,把区间的数值变成一个任意值,问最少需要选择多少的区间才能让整个数组变成一样的值 思路 看看最左边的值和最右边的值即可,如果一样就抓中间的,如果不一样就尝试一下都变成最左边的值或者最右边的值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; // left int l = 0, r = n - 1; while (l < n && data[l] == data[0]) ++l; while (r >= 0 && data[r] == data[n - 1]) -- r; if (data[0] == data[n - 1]) cout << max(r - l + 1, 0) << endl; else cout << min(n - l, r + 1) << endl; } } D. Divisible Pairs 大致题意 已知一个数组,找出满足如下条件的 $i, j$ 对,问有多少对 $(a_i + a_j) \space mod \space x = 0$ $(a_i - a_j) \space mod \space y = 0$ 思路 从取摸特点考虑,容易得出 $$a_i \space mod \space x + a_j \space mod \space x = x$$ 且$$a_i \space mod \space y = a_j \space mod \space y$$ 所以只需要统计 $mod \space x$ 和 $mod \space y$ 的结果即可。我这里直接用了高位 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, x, y; cin >> n >> x >> y; map<int, int> cnt; int ans = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; int a = tmp % x, b = tmp % y; auto iter = cnt.find(a << 32 | b); if (iter != cnt.end()) ans += iter->second; ++cnt[(a == 0 ? 0 : (x - a)) << 32 | b]; } cout << ans << endl; } } E. Anna and the Valentine’s Day Gift 大致题意 有一个数组,两个人博弈 A 每次允许将数组中的一个值,在 10 进制上做翻转,并清除掉前导 0 B 每次允许将数组的两个值在十进制上直接拼接在一块 问最终得到的唯一一个的数值和 $10^m$ 的大小关系是什么 思路 一个是要通过翻转来删除后缀 0,能够有效的减少最终数值的长度,而另外一个可以拼接把后缀 0 隐藏在数值内部,所以只需要考虑所有的后缀 0 长度即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<int> data(n); int tot = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; for (int j = 1000000000, k = 9; j >= 1; j /= 10, --k) if (tmp % j == 0) { data[i] = k; break; } for (int j = 1000000000, k = 10; j >= 1; j /= 10, --k) if (tmp >= j) { tot += k; break; } } sort(data.begin(), data.end(), greater<>()); for (int i = 0; i < n; i += 2) tot -= data[i]; cout << (tot > m ? "Sasha" : "Anna") << endl; } } F. Chat Screenshots 大致题意 有一个未知的默认的初始的排列,现在给出 $k$ 个通过其演变来的数组,演变的方式是将原始数组中的某一个值提到最开头,其他值顺序不变 问这些数组是否来自同一个初始的排列 思路 放弃第一个值,直接拓扑即可,能拓扑就是成功 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; vector<vector<int>> data(k); for (auto &v: data) { v.resize(n); for (auto &i: v) cin >> i; } if (k == 1) { cout << "YES" << endl; continue; } vector<set<int>> map(n); for (const auto &v: data) for (int i = 2; i < n; ++i) map[v[i - 1] - 1].insert(v[i] - 1); vector<int> deg(n); for (const auto &v: map) for (const auto &i: v) ++deg[i]; queue<int> q; int cnt = 0; for (int i = 0; i < n; ++i) if (!deg[i]) q.push(i); while (!q.empty()) { auto cur = q.front(); q.pop(); ++cnt; for (const auto& i: map[cur]) if (!--deg[i]) q.push(i); } cout << (cnt == n ? "YES" : "NO") << endl; } } G. One-Dimensional Puzzle 大致题意 有 4 种方块,现在需要把它们拼接在一行里,问有多少种排列方式 思路 显然,当不存在 1 和 2 的时候,同时仅存在 3 和 4 的时候,那么 如果只有 3 或者 4,那么只有一种排法 如果同时有 3 和 4,那么就没有排法 接下来要考虑的肯定是 1 和 2 至少其中一个有的情况。 也容易发现,3 和 4 本质上并不会改变接口的形状,只是增长了一些现有的结构罢了,所以容易得出,3 / 4 在是否能够排列出这件事上,不重要 而 1 和 2 不一样,前者会减少一个凹形,后者会减少一个凸形,而一个 1 只会引入两个凸形,如果恰好,2 的数量比 1 多两个,那么必然会导致无法组成一行 同理,1 比 2 多两个也会导致组成不了形状。实际上也很容易得出,一定上组成 1/2/1/2/1/2 这样的依次排列形状(先不考虑 3/4) 所以如果 1 和 2 一样多,那么就可以得到 1/2/1/2 这样的组合,同时也可以得到 2/1/2/1 这样的组合。 如果恰好差一个,那么必然是 1/2/1/2/1 或者 2/1/2/1/2 其中之一,显然此时分成了两种情况考虑 接下来看 3/4 的情况,实际上 3/4 就是往 1/2 组成的结构里插入即可,于是问题就回到了在 $n$ 个和盒子中放 $m$ 个苹果的问题,注意可以空箱子 即答案就是 $\begin{pmatrix} n + m - 1 \\ m - 1 \end{pmatrix}$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { constexpr int mod = 998244353; auto qp = [&](int a, int p) { int res = 1; while (p) { if (p & 1) res = res * a % mod; a = a * a % mod; p >>= 1; } return res; }; auto inv = [&](int v) { return qp(v, mod - 2); }; auto step = [&](int n) { int res = 1; for (int i = 2; i <= n; ++i) res = res * i % mod; return res; }; auto cal = [&](int n, int m) { int a = step(n + m -1), b = step(m - 1), c = step(n); a = a * inv(b) % mod; a = a * inv(c) % mod; return a; }; int c1, c2, c3, c4; cin >> c1 >> c2 >> c3 >> c4; if (abs(c1 - c2) > 1 || (c1 == 0 && c2 == 0 && c3 != 0 && c4 != 0)) { cout << 0 << endl; continue; } if (c1 == 0 && c2 == 0) { cout << 1 << endl; continue; } int ans = 1; int tmp = max(c1, c2); if (c1 == c2) { int ans1 = 1, ans2 = 1; if (c3 != 0) { ans1 = ans1 * cal(c3, tmp) % mod; ans2 = ans2 * cal(c3, tmp + 1) % mod; } if (c4 != 0) { ans2 = ans2 * cal(c4, tmp) % mod; ans1 = ans1 * cal(c4, tmp + 1) % mod; } ans = (ans1 + ans2) % mod; } else { if (c3 != 0) ans = ans * cal(c3, tmp) % mod; if (c4 != 0) ans = ans * cal(c4, tmp) % mod; } cout << ans << endl; } }

2024/4/6
articleCard.readMore

Codeforces Round 924 (Div. 2)

A. Rectangle Cutting 大致题意 有一个矩型,将其切割成两半,然后再拼接起来,问是否可能得到另外一个矩型 思路 简单题,直接尝试一下就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b; cin >> a >> b; bool flag = false; if (a % 2 == 0) { int ra = a / 2, rb = b * 2; if (ra != b || rb != a) flag = true; } if (b % 2 == 0) { int ra = a * 2, rb = b / 2; if (ra != b || rb != a) flag = true; } cout << (flag ? "YES" : "No") << endl; } } B. Equalize 大致题意 已知一个数组,现在将一个等长的排列加到这个数组上,问最多出现多少个相同的值 思路 等价于在长度为 $n$ 的值范围内,原始数组有多少个不同的值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; sort(data.begin(), data.end()); int end = (int)(unique(data.begin(), data.end()) - data.begin()); int l = 0, ans = 0; for (int r = 0; r < end; ++r) { while (data[r] - data[l] >= n) ++l; ans = max(ans, r - l + 1); } cout << ans << endl; } } C. Physical Education Lesson 大致题意 有一个数组,其值类似一个波长为 $x$ 的波,从 $1 \rightarrow k \rightarrow 1$,现在只知道第 $n$ 的位置是 $x$,问有多种不同的波的可能 思路 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int x, n, v[2]; cin >> x >> n; // upside v[0] = x - n; // downside v[1] = x + n - 2; set<int> st; auto add = [&](int x) { if (x % 2 || x < n * 2 - 2) return; st.insert(x); }; auto cal = [&](int x) { int r = min((int)sqrt(x) + 10, x); for (int i = 1; i < r; ++i) if (x % i == 0) { add(i); add(x / i); } }; cal(v[0]); cal(v[1]); cout << st.size() << endl; } } D. Lonely Mountain Dungeons 大致题意 有 $n$ 个不同的种族,每个种族有不同数量的士兵,现在需要将它们组成 $k$ 只军队,每个士兵必定属于某一个军队 每多创建一个军队,其就要减少 $x$ 的战斗力,而当同一个种族的两个士兵被分配到不同的队伍的情况下,则会增加 $b$ 单位的战斗力 问最大的战斗力可能是多少 思路 三分一下队伍数量即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, b, x; cin >> n >> b >> x; vector<int> c(n); for (auto &i: c) cin >> i; int l = 1, r = 2e5 + 10; auto check = [&](int mid) { int res = -x * (mid - 1); for (const auto &i: c) { int v = i / mid, c1 = i % mid, c2 = mid - c1; res += b * c1 * (v + 1) * (i - v - 1) / 2; res += b * c2 * v * (i - v) / 2; } return res; }; while (l + 10 < r) { int ml = (2 * l + r) / 3, mr = (l + 2 * r) / 3; int rl = check(ml), rr = check(mr); if (rl < rr) l = ml; else r = mr; } int ans = 0; for (int i = l; i <= r; ++i) ans = max(ans, check(i)); cout << ans << endl; } } E. Modular Sequence 大致题意 有一个数组,第一个值已经确定,其后的每一个值的,等于前一个值 $+ y$ 或者等于 $mod \space y$,且已知长度和总和,问是否存在这样的数组 思路 容易得到,最终因为变化都和 $y$ 有关,所以 $x \space y$ 这部分的值,必然会被每一个单位所保留,即每一个值必定等价于 $t \times y + x \space mod \space y$ 所以可以先 $s \leftarrow $,那么所有值就等于 $t \times y$ 再统一除以 $y$ 可以得到 $s \leftarrow \frac{s - n \times (x \space mod \space y)}{y}$ 而数组则是几个递增的阶梯($0, 1, 2, \dots$)组成 所以只需要求解阶梯的数量和每个阶梯的长度即可 容易得到一个简单的结论:最长的阶梯不超过 $650$,因为 $(1 + 650) \times 650 / 2 = 211575$,所以可以通过暴力的手段解决 定义 dp[i] 表示当前 $s$ 还剩下 $i$ 个值需要处理的时候,已经消耗了多少个位置,对于每一个 $i$,暴力遍历 $650$ 种可能性即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, x, y, s; cin >> n >> x >> y >> s; if ((s - n * (x % y)) % y || (x % y) * n > s || x > s) { cout << "NO" << endl; continue; } int rs = (s - n * (x % y)) / y; int st = x / y - 1; if (x / y > rs) { cout << "NO" << endl; continue; } vector<pair<int, int>> dp(rs + 1, {0x3fffffff, -1}); dp[rs] = {0, -1}; for (int i = st + 1, tmp = 0; i <= s; ++i) { tmp += i; if (tmp == 0) continue; if (rs - tmp < 0) break; dp[rs - tmp] = {i, rs}; } for (int i = rs - 1; i >= 1; --i) for (int j = 1; j < 623; ++j) { if (i - (1 + j) * j / 2 < 0) break; if (dp[i - (1 + j) * j / 2].first <= dp[i].first + j + 1) continue; dp[i - (1 + j) * j / 2] = {dp[i].first + j + 1, i}; } if (st + n < dp[0].first) { cout << "NO" << endl; continue; } cout << "YES" << endl; int cur = 0; vector<int> res; while (~cur) { if (dp[cur].second != -1) res.push_back(dp[cur].second - cur); cur = dp[cur].second; if (res.size() > 100) { cerr << 1; } } reverse(res.begin(), res.end()); int index = 0; for (int i = 0; i < res.size(); ++i) { int cost = 0, j = i == 0 ? st + 1 : 0; while (cost <= res[i]) { cout << j * y + x % y << ' '; ++index; ++j; cost += j; } } while (index < n) { cout << x % y << ' '; ++index; } cout << endl; } }

2024/4/5
articleCard.readMore

Codeforces Round 923 (Div. 3)

A. Make it White 大致题意 有一段黑白间隔的数组,允许选择其中的一段,将其涂成白色,问最小要多长 思路 找到最左边和最右边黑色即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.resize(n); cin >> str; int l = n, r = 0; for (int i = 0; i < n; ++i) if (str[i] == 'B') { l = min(l, i); r = max(r, i); } cout << r - l + 1 << endl; } } B. Following the String 大致题意 已知一个数组,其映射一个相同长度的字符串,其中每一个数值表示对应字符串里的这个位置上的字母,是整个字符串里第几次出现 给出一个合理的字符串 思路 找就行了,对于每一个位置,找到一个合理的字母放上去就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int cnt[26] = {}; for (int i = 0; i < n; ++i) { for (int j = 0; j < 26; ++j) { if (cnt[j] == data[i]) { cout << static_cast<char>(j + 'a'); ++cnt[j]; break; } } } cout << endl; } } C. Choose the Different Ones! 大致题意 有两个数组,分别取出 $\frac{k}{2}$ 个数值,使得正好得到 $[1, k]$ 这几个数,问是否可能 思路 找出 $[1, k]$ 中,仅存在一侧的数值,看看是不是有一次持有的这种值超过 $\frac{k}{2}$ 个即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m, k; cin >> n >> m >> k; vector<char> data(k + 1); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp <= k) data[tmp] |= 1; } for (int i = 0; i < m; ++i) { int tmp; cin >> tmp; if (tmp <= k) data[tmp] |= 2; } int cnt[2] = {}; bool flag = true; for (int i = 1; i <= k; ++i) if (data[i] == 0) flag = false; else if (data[i] <= 2) ++cnt[data[i] - 1]; cout << (flag && cnt[0] <= k / 2 && cnt[1] <= k / 2 ? "YES" : "NO") << endl; } } D. Find the Different Ones! 大致题意 有一个数组,每次给出一个区间的询问,问区间内是否存在任何两个值不一样 思路 只要记录所有值发生变化的下标即可,然后找一下区间内有没有下标,有的话取下标两边的值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, q; cin >> n; set<int> st; int last = -1; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (i != 0 && tmp != last) st.insert(i + 1); last = tmp; } cin >> q; for (int i = 0; i < q; ++i) { int l, r; cin >> l >> r; const auto iter = st.upper_bound(l); if (iter == st.end() || *iter > r) cout << -1 << ' ' << -1 << endl; else cout << *iter - 1 << ' ' << *iter << endl; } cout << endl; } } E. Klever Permutation 大致题意 已知 $n, k$,需要给出一个 $n$ 的排列,使得任取两组相邻的 $k$ 个数值组成的和,差值不超过 1 思路 从滑动窗口的视角看比较容易 把原始的有序排列拆成 $\left \lfloor \frac{n}{k} \right \rfloor$ 份,然后依次从每一份中取一个值,排列成一个数组即可 注意取的时候,奇数份内从大到小,而偶数份从小到大即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; vector<pair<int, int>> data(k); int page = n / k, start = n % k; data[0] = {1, page + (start != 0)}; for (int i = 1; i < k; ++i) { data[i] = {data[i - 1].second + 1, data[i - 1].second + page}; if (i < start) data[i].second++; } for (int i = 0; i < n; ++i) if (i % 2) cout << data[i % k].second-- << ' '; else cout << data[i % k].first++ << ' '; cout << endl; } } F. Microcycle 大致题意 有一个无向图,找一个包含最小边权的环 思路 类似生成树,只不过反过来,从最大权重的边开始遍历,找到最后一个会触发环逻辑的边即可,然后再根据确认的边的两个点找环即可 用一下并查集即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<tuple<int, int, int>> data(m); for (auto &[u, v, w]: data) cin >> u >> v >> w; sort(data.begin(), data.end(), [](const tuple<int, int, int> &lhs, const tuple<int, int, int> rhs) { return get<2>(lhs) > get<2>(rhs); }); vector<int> fa(n + 1); for (int i = 0; i < n + 1; ++i) fa[i] = i; function<int(int)> find = [&](const int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }; auto join = [&](int x, int y) { x = find(x); y = find(y); if (x == y) return false; fa[y] = x; return true; }; tuple<int, int, int> start; vector<int> head(n + 1, -1); vector<pair<int, int>> edges; for (const auto &[u, v, w]: data) { if (!join(u, v)) start = {u, v, w}; else { edges.emplace_back(u, head[v]); head[v] = (int) edges.size() - 1; edges.emplace_back(v, head[u]); head[u] = (int) edges.size() - 1; } } vector<int> dis(n + 1, -1); queue<int> q; auto [l, r, w] = start; dis[l] = 0; q.push(l); while (!q.empty()) { auto cur = q.front(); q.pop(); for (int e = head[cur]; ~e; e = edges[e].second) { if (dis[edges[e].first] != -1) continue; dis[edges[e].first] = dis[cur] + 1; q.push(edges[e].first); } } cout << w << ' ' << dis[r] + 1 << endl; cout << r; int cur = r; while (cur != l) { for (int e = head[cur]; ~e; e = edges[e].second) { if (dis[edges[e].first] + 1 == dis[cur]) { cur = edges[e].first; cout << ' ' << cur; break; } } } cout << endl; } } G. Paint Charges 大致题意 有一个数组,数组上,对于每一个值可以进行如下操作其中一个,或者不操作 将当前值左侧的 $a_i$ 个值进行染色(包含自己) 将当前值右侧的 $a_i$ 个值进行染色(包含自己) 问最少操作几次,可以使得整个数组都被染色 思路 考虑 dp,定义 dp[i][j] 表示,当前是关注的是第 $i$ 个值,且此时染色到 $j$ 位置的时候,最小花费是多少 显然 $dp_{i,j} = dp_{i-1,j}$ 同时,考虑向左和向右的填涂即可 此时可以发现,大部分样例都过了,除了一个特殊的 case:一个值先不管左边,先往右进行染色,然后由右边的值来补偿左边的。 这种 case 也可以融入进 dp 的逻辑 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n + 1); for (int i = 1; i <= n; ++i) cin >> data[i]; vector<vector<int>> dp(n + 1); constexpr int INF = 0x3fffffff; for (auto& item: dp) item.resize(n + 1, INF); dp[0][0] = 0; for (int i = 1; i <= n; ++i) { // nothing for (int j = 0; j < n + 1; ++j) dp[i][j] = dp[i - 1][j]; // go left for (int j = max(i - data[i] + 1, 0); j <= i; ++j) dp[i][j] = min(dp[i][j], dp[i - 1][max(i - data[i], 0)] + 1); // go right for (int j = i; j <= min(i + data[i] - 1, n); ++j) dp[i][j] = min(dp[i][j], dp[i - 1][i - 1] + 1); // go left + another go right for (int j = 1; j < i; ++j) { if (i - data[i] + 1 >= j || j + data[j] - 1 <= i) continue; for (int k = max(i - data[i], 0); k <= min(j + data[j] - 1, n); ++k) dp[i][k] = min(dp[i][k], dp[j - 1][max(i - data[i], 0)] + 2); } } cout << dp[n][n] << endl; } }

2024/3/31
articleCard.readMore

Codeforces Round 922 (Div. 2)

A. Brick Wall 大致题意 有一堵砖墙,由砖块组成,每一个砖块都是 $1 \times k$ ($k$ 可以是任意值,每一块砖块的 $k$ 可以不一样)的方块,可以横放或者纵向放 问横放和纵放的最大差值是多少 思路 那全都横放不就行了 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; cout << n * (m / 2) << endl; } } B. Minimize Inversions 大致题意 有两个数组,每次允许操作选择两个下标,在两个数组中分别操作交换这两个下标的值 问让这两个数组的逆序对数量之和最小,应该如何操作 思路 大胆猜测,把其中一个数组排序好就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<pair<int, int>> data(n); for (auto& [fst, snd]: data) cin >> fst; for (auto& [fst, snd]: data) cin >> snd; sort(data.begin(), data.end()); for (int i = 0; i < n; ++i) cout << data[i].first << " \n"[i == n - 1]; for (int i = 0; i < n; ++i) cout << data[i].second << " \n"[i == n - 1]; } } C. XOR-distance 大致题意 有两个数,现在希望找到一个 $x$,使得 $\left | (a \oplus x) - (b \oplus x)\right |$ 最小,且 $x \in [0, r]$ 思路 由于是异或运算,且最后取了绝对值,实际上对于每一个比特位而言,$x$ 取什么毫无意义。因为对于这个比特位而言,$x$ 取任意值,不同的则还是不同,相同的则还是相同 所以考虑的情况是,某个高的比特位发生了 $a \neq b$ 的情况,这个时候需要努力去构造另外一个值的下面的比特位,使得高位的这个差值带来的影响尽可能小 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b, r; cin >> a >> b >> r; auto f = [&](const int v, int i) { int rs = r, res = 0; for (; i >= 0; --i) { if ((a & 1LL << i) == (b & 1LL << i)) res += 1LL << i; else if (v & 1LL << i) { if (rs >= 1LL << i) rs -= 1LL << i; else res += 2 * (1LL << i); } } return res; }; int ans = 0; for (int i = 63; i >= 0; --i) { if ((a & 1LL << i) == (b & 1LL << i)) continue; ans = 1 + f(a & 1LL << i ? a : b, i - 1); break; } cout << ans << endl; } } D. Blocking Elements 大致题意 从一个数组中,取出一部分值,将整个数组拆成 $n$ 份,将每一份内进行求和,同时取出的值也作为单独的一份进行求和,这些求和值中最大的就是这个数组的代价 问代价最小是多少 思路 显然,可以二分,问题是如何检查二分的答案是否合法,这里设二分得到的答案是 $v$ 可以通过 dp 的方式来计算,令 dp[i] 作为第 $i$ 个值被选中后,$[1, i]$ 中被选中的那些值的总代价 可以得到 $dp[i] = dp[j] + a[i]$,其中 $j \in [l, i), \sum_{x=l}^{i-1} a_x \leq v$ 故搞个优先队列维护一下即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n), dp(n); for (auto& i: data) cin >> i; auto check = [&](const int v) { priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; pq.emplace(0, -1); int l = 0, tot = 0; for (int i = 0; i < n; ++i) { if (pq.empty()) dp[i] = data[i]; else dp[i] = pq.top().first + data[i]; tot += data[i]; while (tot > v) { tot -= data[l]; ++l; } pq.emplace(dp[i], i); while (!pq.empty() && pq.top().second + 1 < l) pq.pop(); } while (!pq.empty()) { if (pq.top().first <= v) return true; pq.pop(); } return false; }; int l = 0, r = 1e18; while (l + 1 < r) { if (const int mid = l + r >> 1; check(mid)) r = mid; else l = mid; } cout << r << endl; } } E. ace5 and Task Order 大致题意 有一个未知的数组 $a$ 和一个未知的初始值 $x$ 每次允许你询问一个 $i$,若 $a_i < x$,则返回 <,且 $x \leftarrow x - 1$ $a_i > x$,则返回 >,且 $x \leftarrow x + 1$ $a_i = x$,则返回 = 要求求出原始数组 思路 因为不断轮询同一个值,必然最后 $x$ 和它相同 这之后再询问别的值,可以得到它们的关系,同时再询问一次之前的那个值,就可以恢复回来 可以考虑类似快排的方式进行操作即可。注意可以考虑随机函数避免被数据恶心 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> pos(n + 1); for (int i = 1; i <= n; ++i) pos[i] = i; auto pre = [&](const int i) { while (true) { cout << "? " << i << endl; cout.flush(); char tmp; cin >> tmp; if (tmp == '=') return; } }; auto check = [&](const int i, const int base) { cout << "? " << i << endl; cout.flush(); char tmp, temp; cin >> tmp; cout << "? " << base << endl; cout.flush(); cin >> temp; return tmp == '<'; }; function<void(int, int)> qs = [&](const int l, const int r) { if (l >= r) return; swap(pos[rand() % (r - l) + l], pos[r]); pre(pos[r]); int c = l; for (int i = l; i < r; ++i) if (check(pos[i], pos[r])) swap(pos[c++], pos[i]); swap(pos[c], pos[r]); qs(l, c - 1); qs(c + 1, r); }; qs(1, n); vector<int> ans(n + 1); for (int i = 1; i <= n; ++i) ans[pos[i]] = i; cout << "! "; for (int i = 1; i <= n; ++i) cout << ans[i] << " \n"[i == n]; cout.flush(); } }

2024/3/24
articleCard.readMore

Codeforces Round 921 (Div. 2)

A. We Got Everything Covered! 大致题意 有一个字符串长度为 $n$,其最多包含 $k$ 种不同的字母,你需要给出一个序列,使得这个字符串一定是你给出的序列的子序列 思路 就是要满足 $k$ 种字母,长度为 $n$ 下的所有可能的组合,即每一个位置都可能是 $k$ 个值 所以最简单的方式就是把 $k$ 个字母依次输出,重复 $n$ 次即可 AC code 1 2 3 4 5 6 7 8 9 10 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) cout << static_cast<char>('a' + j); cout << endl; } } B. A Balanced Problemset? 大致题意 把一个数值 $x$,拆成 $n$ 份,问它们的 gcd 最大可以是多少 思路 因为 gcd 意味着所有值都有这个因子,那么它们加起来之后,也一定有这个因子。故这个值必定是最初的值的因子 所以找一个够分成 $n$ 份的即可,不需要均分 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; const int r = static_cast<int>(sqrt(n)) + 1; int ans = 1; for (int i = 1; i <= min(r, n); ++i) { if (n % i != 0) continue; if (i >= m) ans = max(ans, n / i); else if (n / i >= m) ans = max(ans, i); } cout << ans << endl; } } C. Did We Get Everything Covered? 大致题意 和 A 题刚好相反,找一个不满足的字符串,使得不是给出的字符串的子序列即可 思路 考虑最差的情况,即每次都取从左到右最后出现的那个字母的值,即可尽可能的往后选取 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k, m; cin >> n >> k >> m; string str; str.resize(m); cin >> str; set<char> st; int tot = 0; vector<char> ans; for (const auto& c: str) { if (c < 'a' || c >= 'a' + k) continue; st.insert(c); if (st.size() == k) { st.clear(); ++tot; ans.push_back(c); } } if (tot >= n) cout << "YES" << endl; else { cout << "NO" << endl; char c = 'a'; for (char i = 0; i < k; ++i) if (!st.count(i + 'a')) c = i + 'a'; for (int i = 0; i < n; ++i) if (i < ans.size()) cout << ans[i]; else cout << c; cout << endl; } } } D. Good Trip 大致题意 有 $n$ 个人,其中有 $m$ 对朋友,每对朋友都有一个亲密度 $f_i$。 每次随机选择两个人,如果它们是朋友,则得到对应亲密度的积分,然后使得他们的亲密度 +1 选择 $k$ 次后,期望积分是多少 思路 容易得到任何一种组合的选取的概率是 $\frac{2}{n \times (n-1)}$,故单次提供的共享应该是 $f_i \times \frac{2}{n \times (n-1)}$ 而每次结束之后,被选中的朋友的积分会加一,而对于期望而言,相当于每一对朋友的积分都增加 $\frac{2}{n \times (n-1)}$ 依次可以得到,最终每一对的共享就是 $f_i \times \frac{2}{n \times (n-1)} + (f_i + \frac{2}{n \times (n-1)}) \times \frac{2}{n \times (n-1)} + \dots + (f_i + (k - 1) \times \frac{2}{n \times (n-1)}) \times \frac{2}{n \times (n-1)}$ 再化简一下,取一下逆元即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { constexpr int mod = 1e9 + 7; auto qp = [&](int a, int p) { int ans = 1; while (p) { if (p & 1) ans = ans * a % mod; a = a * a % mod; p >>= 1; } return ans; }; int n, m, k; cin >> n >> m >> k; const int i = qp(n * (n - 1) / 2 % mod, mod - 2); vector<tuple<int, int, int>> data(m); for (auto& [l, r, v]: data) cin >> l >> r >> v; int ans = 0; for (const auto [_l, _r, v]: data) { const int l = v * k % mod; const int r = i * ((k - 1) * k / 2 % mod) % mod; const int t = (l + r) * i % mod; ans = (ans + t) % mod; } cout << ans << endl; } }

2024/3/23
articleCard.readMore

Educational Codeforces Round 161 (Rated for Div. 2)

A. Tricky Template 大致题意 设定一种模式串,对于模式串中的每一个字符,如果是小写,则表示必须匹配这个小写字母,如果是大写,则表示必定不匹配对应的那个小写字母 再给出三个字符串,问是否存在一个模式串,恰好匹配前面两个字符串,同时不匹配第三个字符串 思路 只要有一个位置的字母,前两个字符串和第三个字符串都不同即可,这样只要那个位置的模式串是大写的第三个字符串的字符即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string a, b, c; a.resize(n); b.resize(n); c.resize(n); cin >> a >> b >> c; bool flag = false; for (int i = 0; i < n; ++i) if (a[i] != c[i] && b[i] != c[i]) flag = true; cout << (flag ? "YES" : "NO") << endl; } } B. Forming Triangles 大致题意 有 n 条边,每条边的长度都是 $2^x$,问可以组成多少个使用了不同边的三角形 思路 因为 $2^x$ 恰好满足一个特点:$2^{a} + 2^{b} < 2^c$,当 $a < b < c$ 时,也就是容易得到,至少有两条边相同才有可能 所以只需要讨论一下两条边相同和三条边相同的情况即可,当然也可以一起讨论了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; map<int, int> cnt; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++cnt[tmp]; } int tot = 0, ans = 0; for (const auto [fst, snd]: cnt) { if (snd == 2) ans += tot; else if (snd > 2) ans += tot * snd * (snd - 1) / 2 + snd * (snd - 1) * (snd - 2) / 6; tot += snd; } cout << ans << endl; } } C. Closest Cities 大致题意 有一排城市,每个城市都有一个坐标,每个城市都可以前往其他的城市。而一个城市距离较近的那个城市的花费成本为 1,而前往其他城市的成本就是距离 计算任意两个城市之间的距离 思路 因为移动移动是从左移动到右边,或者从右边移动到左边,路径只有一条,所以可以前后做两次前缀和解决 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n), cl(n), cr(n); for (auto& i: data) cin >> i; cl[0] = cr[n - 1] = 0; for (int i = 1; i < n; ++i) { if (i == 1 || data[i - 1] - data[i - 2] >= data[i] - data[i - 1]) cl[i] = cl[i - 1] + 1; else cl[i] = cl[i - 1] + data[i] - data[i - 1]; } for (int i = n - 2; i >= 0; --i) { if (i == n - 2 || data[i + 2] - data[i + 1] >= data[i + 1] - data[i]) cr[i] = cr[i + 1] + 1; else cr[i] = cr[i + 1] + data[i + 1] - data[i]; } int m; cin >> m; for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; if (u <= v) cout << cl[v - 1] - cl[u - 1] << endl; else cout << cr[v - 1] - cr[u - 1] << endl; } } } D. Berserk Monsters 大致题意 有一排怪物,每一个怪物都有一定攻击力和防御力,当一个怪物一次性受到的攻击大于其防御力的时候,将会死亡 现在每个怪物将会同时攻击它相邻的两个怪物,经过 $n$ 轮次后,问每一轮死了多少怪物 思路 首先是非常容易计算是否会死亡,即只要相邻两个怪物的攻击力高于其防御力即可,由于不是生命值,所以很容易统计 而每轮只有死亡怪物后,其相邻的怪物才会有可能死亡,所以只需要考虑每次发生变化的怪物附近即可,不需要考虑全部怪物 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> a(n), d(n); for (auto& i: a) cin >> i; for (auto& i: d) cin >> i; map<int, pair<int, int>> mp; for (int i = 0; i < n; ++i) mp[i] = {a[i], d[i]}; auto check = [&](map<int, pair<int, int>>::iterator& cur) { int cost = 0; if (cur != mp.begin()) { --cur; cost += cur->second.first; ++cur; } ++cur; if (cur != mp.end()) cost += cur->second.first; --cur; return cost > cur->second.second; }; set<int> s[2]; for (auto iter = mp.begin(); iter != mp.end(); ++iter) if (check(iter)) s[0].insert(iter->first); int cur = 0, nxt = 1, tot = 0; vector ans(n, 0); while (!s[cur].empty()) { for (const int& c: s[cur]) { ++ans[tot]; mp.erase(c); } for (const int& c: s[cur]) { auto iter = mp.upper_bound(c); if (iter != mp.end()) if (check(iter)) s[nxt].insert(iter->first); if (iter != mp.begin()) { --iter; if (check(iter)) s[nxt].insert(iter->first); } } s[cur].clear(); swap(cur, nxt); ++tot; } for (int i = 0; i < n; ++i) cout << ans[i] << " \n"[i == n - 1]; } } E. Increasing Subsequences 大致题意 请构造一个字符串,使其内部的递增子序列的数量恰好是 $n$ 个 思路 容易得到,如果是简单的递增序列,其长度子序列数量的关系是 lencountaddition 01- 121 242 384 4168 很明显与 $2^x$ 有关,如果已经存在一个从 1 开始的递增序列,往其后面添加一个数值 $x$,则可以带来 $2^{x-1}$ 个数量的增加 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; --n; vector<int> ans; int cur = 62; while (n < (1LL << cur) - 1) --cur; for (int i = 1; i <= cur; ++i) ans.push_back(i); n -= (1LL << cur) - 1; while (n) { while (n >= 1LL << cur - 1) { n -= 1LL << cur - 1; ans.push_back(cur); } --cur; } cout << ans.size() << endl; for (int i = 0; i < ans.size(); ++i) cout << ans[i] << " \n"[i == ans.size() - 1]; } }

2024/3/22
articleCard.readMore

Codeforces Round 920 (Div. 3)

A. Square 大致题意 告诉你一个正方形的四个顶点的坐标,问正方形的面积 思路 记录最大和最小的 x 和 y,很好计算 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int mi = 1000, ma = -1000; for (int i = 0; i < 4; ++i) { int u, v; cin >> u >> v; mi = min(mi, v); ma = max(ma, v); } cout << (ma - mi) * (ma - mi) << endl; } } B. Arranging Cats 大致题意 有两个 01 字符串,允许对第一个字符串进行如下操作 将一个 1 变成 0 将一个 0 变成 1 将一个 1 和另外一个 0 交换一下位置 问最多操作几次能让两个字符串相同 思路 多用第三个方法即可,统计 1 的数量即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str1, str2; str1.resize(n); str2.resize(n); cin >> str1 >> str2; int cnt[2][2] = {}; for (int i = 0; i < n; ++i) { if (str1[i] == str2[i]) continue; ++cnt[0][str1[i] - '0']; ++cnt[1][str2[i] - '0']; } cout << max(cnt[0][1], cnt[1][1]) << endl; } } C. Sending Messages 大致题意 有一个手机,待机每小时要消耗 $a$ 电量,而每次开机关机则需要消耗 $b$ 电量,最开始有 $f$ 电量 问在固定的 $n$ 个发送信息任务是否能够完成 思路 计算两次相邻的信息之间,选择待机还是选择关机即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, f, a, b; cin >> n >> f >> a >> b; int last = 0; for (int i = 0; i < n; ++i) { int cur; cin >> cur; f -= min(b, a * (cur - last)); last = cur; } cout << (f > 0 ? "YES" : "NO") << endl; } } D. Very Different Array 大致题意 有两个数组 $a, b$,允许从 $b$ 选择 $x$ 个值,组成和 $a$ 长度相同的字符串,使得和 $a$ 尽可能不一样 思路 排序后,大的和小的匹配,小的和大的匹配,注意要同时开始匹配,选择两侧差值较大者 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<int> a(n), b(m); for (auto& i: a) cin >> i; for (auto& i: b) cin >> i; sort(a.begin(), a.end()); sort(b.begin(), b.end()); int l = 0, r = 0, ans = 0; while (l + r < n) { if (abs(a[l] - b[m - l - 1]) > abs(a[n - r - 1] - b[r])) { ans += abs(a[l] - b[m - l - 1]); ++l; } else { ans += abs(a[n - r - 1] - b[r]); ++r; } } cout << ans << endl; } } E. Eat the Chip 大致题意 有两个棋子在棋盘上,只允许向前、向左前、向右前移动,问是否可能发送吃的可能 思路 每个棋子的可能到达的格子可以绘制出来,只需奥看最终的相遇那一行是否是有覆盖关系即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m, ax, ay, bx, by; cin >> n >> m >> ax >> ay >> bx >> by; if (bx <= ax) { cout << "Draw" << endl; continue; } int al = ay, ar = ay, bl = by, br = by; const bool flag = (bx - ax) % 2; while (ax < bx) { al = max(1, al - 1); ar = min(m, ar + 1); ++ax; if (ax == bx) break; bl = max(1, bl - 1); br = min(m, br + 1); --bx; } if (flag) cout << (al <= bl && ar >= br ? "Alice" : "Draw") << endl; else cout << (bl <= al && br >= ar ? "Bob" : "Draw") << endl; } } F. Sum of Progression 大致题意 有一个数组,给出 $s, d, k$,计算 $\sum{i=0}^{k} (i + 1) \times a\{s+i \times d}$ 思路 分两种情况做,如果 $k$ 比较大,那么可以暴力,如果比较小,那么就通过前缀和进行优化计算 而前缀和则需要考虑间隔 $[1, sqrt(n)]$ 的每一种情况 $x$ 每一种情况下需要计算 $s_i = s_{i-x} + a_i$ 和 $s_i = s_{i-x} + t \times a_i$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, q; cin >> n >> q; vector<int> data(n); for (auto& i: data) cin >> i; const int cap = min(static_cast<int>(sqrt(n)) + 1, n); vector<vector<int>> a(cap), b(cap); for (int i = 0; i < cap; ++i) { a[i].resize(n, 0); b[i].resize(n, 0); for (int j = 0; j <= i; ++j) a[i][j] = b[i][j] = data[j]; for (int j = i + 1; j < n; ++j) { a[i][j] = a[i][j - i - 1] + (j + i + 1) / (i + 1) * data[j]; b[i][j] = b[i][j - i - 1] + data[j]; } } for (int i = 0; i < q; ++i) { int s, d, k; cin >> s >> d >> k; if (d <= cap) { const int start = s - d, end = s + d * (k - 1), cp = (s - 1) / d; const int as = a[d - 1][end - 1] - (start <= 0 ? 0 : a[d - 1][start - 1]), bs = cp * (b[d - 1][end - 1] - (start <= 0 ? 0 : b[d - 1][start - 1])); cout << as - bs << ' '; } else { int ans = 0; for (int j = 0; j < k; ++j) ans += (j + 1) * data[s + j * d - 1]; cout << ans << ' '; } } cout << endl; } } G. Mischievous Shooter 大致题意 可以在一个图上绘制固定形状的一个三角形,问最多能覆盖多少个目标点 思路 也是前缀和,用斜向的前缀和即可 至于四种方向,可以考虑翻转图,而不是翻转形状 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, m, k; cin >> n >> m >> k; vector<string> map(n); for (auto& s: map) { s.resize(m); cin >> s; } vector<vector<int>> h(n), v(n), r(n); for (auto& i: h) i.resize(m, 0); for (auto& i: v) i.resize(m, 0); for (auto& i: r) i.resize(m, 0); auto cal = [&](vector<vector<bool>> &mp) { for (int i = 0; i < n; ++i) { h[i][0] = v[i][0] = r[i][0] = mp[i][0]; h[i][m - 1] = v[i][m - 1] = r[i][m - 1] = mp[i][m - 1]; } for (int j = 0; j < m; ++j) { h[0][j] = v[0][j] = r[0][j] = mp[0][j]; h[n - 1][j] = v[n - 1][j] = r[n - 1][j] = mp[n - 1][j]; } for (int i = 0; i < n; ++i) for (int j = 1; j < m; ++j) h[i][j] = h[i][j - 1] + mp[i][j]; for (int j = 0; j < m; ++j) for (int i = 1; i < n; ++i) v[i][j] = v[i - 1][j] + mp[i][j]; for (int i = 1; i < n; ++i) for (int j = m - 2; j >= 0; --j) r[i][j] = r[i - 1][j + 1] + mp[i][j]; vector<vector<int>> ans(n); for (auto& i: ans) i.resize(m, 0); int res = 0; // tl ans[0][0] = 0; for (int i = 0; i <= min(k, n - 1); ++i) for (int j = 0; j <= min(k - i, m - 1); ++j) ans[0][0] += mp[i][j]; for (int i = 0; i < n; ++i) { if (i != 0) { ans[i][0] = ans[i - 1][0]; ans[i][0] -= h[i - 1][min(k, m - 1)]; const int out = max(i + k - n + 1, 0); if (out >= m) continue; ans[i][0] += r[i + k - out][out] - (k + 1 >= m ? 0 : r[i - 1][k + 1]); } for (int j = 1; j < m; ++j) { ans[i][j] = ans[i][j - 1]; ans[i][j] -= v[min(i + k, n - 1)][j - 1] - (i == 0 ? 0 : v[i - 1][j - 1]); if (j + k >= m + n - 1 - i) continue; const int out = max(i + k - n + 1, 0); ans[i][j] += r[i + k - out][j + out] - (i == 0 || j + k + 1 >= m ? 0 : r[i - 1][j + k + 1]); } } for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) res = max(res, ans[i][j]); #ifdef ACM_LOCAL for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { int tmp = 0; for (int a = 0; i + a < n && a <= k; ++a) for (int b = 0; b + a <= k && j + b < m; ++b) tmp += mp[i + a][j + b]; if (tmp != ans[i][j]) cerr << "tl: " << i << ' ' << j << ' ' << tmp << '-' << ans[i][j] << endl; } } #endif return res; }; vector<vector<bool>> mp; mp.resize(n); for (auto &i: mp) i.resize(m); int ans = 0; // 1 for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) mp[i][j] = map[i][j] == '#'; ans = max(cal(mp), ans); // 2 for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) mp[i][j] = map[i][m - j - 1] == '#'; ans = max(cal(mp), ans); // 3 for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) mp[i][j] = map[n - i - 1][j] == '#'; ans = max(cal(mp), ans); // 4 for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) mp[i][j] = map[n - i - 1][m - j - 1] == '#'; ans = max(cal(mp), ans); cout << ans << endl; } }

2024/3/19
articleCard.readMore

Codeforces Round 919 (Div. 2)

A. Satisfying Constraints 大致题意 给出一堆条件,包括是否大于、小于且不等于某个值,问最终有几个值符合条件 思路 先记录最小的那个区间,然后再过滤掉不满足的,就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; int l = 0, r = INT_MAX; set<int> s; for (int i = 0; i < n; ++i) { int o, x; cin >> o >> x; if (o == 1) l = max(l, x); else if (o == 2) r = min(r, x); else s.insert(x); } int cnt = r - l + 1; for (auto& v: s) if (v <= r && v >= l) --cnt; cout << max(0, cnt) << endl; } } B. Summation Game 大致题意 两个人博弈,一个人先移除最多 $k$ 个值,另外一个人将会把最多 $x$ 个值变成负数,问最终所有值加起来最大是多少 思路 显然,删除最大的最有利,所以枚举删除几个即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k, x; cin >> n >> k >> x; vector<int> data(n); for (auto& i: data) cin >> i; sort(data.begin(), data.end(), greater<>()); int total = 0, fx = 0; for (int i = 0; i < n; ++i) { total += data[i]; fx += i < x ? data[i] : 0; } int cur = 0, ma = 0; for (int l = 0, r = x; l < k; ++l, ++r) { cur -= r < n ? 2 * data[r] : 0; cur += data[l]; ma = max(ma, cur); } cout << total - fx - fx + ma << endl; } } C. Partitioning the Array 大致题意 一个数组,对于每个值再找一个特殊值取模,将其拆成 $n$ 等分,得到的每一个等分的数组相同,问有几种拆法 思路 拆法必然是数组长度的因子,暴力枚举即可。因为一个树的因子一定不会很多 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int gcd(const int a, const int b) { return b == 0 ? a : gcd(b, a % b); } void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; auto check = [&](const int x) { const int len = n / x; int tmp = 0; for (int i = 1; i < x; ++i) for (int j = 0; j < len; ++j) tmp = gcd(abs(data[i * len + j] - data[(i - 1) * len + j]), tmp); return tmp != 1; }; int ans = 0; for (int i = 1; i * i <= n; ++i) { if (n % i) continue; ans += check(i); if (i * i != n) ans += check(n / i); } cout << ans << endl; } } D. Array Repetition 大致题意 开始有一个空的数组,有两种操作: 往数组最后加一个元素 $x$ 把整个数组复制 $x$ 次 问最终的数组中,第 $i$ 位是什么 思路 最终的数值一定是第一个操作得到的,所以可以考虑不断递归逆向整个操作过程,看看目标位置最终是被哪一次加入元素带来的 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, q; cin >> n >> q; vector<pair<int, int>> data(n); for (auto& [fst, snd]: data) cin >> fst >> snd; map<int, list<int>> mp; int maxK = 0; for (int i = 0; i < q; ++i) { int tmp; cin >> tmp; mp[tmp].push_back(i); maxK = max(maxK, tmp); } int tot = 0, i = 0; for (; i < n; ++i) { if (data[i].first == 1) ++tot; else { if ((maxK + tot - 1) / tot <= 1 + data[i].second) data[i].second = (maxK + tot - 1) / tot; tot *= 1 + data[i].second; } if (tot >= maxK) break; } vector<int> ans(q); for (; i >= 0; --i) { if (data[i].first == 1) { if (const auto iter = mp.find(tot); iter != mp.end()) for (const auto& v: iter->second) ans[v] = data[i].second; mp.erase(tot); --tot; } else { const int len = tot / (1 + data[i].second); for (auto iter = mp.upper_bound(len); iter != mp.end(); ) { list<int>& l = mp[(iter->first - 1) % len + 1]; l.splice(l.end(), iter->second); auto nxtIter = iter; ++nxtIter; mp.erase(iter); iter = nxtIter; } tot = len; } } for (int i = 0; i < q; ++i) cout << ans[i] << " \n"[i == q - 1]; } }

2024/3/16
articleCard.readMore

Hello 2024

A. Wallet Exchange 大致题意 Alice 和 Bob 博弈,有两个钱包,每次可以选择一个钱包取走一块钱,问谁会没有办法操作 思路 求和对 2 取模就行了 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b; cin >> a >> b; cout << ((a + b) % 2 ? "Alice" : "Bob") << endl; } } B. Plus-Minus Split 大致题意 有一个 -+ 组成的字符串,允许将其拆成任意数量段,将 - 视为 -1 然后将 + 视为 1,然后对每一段单独求和 再将每一段的和乘上其长度,得到段的成本,所有段的成本之和就是总成本,问让成本最低怎么办 思路 易得,除了之和等于 0 的情况,其他情况都不要合成一个段,所以最终就是求和成 0 的段以外部分成本 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.resize(n); cin >> str; int cnt[2] = {}; for (const auto& c: str) ++cnt[c == '+']; cout << abs(cnt[0] - cnt[1]) << endl; } } C. Grouping Increases 大致题意 将一个字符串拆成两个子序列,每个子序列内,每有一对相邻的正序对就算一个成本,问如何拆让拆成本最小 思路 贪心模拟即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; int data[2] = {0, 0}; int ans = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp > data[0] && tmp > data[1]) { data[data[0] > data[1] ? 1 : 0] = tmp; ++ans; } else if (tmp <= data[0] && tmp <= data[1]) data[data[0] > data[1] ? 1 : 0] = tmp; else data[data[0] > data[1] ? 0 : 1] = tmp; } cout << max(ans - 2, 0) << endl; } } D. 01 Tree 大致题意 有一个 01 字典树,已知每个叶子节点的值中 1 的数量,以及所有叶子节点的顺序 问是否存在这样的字典树 思路 因为每个值必然有一个相邻的节点和它差 1(那个节点不一定是叶子节点) 所以可以从最大值开始,每次找它相邻的值上是否有一个恰好比它小 1 的值,那么可以删除这两个值,把他们的父节点的值加进去(恰好就是它们两个中的较小者) 注意相邻的两个相同相邻的值的时候,它们可以合并 整个过程有点类似哈夫曼编码的过程 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; map<int, int> mp; vector<vector<int>> index(n); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mp.emplace(i, tmp); index[tmp].push_back(i); } for (int t = n - 1; t > 0; --t) { auto& v = index[t]; if (v.empty()) continue; for (int i: v) { const auto iter = mp.find(i); // check near same auto riter = iter; ++riter; if (riter != mp.end() && riter->second == t) { mp.erase(iter); continue; } // check near - 1 if (riter->second == t - 1) { mp.erase(iter); continue; } if (auto liter = iter; liter != mp.begin()) { --liter; if (liter->second == t - 1) { mp.erase(iter); continue; } } } } cout << (mp.size() == 1 && mp.begin()->second == 0 ? "YES" : "NO") << endl; } }

2024/3/10
articleCard.readMore

Good Bye 2023

A. 2023 大致题意 已知一个数组,其每个值的乘积之和恰好是 2023,但是删除掉了 $k$ 个值后,得到数组 $b$,在已知 $b$ 的情况下反推原数组 思路 反过来除一下即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; int sum = 1; bool flag = true; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; sum *= tmp; if (sum > 2023) { flag = false; sum = 0; } } if (flag && 2023 % sum == 0) { cout << "YES\n" << 2023 / sum; for (int i = 1; i < k; ++i) cout << ' ' << 1; cout << endl; } else cout << "NO" << endl; } } B. Two Divisors 大致题意 有一个值 $x$,已知它的两个最大的因子,问 $x$ 思路 若 $gcd(a, b) == a$ 则为 $b b / a$ 否则为 $a b / gcd(a, b)$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 #define int unsigned long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b; cin >> a >> b; if (const int c = __gcd(a, b); c != a) cout << a * b / c << endl; else cout << b / c * b << endl; } } C. Training Before the Olympiad 大致题意 有两个人玩游戏,每次可以选择一个数组中的两个值,移除它们两个并添加一个 $\left \lfloor \frac{a_i + a_j}{2} \right \rfloor \times 2$ 第一个操作的人希望让最后剩下的那个值最大,第二个操作的人希望最小,问依次操作的情况下,最终结果是多少 思路 因为每次操作结束多出来的一定是偶数,所以要尽可能变成偶数 所以第一个人一定是尽力把两个奇数先合并了,第二个人一定是尽力选一个奇数一个偶数 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int cnt = 0, tot = 0; for (int i = 0; i < n; ++i) { cnt += data[i] % 2; tot += data[i]; if (i == 0) cout << tot << ' '; else cout << tot - (cnt / 3 + (cnt % 3 == 1)) << ' '; } cout << endl; } } D. Mathematical Problem 大致题意 给出一个 $n$,希望找到 $n$ 个 $n$ 位的数值,它们都是某个值的幂次,且这 $n$ 个值都可以相互通过交换数值位置变换得到 给出一组解 思路 因为当 $n = 11$ 的时候,能够凑成的数量已经超过 99 个,所以剩下的情况下只要在上面的情况下后面加 0 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define int long long void solve() { vector<vector<int>> data(12); data[1] = {9}; data[3] = {169, 196, 961}; data[5] = {16384, 31684, 36481, 38416, 43681}; data[7] = {1493284, 3214849, 3912484, 4239481, 4293184, 4932841, 9132484}; data[9] = {236759769, 297769536, 369677529, 526977936, 677925369, 769729536, 773562969, 796763529, 927567936, 956293776, 993762576}; data[11] = { 10948273956, 12395704896, 12899053476, 13056947289, 13769849025, 14909875236, 15072963984, 15890367249, 16385792049, 16795382409, 17250983649, 17592308496, 17805432969, 17869540329, 18096437529, 18543902976, 19036548729, 19936875204, 23897140569, 24091385796, 24196735809, 24371580996, 27939456801, 28347109956, 29137465809, 29163075984, 30524981796, 30896147529, 30965792841, 31489567209, 31528974096, 31829057649, 31899746025, 31957640289, 34507892169, 34860997521, 37869549201, 37918604529, 38495617209, 39276501489, 39564790281, 39784690521, 40217895936, 42513078969, 42750938169, 43596187209, 43608715929, 45192807396, 47031295689, 48913072569, 48963510729, 49218753609, 49327965801, 49918730625, 49956273081, 50329781649, 50473912896, 50824997136, 51436879209, 52349897601, 54197236809, 58396172409, 58730129649, 59380429761, 59438927601, 59632174809, 62391547089, 63975102489, 65378910249, 68135094729, 69713425089, 70152938496, 71489390625, 71985963204, 72153480996, 72195390864, 72953469801, 74902931856, 75318960249, 76921358409, 78031952964, 79495238601, 79542613089, 79910243856, 80319527649, 80356941729, 84701953296, 84703699521, 84992073156, 85970931264, 86293175049, 86939471025, 87965034921, 90127845369, 90163874529, 90248571396, 90372985641, 91302478569, 91534687209, 91625473809, 91987250436, 92305984761, 92504789316, 94829507136, 94971830625, 95971083264, 96438197025, 98015329476, 99270365184, 99764381025, 99853472016 }; int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; if (n <= 11) for (int i = 0; i < n; ++i) cout << data[n][i] << endl; else for (int i = 0; i < n; ++i) { cout << data[11][i]; for (int j = 11; j < n; ++j) cout << '0'; cout << endl; } } }

2024/3/3
articleCard.readMore

Codeforces Round 918 (Div. 4)

A. Odd One Out 大致题意 找出三个值中不同的那个 思路 把三个值异或一下就行了 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b, c; cin >> a >> b >> c; cout << (a ^ b ^ c) << endl; } } B. Not Quite Latin Square 大致题意 有一个矩阵,每一行每一列由 ABC 组成,问缺少的那个是什么 思路 直接统计所有 ABC 数量,少的那个就是 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { string str; int cnt[3] = {}; for (int i = 0; i < 3; ++i) { cin >> str; for (const auto& c: str) if (c != '?') ++cnt[c - 'A']; } cout << (cnt[0] == 2 ? 'A' : (cnt[1] == 2 ? 'B' : 'C')) << endl; } } C. Can I Square? 大致题意 给一个数组,问所有值加起来是否是一个平方数 思路 二分一下就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; int sum = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; sum += tmp; } int l = 1, r = 1e9 + 10; while (l + 1 < r) { if (const int mid = (l + r) >> 1; mid * mid <= sum) l = mid; else r = mid; } cout << (l * l == sum ? "YES" : "NO") << endl; } } D. Unnatural Language Processing 大致题意 已知一段话仅有 abcde 组成,且组成的每个单词都是“辅音+元音”或者“辅音+元音+辅音”的格式,要求分割一下字符串 思路 把所有元音前面那个作为开头就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.reserve(n); cin >> str; cout << str[0]; for (int i = 1; i < n; ++i) { if (i + 1 < n && (str[i + 1] == 'a' || str[i + 1] == 'e')) cout << '.'; cout << str[i]; } cout << endl; } } E. Romantic Glasses 大致题意 有一个原始数组,选取它的一段区间,这段区间内的偶数位和奇数位各自相加恰好相等,问是否存在 思路 把原始数组的奇数位置和偶数位置各自累加,做前缀和,然后再求差值,找是否存在差值相同的情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int pre[2] = {}; map<int, int> mp; ++mp[0]; for (int i = 0; i < n; ++i) { pre[i % 2] += data[i]; ++mp[pre[1] - pre[0]]; } bool flag = false; for (auto [fst, snd]: mp) if (snd >= 2) flag = true; cout << (flag ? "YES" : "NO") << endl; } } F. Greetings 大致题意 每个人都从 $a_i$ 走到 $b_i$ 问是否会发生几次相撞 思路 对着 $a$ 排序后,对 $b$ 求逆序对即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #define ll long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<pair<int, int>> data(n); vector<int> b(n); for (auto& [fst, snd]: data) cin >> fst >> snd; for (int i = 0; i < n; ++i) b[i] = data[i].second; sort(data.begin(), data.end()); function<ll(vector<int>&, vector<int>&, int, int)> mergeSort = [&](vector<int>& record, vector<int>& tmp, const int l, const int r) { if (l >= r) return 0ll; const int mid = (l + r) / 2; ll inv_count = mergeSort(record, tmp, l, mid) + mergeSort(record, tmp, mid + 1, r); int i = l, j = mid + 1, poss = l; while (i <= mid && j <= r) { if (record[i] <= record[j]) { tmp[poss] = record[i]; ++i; inv_count += j - (mid + 1); } else { tmp[poss] = record[j]; ++j; } ++poss; } for (int ind = i; ind <= mid; ++ind) { tmp[poss++] = record[ind]; inv_count += j - (mid + 1); } for (int ind = j; ind <= r; ++ind) { tmp[poss++] = record[ind]; } copy(tmp.begin() + l, tmp.begin() + r + 1, record.begin() + l); return inv_count; }; vector<int> record(n), tmp(n); for (int i = 0; i < n; ++i) record[i] = data[i].second; cout << mergeSort(record, tmp, 0, n - 1) << endl; } } G. Bicycles 大致题意 每个城市都有不同速度的车,从 1 号城市出发,问走到 n 号城市需要多久 思路 计算到达每一个城市,且用 $i$ 辆车的情况下,最小费用是多少,然后在图上不断 bfs 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #define int long long void solve() { int _; cin >> _; // bool flag = false; for (int tc = 0; tc < _; ++tc) { int n, m; cin >> n >> m; vector<int> head(n + 1, -1), s(n + 1); vector<tuple<int, int, int>> edges(m * 2); for (int i = 0; i < m; ++i) { int u, v, w; cin >> u >> v >> w; edges[i << 1] = {u, w, head[v]}; edges[i << 1 | 1] = {v, w, head[u]}; head[v] = i << 1; head[u] = i << 1 | 1; } for (int i = 1; i <= n; ++i) cin >> s[i]; vector<vector<int>> last(n + 1); for (auto &i: last) i.resize(n + 1, LONG_LONG_MAX); last[1][1] = 0; queue<int> q; q.push(1); while (!q.empty()) { auto c = q.front(); q.pop(); int e = head[c]; while (~e) { const auto& [to, w, next] = edges[e]; e = next; bool flag = false; for (int i = 1; i <= n; ++i) { if (last[c][i] == LONG_LONG_MAX) continue; if (const int nc = last[c][i] + w * s[i]; last[to][i] > nc) { flag = true; last[to][i] = nc; last[to][to] = min(last[to][to], last[to][i]); } } if (flag) q.push(to); } } int ans = LONG_LONG_MAX; for (int i = 1; i <= n; ++i) ans = min(ans, last[n][i]); cout << ans << endl; } }

2024/3/3
articleCard.readMore

个人备份的常用 macOS 清理命令

清理 brew(/opt/homebrew) 1 2 3 brew upgrade brew autoremove brew cleanup --prune=0 清理 log 日志(/private/var/db/diagnostics) 1 sudo log erase --all

2024/3/2
articleCard.readMore

Codeforces Round 917 (Div. 2)

A. Least Product 大致题意 有一个数组,允许你将每个值变成比原值更接近 0 的值,问如何操作可以使得整个数组的积最小 思路 如果负数是偶数个,那么随便找个值变成 0,如果是奇数个,那就别动 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; bool flag = false, zero = false; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; flag ^= tmp < 0; zero |= tmp == 0; } if (zero || flag) cout << 0 << endl; else cout << "1\n1 0" << endl; } } B. Erase First or Second Letter 大致题意 有一个字符串,允许每次删除第一个或者第二个字母,操作无限制次数,问最多可以有多少个不同的字符串 思路 可以这样思考,如果是一个长度的字符串,那么必然是有多少种字母就是多少个 如果长度等于 2,那么必然是上面的基础上,再加上最后那个字母,那么应该等于首字母的种类数 三个的情况,那必然是在一个字母的基础上,加上原字符串最后两个字母,那么应该也等于首字母的种类数 所以只需要考虑位置即可,如果一个字母出现的第一个位置就是最后那个了,那么必然没有它开头的两个、三个的字符串了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.reserve(n); cin >> str; map<char, int> cnt; for (int i = 0; i < n; ++i) ++cnt[str[i]]; int ans = 0; for (int i = n - 1; i >= 0; --i) { ans += static_cast<long long>(cnt.size()); if (const auto iter = cnt.find(str[i]); iter->second == 1) cnt.erase(iter); else --iter->second; } cout << ans << endl; } } C. Watering an Array 大致题意 有两个字符串,$a, b$,长度为 $n, d$,必须要操作 $d$ 次,每次可以选择下面两个其中一个,且必须执行其中一个,假设本次是第 $x$ 次操作 将 $\forall i \in [1, d_x], a_i \leftarrow a_i + 1$ 计算 $a_i = i$ 的数量,并获得对应的分数,然后再进行 $\forall i \in [1, n], a_i \leftarrow 0$ 问最高分数可以是多少 思路 假设某次操作了 2 操作,然后因为每次会让所有值加一,所以无论操作几次,最终最多也只有一个值能满足要求 所以最好的方案是操作一次 1 然后操作一次 2,这样可以稳定拿到一分,等价于两次操作必定能拿到一次 所以核心需要关注的是最开始的 $a$ 数组的情况,因为数组仅有 2000 个,而且 $b$ 数组是一个循环数组 假设我们开始操作了 $x$ 次 1 后再进行操作 2,那么带来的最大分数就是 $n - \frac{x}{k}$ (操作 $k$ 次最多只有一次对整个数组都 +1 的情况下的分值最大,否则最大分值就是 $n - x$ 了) 而如果不那么做,直接按照上面的方案走,可以拿到 $\frac{x}{2}$ 的分数 所以得到 $\frac{x}{2} < n - \frac{x}{k} \rightarrow (1 + \frac{2}{k}) x < 2n \rightarrow x < 2n$ 所以考虑 $2n$ 以内的情况即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k, d; cin >> n >> k >> d; vector<int> data(n), v(k); for (auto& i: data) cin >> i; for (auto& i: v) cin >> i; // init ans int ans = 0; for (int i = 0; i < n; ++i) if (data[i] == i + 1) ++ans; ans += (d - 1) / 2; for (int i = 0; i < min(2 * n, d - 1); ++i) { int tmp = 0; for (int j = 0; j < n; ++j) { if (j < v[i % k]) ++data[j]; if (data[j] == j + 1) ++tmp; } tmp += (d - 2 - i) / 2; ans = max(ans, tmp); } cout << ans << endl; } } D. Yet Another Inversions Problem 大致题意 给出两个初始的数组 $p, q$,其中 $p$ 数组必定是 $[1, 2n-1]$ 中的所有奇数的排列,而 $q$ 数组一定是 $[0, k - 1]$ 的排列 现在构建一个新的数组 $a_{i \times k + j} = p_i \times 2^{q_j}$ 问这个数组有多少个逆序对 思路 首先是 $q$ 数组本身的逆序,这种情况下,自己和自己就可以产生逆序对,这个部分可以通过归并排序解决,所以单独处理即可 接下来考虑不同数值之间的情况,显然可以通过 01 字典树完成。注意因为乘法其实就是左移,所以可以直接用每个字符串的最大比特位开始建树, 记录在经过某个节点的时候,剩下多长的比特位。 然后再遍历数组,每次遍历的点从树中移除,并且在遍历到某个节点的时候,需要关注一下它的姐妹节点上有多少数值经过了, 可以根据其剩下的比特位,推算出有多少的组合可以比它大,还需要关注在当前节点结束的数值的情况, 以及如果当前自己已经结束,那么剩下经过这个点的其他字符串的情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 #define ll long long void solve() { int _; cin >> _; vector<vector<int>> tree(2e5 * 2), next(2e5 * 2); vector<int> end(2e5 * 2); for (auto& i: tree) i.resize(20, 0); for (auto& i: next) i.resize(2, 0); for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; vector<int> p(n), q(k), cache(k); for (auto& i: p) cin >> i; for (auto& i: q) cin >> i; int root, tail = 0; auto build = [&] { for (int i = 0; i < 20; ++i) tree[tail][i] = 0; next[tail][0] = next[tail][1] = -1; end[tail] = 0; assert(tail < tree.size()); return tail++; }; root = build(); auto move = [&](const int x, const int v) { int i = 20; while (i >= 0 && (x & 1 << i) == 0) --i; int cur = root; while (i >= 0) { const int flag = x & 1 << i ? 1 : 0; if (next[cur][flag] == -1) next[cur][flag] = build(); cur = next[cur][flag]; tree[cur][i] += v; --i; } end[cur] += v; }; for (const auto& pi: p) move(pi, 1); ll ans = 0; constexpr ll mod = 998244353; auto pos = [&](const int gap, const int cnt) { if (gap >= k) return; const ll ps = 1ll * (k - gap + 1) * (k - gap) / 2 % mod; const ll tmp = ps * cnt % mod; ans = (ans + tmp) % mod; }; auto neg = [&](int gap, const int cnt) { gap += 1; if (gap > k) return; const ll ps = 1ll * (k - gap + 1) * (k - gap) / 2 % mod; const ll all = 1ll * (k + 1) * k / 2 % mod; const ll tmp = (all - ps - k) * cnt % mod; ans = (ans + tmp) % mod; }; for (const auto& pi: p) { move(pi, -1); int i = 20; while (i >= 0 && (pi & 1 << i) == 0) --i; int cur = root; while (i >= 0) { const int flag = pi & 1 << i ? 1 : 0; if (const int other = next[cur][flag ^ 1]; other != -1) for (int ind = 0; ind < 20; ++ind) if (tree[other][ind] > 0) { const int gap = ind + (flag ^ 1) - i; pos(max(0, gap), tree[other][ind]); if (gap < 0) neg(-gap, tree[other][ind]); } cur = next[cur][flag]; const int gap = max(-i, -k + 1); pos(max(0, gap), end[cur]); if (gap < 0) neg(-gap, end[cur]); --i; } auto cal = [&](const int node) { if (node == -1) return; for (int ind = 0; ind < 20; ++ind) if (tree[node][ind] > 0) { const int gap = ind + 2; pos(max(0, gap), tree[node][ind]); if (gap < 0) neg(-gap, tree[node][ind]); } }; cal(next[cur][0]); cal(next[cur][1]); } function<ll(vector<int>&, vector<int>&, int, int)> mergeSort = [&](vector<int>& record, vector<int>& tmp, const int l, const int r) { if (l >= r) return 0ll; const int mid = (l + r) / 2; ll inv_count = mergeSort(record, tmp, l, mid) + mergeSort(record, tmp, mid + 1, r); int i = l, j = mid + 1, poss = l; while (i <= mid && j <= r) { if (record[i] <= record[j]) { tmp[poss] = record[i]; ++i; inv_count += j - (mid + 1); } else { tmp[poss] = record[j]; ++j; } ++poss; } for (int ind = i; ind <= mid; ++ind) { tmp[poss++] = record[ind]; inv_count += j - (mid + 1); } for (int ind = j; ind <= r; ++ind) { tmp[poss++] = record[ind]; } copy(tmp.begin() + l, tmp.begin() + r + 1, record.begin() + l); return inv_count; }; const ll cnt = mergeSort(q, cache, 0, k - 1); const ll tmp = cnt * n % mod; ans = (ans + tmp) % mod; cout << ans << endl; } }

2024/2/25
articleCard.readMore

Pinely Round 3 (Div. 1 + Div. 2)

A. Distinct Buttons 大致题意 初始在 $(0, 0)$ 点,问是否可能只往三个方向移动的情况下,到达所有给出的点位,不需要按照顺序 思路 看看所有点是不是都在两个相邻的象限内即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; bool flag[4] = {true, true, true, true}; for (int i = 0; i < n; ++i) { int u, v; cin >> u >> v; if (u > 0) flag[0] = false; if (u < 0) flag[1] = false; if (v > 0) flag[2] = false; if (v < 0) flag[3] = false; } cout << (flag[0] || flag[1] || flag[2] || flag[3] ? "YES" : "NO") << endl; } } B. Make Almost Equal With Mod 大致题意 有一个数组,允许你挑选一个值,让所有值 mod 它之后,剩下的值中至少有两个不一样的,问可能的选择 思路 从二进制角度看,找到最后一位大家都不一样的,然后取比它大一点的那个 $2^n$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int k = 2; while (true) { set<int> tmp; for (int i = 0; i < n; ++i) tmp.insert(data[i] % k); if (tmp.size() > 1) { cout << k << endl; break; } k <<= 1; } } } C. Heavy Intervals 大致题意 有一堆区间$[l_i, r_i]$和相同数量的以及数组 $c$,问是否可以通过重新排列每个区间的 $l$, $r$ 以及 $c$,使得 $\sum_{i=1}^n c_i \times (r_i - l_i)$ 最小 思路 可以得到,要让最大的 $c$ 去匹配最小的区间即可,所以要尽可能制造 $(r_i - l_i)$ 之和不变的情况下,区间差异最大 所以可以从大到小遍历 $r$ 去找对应第一个匹配的 $l$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> l(n), c(n); map<int, int> r; for (auto& i: l) cin >> i; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++r[tmp]; } for (auto& i: c) cin >> i; sort(l.begin(), l.end(), greater<>()); sort(c.begin(), c.end()); vector<int> len(n); for (int i = 0; i < n; ++i) { const auto iter = r.upper_bound(l[i]); len[i] = iter->first - l[i]; if (iter->second == 1) r.erase(iter); else --iter->second; } sort(len.begin(), len.end()); int ans = 0; for (int i = 0; i < n; ++i) ans += len[i] * c[n - 1 - i]; cout << ans << endl; } } D. Split Plus K 大致题意 有一个初始的数组,允许每次选择其中的一个值,让其加上给出的 $k$,然后拆成两个, 问经过多少次操作后,整个数组的所有值相同 思路 假设最终的值为 $m$,可以得到 $a_i = m \times t_i - (t_i - 1) \times k$ 化简得到 $a_i - k = t_i \times (m - k)$ 由于都是整数,且所有 $i$ 的 $m - k$ 相同,则可以得到 $m - k$ 可以是 $gcd_{i=1}^n (a_i - k)$ 那么就简单了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define int long long // NOLINTNEXTLINE(*-no-recursion) auto gcd(const int a, const int b) -> int { if (b == 0) return a; return gcd(b, a % b); } void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; vector<int> data(n); for (auto& i: data) cin >> i; if (n == 1) { cout << 0 << endl; continue; } int mk = data[0] - k; for (int i = 1; i < n; ++i) mk = gcd(mk, data[i] - k); if (mk == 0) { cout << 0 << endl; continue; } int ans = LONG_LONG_MAX; auto check = [&](int m) { if (m == k) return false; bool flag = true; int tmp = 0; for (int i = 0; i < n; ++i) { if ((data[i] - m) % (m - k) != 0 || (data[i] - m) / (m - k) < 0) { flag = false; break; } tmp += (data[i] - m) / (m - k); } if (flag) ans = min(ans, tmp); return flag; }; check(mk + k); cout << (ans == LONG_LONG_MAX ? -1 : ans) << endl; } }

2024/2/24
articleCard.readMore

Educational Codeforces Round 160 (Rated for Div. 2)

A. Rating Increase 大致题意 有两个分数,并列写在一起了,已知第一个分数一定小于第二个分数,问是否可能,并给出一种拆法 思路 找到第二个非 0 的值前面拆开就行,就是最优的情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { string str; str.reserve(10); cin >> str; int a[2] = {str.front() - '0', 0}, cur = 0; for (int i = 1; i < str.size(); ++i) { if (cur == 0 && str[i] != '0') { cur = 1; } a[cur] *= 10; a[cur] += str[i] - '0'; } if (a[0] < a[1]) cout << a[0] << ' ' << a[1] << endl; else cout << -1 << endl; } } B. Swap and Delete 大致题意 有一个 $01$ 字符串,允许选择一个字符进行删除(并消耗一点成本),或者交换两个值(不消耗成本), 问是否可以经过任意次数操作后,使得新的字符串和原来的字符串没有一个字符相同 思路 从头开始尽力使用交换即可,如果遇到一个字符不能靠交换解决了,那么其后面的字符都得删掉 因为是要与原始字符串不同,仅删掉这个字符,后面的字符就到这个字符的位置了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; string str; str.reserve(2e5 + 10); for (int tc = 0; tc < _; ++tc) { cin >> str; int cnt[2] = {}; for (const auto& c: str) ++cnt[c - '0']; int ans = 0; for (int i = 0; i < str.size(); ++i) { if (cnt[(str[i] - '0') ^ 1]) --cnt[(str[i] - '0') ^ 1]; else { ans = static_cast<int>(str.size()) - i; break; } } cout << ans << endl; } } C. Game with Multiset 大致题意 有一个有 $2^n$ 组成的集合,每次允许往里面添加值,问是否可以靠这几个值相加得到某个具体的值 思路 从二进制角度考虑即可,为每一个位置进行凑,不足就让下面的位置进上来 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int n; cin >> n; int cnt[30] = {}; for (int q = 0; q < n; ++q) { int op, v; cin >> op >> v; if (op == 1) ++cnt[v]; else { int last = 0; for (int i = 29; i >= 0; --i) { last <<= 1; int cur = last + ((v & (1 << i)) ? 1 : 0); last = max(0, cur - cnt[i]); } cout << (last == 0 ? "YES" : "NO") << endl; } } }

2024/2/17
articleCard.readMore

Codeforces Round 915 (Div. 2)

A. Constructive Problems 大致题意 有一个棋盘,允许选择一定数量的方格先进行染色 若某个方格的相邻四个格子中,横向至少有一个已经染色,且纵向至少也有一个已经染色的情况下,那么这个格子也可以被自然染色 问最少最初选择的方格数量是多少 思路 对角线即可 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b; cin >> a >> b; cout << max(a, b) << endl; } } B. Begginer’s Zelda 大致题意 有一棵树,允许每次选择树上的一条路径,然后将路径上的所有的点都挤压到一个点上,问最多需要挤压几次才能让整个树变成一个点 思路 其实只需要统计叶子结点数量就行了,然后两两连线挤压即可,必定存在一种方法使得整个树的所有边被遍历 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> deg(n + 1, 0); for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; ++deg[u]; ++deg[v]; } int cnt = 0; for (int i = 1; i <= n; ++i) cnt += deg[i] == 1; cout << (cnt + 1) / 2 << endl; } } C. Largest Subsequence 大致题意 有一个字符串,接下来有如下操作 找到这个字符串中字典序最大子序列 将这个子序列进行右移操作,仅对序列内的值生效 问需要操作几次才能使得整个数组有序 思路 因为是字典序最大子序列,那么必然得到的子序列是一个递减的序列。 而要求是右移,即把最后面的值放到最前面,那么必然放到最前面的是最小的那个值,那么必然下一次得到字典序最大子序列的时候,必定不会包含这个值了 也就是说,实际上每次操作后,下一次得到的子序列就是上一次的子序列删掉最开头的位置和最后面的那个值,即排序操作仅对这个子序列生效 所以只需要看这个子序列需要操作几次才能有序,以及是否能够保证整个序列有序 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.reserve(n); cin >> str; vector<int> st(n); int r = 0; for (int i = 0; i < n; ++i) { while (r > 0 && str[st[r - 1]] < str[i]) --r; st[r++] = i; } int ans = r; for (int i = 0; i < r; ++i) if (str[st[i]] == str[st[0]]) --ans; --r; for (int l = 0; l < r; ++l, --r) swap(str[st[l]], str[st[r]]); for (int i = 1; i < n; ++i) if (str[i] < str[i - 1]) ans = -1; cout << ans << endl; } } D. Cyclic MEX 大致题意 有一个 $[0, n-1]$ 的排列,允许进行任意次的右移操作 问找到一种的排列,使得 $\sum_{i=1}^n mex([a_1, a_2, \dots a_{i}])$ 最大 思路 这种数组的 $mex$ 其实就等于要求算的那个值后面的值中最小的那个值 考虑每次右移带来的效果 首先是最前面的那个 $mex([a_1])$ 会被删除掉 然后影响从最后一个开始,找到第一个比当前值更小的值,这期间的所有值带来的贡献都变成当前值 然后再加上固定 $n$ 的贡献 所以可以考虑单调栈的方式去做 但是我觉得这个方案有点累,所以直接用线段树了。虽然说是仅影响了更小的那个值以及后面的值 但是要明确的是,那个更小的值前面的带来的贡献,必然小于等于那个更小的值,所以只需要全局把贡献降低到当前值即可 用样例举个例子 index12345678 start23670145 mex00001458 rotate36701452 mex00012228 可以看到,首先是 $2$ 移动到后面去了,贡献变成了 $8$ 然后是可以注意到,因为 $2$ 在最后面,所以所有值的贡献是不可能超过 $2$ 的,因为它必然是所有值后面的值 所以直接用线段树暴力即可,注意做好懒处理 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #define ll long long struct SegTree { vector<ll> sum, ma, mi; vector<bool> lazy; explicit SegTree(const int n) : sum((n << 1) + 10), ma((n << 1) + 10), mi((n << 1) + 10), lazy((n << 1) + 10) {} static int get(const int l, const int r) { return (l + r) | (l != r); } void up(const int l, const int r) { const int mid = (l + r) >> 1; const int i = get(l, r), li = get(l, mid), ri = get(mid + 1, r); sum[i] = sum[li] + sum[ri]; ma[i] = max(ma[li], ma[ri]); mi[i] = min(mi[li], mi[ri]); } void push(const int l, const int r) { const int mid = (l + r) >> 1; const int i = get(l, r), li = get(l, mid), ri = get(mid + 1, r); lazy[i] = false; lazy[li] = true; lazy[ri] = true; sum[li] = (mid - l + 1) * mi[i]; sum[ri] = (r - mid) * mi[i]; ma[li] = ma[ri] = ma[i]; mi[li] = mi[ri] = mi[i]; } void update(const int l, const int r, const int x, const ll v) { const int i = get(l, r); if (l == r) { sum[i] = ma[i] = mi[i] = v; return; } if (lazy[i]) push(l, r); const int mid = (l + r) >> 1; if (x <= mid) update(l, mid, x, v); else update(mid + 1, r, x, v); up(l, r); } void update(const int l, const int r, const ll v) { int i = get(l, r); if (ma[i] <= v) return; if (mi[i] > v) { ma[i] = mi[i] = v; sum[i] = (r - l + 1) * v; lazy[i] = true; return; } if (l == r) { sum[i] = ma[i] = mi[i] = v; return; } const int mid = (l + r) >> 1; update(l, mid, v); update(mid + 1, r, v); up(l, r); } }; void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; vector flag(n + 1, false); int l = 1, r = n; SegTree tree(r); // init int cur = 0; for (int i = 0; i < n; ++i) { flag[data[i]] = true; while (flag[cur]) ++cur; tree.update(l, r, i + 1, cur); } ll ans = 0; for (int i = 0; i < n; ++i) { tree.update(l, r, data[i]); tree.update(l, r, i + 1, n); ans = max(ans, tree.sum[tree.get(l, r)]); } cout << ans << endl; } }

2024/2/17
articleCard.readMore

Codeforces Round 914 (Div. 2)

A. Forked! 大致题意 棋盘上有一个皇后和一个国王,且骑士的移动方式是给出的 “日” 字形式,存在几个位置,使得骑士可以同时吃国王和皇后 思路 虽然不是 “日” 字,但是一个骑士最多也就只能走 8 个位置,所以暴力枚举就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int a, b, xk, yk, xq, yq; cin >> a >> b >> xk >> yk >> xq >> yq; const int arr[4][2] = {1, 1, 1, -1, -1, 1, -1, -1}; set<pair<int, int>> s; for (const auto ar: arr) { s.insert({xk + a * ar[0], yk + b * ar[1]}); s.insert({xk + b * ar[0], yk + a * ar[1]}); } int ans = 0; for (const auto ar: arr) { if (s.count({xq + a * ar[0], yq + b * ar[1]})) { s.erase({xq + a * ar[0], yq + b * ar[1]}); ++ans; } if (s.count({xq + b * ar[0], yq + a * ar[1]})) { s.erase({xq + b * ar[0], yq + a * ar[1]}); ++ans; } } cout << ans << endl; } } B. Collecting Game 大致题意 有一个数组,选择其中的一个值作为初始值,然后每次进行如下操作: 选择一个剩下的值,满足其小于当前的值 删除它,并把其值加到当前值上 问每一位的值作为初始值的情况下,最多可以干掉多少值 思路 因为每一个值都可以干掉它以及比它小的值,所以只需要这些值加起来比恰好大于它的值还要大的话,那么就可以继续增大 而最终的结果一定是卡在某个值处,使得所有逼比它小的值加起来都没有它大,那么这个时候,比它小的那个值必然只能消除到这个位置 依次类推,只需要依次找到满足 $\sum_{i=1}^x a_i < a_{x+1}$ 即可,那么必然可以类似前缀和一样处理就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<pair<int, int>> data(n); for (auto& [fst, snd]: data) cin >> fst; for (int i = 0; i < n; ++i) data[i].second = i; sort(data.begin(), data.end()); vector<int> ans(n); int l = 0, sum = 0; while (l < n) { int r = l + 1; sum += data[l].first; while (r < n && sum >= data[r].first) { sum += data[r].first; ++r; } for (int i = l; i < r; ++i) ans[data[i].second] = r - 1; l = r; } for (int i = 0; i < n; ++i) cout << ans[i] << " \n"[i == n - 1]; } } C. Array Game 大致题意 有一个数组,允许你每次选择其中的两个值,把它们的差值加入到队列中,问经过 $k$ 次操作后,数组中最小的值最小可能是多少 思路 如果操作三次,那么可以连续两次拿同两个值,然后再让结果的那两值的差值加入数组,那么必然得到 $0$,即最小的值 所以问题只需要考虑一次操作和两次操作即可 一次操作很简单,$n^2$ 暴力扫就行了 二次操作也很简单,$n^2$ 暴力扫的同时,将结果和原始数组中看看,是否有足够相近的值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define int long long void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n, k; cin >> n >> k; set<int> s; vector<int> data(n); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; data[i] = tmp; s.insert(tmp); } if (k >= 3) { cout << 0 << endl; continue; } if (k == 0) { cout << *s.begin() << endl; continue; } int ans1 = *s.begin(), ans2 = *s.begin(); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { int tmp = abs(data[i] - data[j]); auto riter = s.upper_bound(tmp); auto liter = riter; if (liter != s.begin()) --liter; if (riter == s.end()) --riter; ans1 = min(ans1, tmp); ans2 = min(min(ans2, tmp), min(abs(*riter - tmp), abs(*liter - tmp))); } } if (k == 1) cout << ans1 << endl; else cout << ans2 << endl; } } D2. Set To Max (Hard Version) 大致题意 有一个数组,允许每次操作选择一个区间 $[l, r]$,使得 $a_i \leftarrow max(a_{l \dots r}), \forall i \in [l, r]$ 问只操作数组 $a$ 的情况下是否有可能做到 思路 首先,数组 $a$ 里面的每一个值,必然有其最大的作用范围,毕竟是取区间最大,可以通过两次单调栈的方式来找到每个值可以作用到的最大区间 然后只需要找到每一个值所来自哪个值的效果即可,可以用双指针在两个数组上移动即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> a(n), b(n), la(n, 0), ra(n, n); int r = n - 1; for (auto& i: a) cin >> i; for (auto& i: b) cin >> i; stack<int> st; for (int i = 0; i < n; ++i) { while (!st.empty() && a[st.top()] < a[i]) { ra[st.top()] = i; st.pop(); } st.push(i); } while (!st.empty()) st.pop(); for (int i = n - 1; i >= 0; --i) { while (!st.empty() && a[st.top()] < a[i]) { la[st.top()] = i + 1; st.pop(); } st.push(i); } while (!st.empty()) st.pop(); bool flag = true; for (int i = n - 1; i >= 0; --i) { while (r >= 0 && (a[r] != b[i] || la[r] > i || ra[r] <= i)) --r; if (a[r] != b[i] || la[r] > i || ra[r] <= i) { flag = false; break; } } cout << (flag ? "YES" : "NO") << endl; } }

2024/2/15
articleCard.readMore

Codeforces Round 913 (Div. 3)

A. Rook 大致题意 有一个棋盘,上有一个城堡,问这个城堡能走到哪些格子 思路 把横向和纵向的都枚举出来就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { string str; cin >> str; for (int i = 0; i < 8; ++i) { if (str[0] != 'a' + i) cout << static_cast<char>('a' + i) << str[1] << endl; if (str[1] != '1' + i) cout << str[0] << i + 1 << endl; } } } B. YetnotherrokenKeoard 大致题意 有一个键盘,如果输入 B 则删除最后输入的大写字母,如果输入的是 b 则删除最后输入的小写字母,给出输入的字母,问最终输出什么 思路 从后往前遍历去做就比较简单了,统计还有一个 B/b 没有处理过即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; string str; str.reserve(1e6 + 10); for (int tc = 0; tc < _; ++tc) { cin >> str; list<char> l; int cnt[2] = {0, 0}; for (auto iter = str.rbegin(); iter != str.rend(); ++iter) { if (iter.operator*() == 'b') ++cnt[0]; else if (iter.operator*() == 'B') ++cnt[1]; else { if (iter.operator*() >= 'A' && iter.operator*() <= 'Z' && cnt[1]) --cnt[1]; else if (iter.operator*() >= 'a' && iter.operator*() <= 'z' && cnt[0]) --cnt[0]; else l.push_front(iter.operator*()); } } for (const auto& c: l) cout << c; cout << endl; } } C. Removal of Unattractive Pairs 大致题意 每次可以选择两个相邻的字符,如果不同则同时删除,问最后最少是多少个字符 思路 简单题,如果有一个字符的数量超过一半,那就不行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; string str; str.reserve(1e5 + 10); for (int tc = 0; tc < _; ++tc) { int n; cin >> n >> str; int cnt[26] = {}; for (const auto& c: str) ++cnt[c - 'a']; bool flag = false; for (const int i : cnt) { if (i * 2 > n) { cout << i * 2 - n << endl; flag = true; } } if (!flag) cout << (n % 2 ? 1 : 0) << endl; } } D. Jumping Through Segments 大致题意 有 $n$ 个线段,落在 x 轴上,要求从 $0$ 点开始,每次允许往前或者往后走至多 $k$ 步,使得当走完第 $i$ 步的时候,恰好落在第 $i$ 个线段上,问最小的 $k$ 思路 二分 $k$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<pair<int, int>> data(n); for (auto& [fst, snd]: data) cin >> fst >> snd; int ml = 0; for (const auto& [fst, snd]: data) ml = max(fst, ml); if (ml == 0) { cout << 0 << endl; continue; } int l = 0, r = 1e9 + 10; auto check = [&](const int x) { int bl = 0, br = 0; for (const auto& [fst, snd]: data) { bl -= x; br += x; bl = max(bl, fst); br = min(br, snd); if (bl > br) return false; } return true; }; while (l + 1 < r) { if (const int mid = (l + r) >> 1; check(mid)) r = mid; else l = mid; } cout << r << endl; } } E. Good Triples 大致题意 定义 $digsum(x)$ 等于其每一位的数值相加的结果 问是否存在组合 $(a, b, c)$,使得 $a + b + c = n$ 且 $digsum(a) + digsum(b) + digsum(c) = digsum(n)$ 其中 $n$ 为给出的值 思路 从十进制角度考虑问题,从每一位看,三个值每一位可以是 $[0, 9]$。 可以考虑从高位开始逐位枚举当前位的值,因为任意位置最多只能是 $27$,所以每一个位置,可能被下面的位置借走两个值, 所以每一个位置的可能的值是 $x, x-1, x-2$,而同时也需要把下面的位置加上对应的借位的值 每一位的值可能是 $[0, 27]$,每个值所能得到的可能的排列是确定的,只需要将每个位置的排列可能性乘起来就行,做个 dfs 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void solve() { int _; cin >> _; int base[28] = {}; for (int a = 0; a < 10; ++a) for (int b = 0; b < 10; ++b) for (int c = 0; c < 10; ++c) ++base[a + b + c]; for (int tc = 0; tc < _; ++tc) { int n, tot = 0; cin >> n; int tmp = n, index = 7, arr[8] = {}; while (tmp) { arr[index--] = tmp % 10; tot += tmp % 10; tmp /= 10; } long long ans = 0, cur = 1; function<void(int)> dfs = [&](const int i) { if (arr[i] > 27) return; if (i == 7) { int sum = 0; for (const auto& x: arr) sum += x; if (sum == tot) ans += cur * base[arr[i]]; } else { for (int d = 0; d < 3; ++d) { if (arr[i] < d) continue; arr[i] -= d; arr[i + 1] += d * 10; cur *= base[arr[i]]; dfs(i + 1); cur /= base[arr[i]]; arr[i] += d; arr[i + 1] -= d * 10; } } }; dfs(0); cout << ans << endl; } } F. Shift and Reverse 大致题意 有一个数组,每次操作允许进行两个操作其中之一 把最后一个值放到最前面 翻转整个数组 问是否可能通过操作,使得数组变得非递减 思路 有点类似切牌的操作,这么搞最终都是数组原序列的翻转,所以需要数组本身基本有序才行 所以只需要搞清楚是把后面的数直接往前拿,还是说是先翻转后再拿即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int cnt[2] = {}; for (int i = 1; i < n; ++i) if (data[i - 1] > data[i]) ++cnt[0]; else if (data[i - 1] < data[i]) ++cnt[1]; if (cnt[0] > 1 && cnt[1] > 1) { cout << -1 << endl; continue; } if (cnt[0] == 0) { cout << 0 << endl; continue; } if (cnt[1] == 0) { cout << 1 << endl; continue; } int ans = INT_MAX; if (cnt[0] == 1) { if (data.back() <= data.front()) { int key = 0; for (int i = 1; i < n; ++i) if (data[i - 1] > data[i]) key = i; ans = min(min(n - key, key + 2), ans); } } if (cnt[1] == 1) { if (data.back() >= data.front()) { int key = 0; for (int i = 1; i < n; ++i) if (data[i - 1] < data[i]) key = i; ans = min(min(n - key + 1, key + 1), ans); } } cout << (ans == INT_MAX ? -1 : ans) << endl; } } G. Lights 大致题意 有 $n$ 盏灯,$n$ 个开关,每个开关管理两个灯,$i, a_i$,每次使用开关可以把这两盏灯的状态翻转, 问是否存在一种开关方法,使得所有灯被关闭 思路 因为一个开关必定可以改变当前灯的状态,以及改变另外一个灯的状态,所以可以得到一张图, 然后根据拓扑序,如果当前节点是开灯的,那么必然得使用这盏灯的开关,因为这是最后能改变灯状态的开关了,最后可能会成环,没办法拓扑序了 因为每次关灯,会影响到两个灯的状态,所以一个环上必须要恰好还剩下偶数盏灯没有被关闭才行,然后再环上找小弧即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 void solve() { int _; cin >> _; for (int tc = 0; tc < _; ++tc) { int n; cin >> n; string str; str.reserve(n); vector<int> nxt(n), deg(n, 0), ans; cin >> str; for (auto& i: nxt) cin >> i; for (auto& i: nxt) ++deg[--i]; queue<int> q; for (int i = 0; i < n; ++i) if (!deg[i]) q.push(i); while (!q.empty()) { const auto cur = q.front(); q.pop(); if (str[cur] == '1') { ans.push_back(cur); str[cur] = '0'; str[nxt[cur]] = str[nxt[cur]] == '0' ? '1' : '0'; } --deg[nxt[cur]]; if (!deg[nxt[cur]]) q.push(nxt[cur]); } bool ret = true; for (int i = 0; i < n; ++i) { if (!deg[i] || str[i] == '0') continue; int len = 1, half = 1, cur = nxt[i], flag = str[cur] == '0', cnt = str[cur] == '0' ? 0 : 1; while (cur != i) { cur = nxt[cur]; ++len; half += flag; if (str[cur] == '1') { flag ^= 1; ++cnt; } } if (cnt % 2) { ret = false; break; } cur = i; if (half * 2 <= len) flag = 1; else flag = 0; while (len--) { if (flag) ans.push_back(cur); str[cur] = '0'; cur = nxt[cur]; if (str[cur] == '1') flag ^= 1; } } if (!ret) { cout << -1 << endl; continue; } cout << ans.size() << endl; for (int i = 0; i < ans.size(); ++i) cout << ans[i] + 1 << " \n"[i == ans.size() - 1]; } }

2024/2/14
articleCard.readMore

Educational Codeforces Round 159 (Rated for Div. 2)

A. Binary Imbalance 大致题意 有一个 01 字符串,每次允许选择两个相邻的字母中间插入一个 01 字符,如果相邻的两个字母不同则插入 0 否则插入 1 问是否可能经过任意次数操作后,字符串中的 0 比 1 多 思路 只要有 0 就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; int cnt[2] = {0, 0}; for (const auto& c: str) ++cnt[c - '0']; cout << (cnt[0] ? "YES" : "NO") << endl; } } B. Getting Points 大致题意 有 $n$ 天的学期,每一天都可以选择是学习还是休息,学习的话,会得到 $l$ 分,同时可以完成至多两个任务,每个任务可以得到 $t$ 分 任务每隔 $7$ 天会生成一个,第一个任务在第 $1$ 天生成,每个任务一旦被完成就不能再次得到分数 问在至少得到 $p$ 分的情况下,最多可以休息多少天 思路 因为任务每 $7$ 天才有一个,而每天可以完成 $2$ 个,所以必然可以把任务做完。即然要休息时间足够久,那么必然是最后几天完成任务即可 所以只需要考虑最后需要多少天进行学习即可。 当然也可以考虑分类讨论,比如所有天都是完成两个任务的学习,或者个别天是不做任何任务的学习 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, p, l, t; cin >> n >> p >> l >> t; const int cnt = (n + 6) / 7, d = (cnt + 1) / 2; if (d * l + t * cnt >= p) cout << n - ((2 * t + l + p - 1) / (2 * t + l)) << endl; else cout << n - ((p - cnt * t + l - 1) / l) << endl; } } C. Insert and Equalize 大致题意 有一个初始的数组,每个值不同,需要往里面加入一个任选的值,让数组仍然保持不同 然后进行 $t$ 次操作,每次操作是选择一个值并将其加上 $x$,问让所有值都相同的,最少多少的 $t$ 思路 先不管加入一个值这个事情,如果只是纯粹的做加法,那么非常简单,只需要找到所有差值的最大公约数就行了,那么这个公约数就是 $x$ 那么也就可以得到 $t$ 了,即所有值变成 $a_{max}$ 所需要的步数 接下来是加入一个值的部分,因为必须要和原来的数组不同,所以可以考虑尝试 $a_{max} - c \times x$,而 $c$ 就是需要额外增加的成本,所以要尽可能小即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #define int long long int gcd(int a, int b) { if (b == 0) return a; return gcd(b, a % b); } void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; if (n == 1) { cout << 1 << endl; continue; } sort(data.begin(), data.end()); set<int> dif; for (int i = 0; i < n - 1; ++i) dif.insert(data[i + 1] - data[i]); int g = dif.begin().operator*(); for (const auto& s: dif) g = gcd(g, s); int ans = 0; for (const auto& i: data) ans += (data[n - 1] - i) / g; if (dif.size() == 1) ans += n; else { for (int i = n - 2; i >= 0; --i) { if (const int tmp = n - 1 - i; data[i] + tmp * g != data[n - 1]) { ans += tmp; break; } } } cout << ans << endl; } } E. Collapsing Strings 大致题意 有一堆的字符串,使用 $\left | x \right |$ 表示 $x$ 的字符串长度 定义一个函数,$C(a, b)$,其中 $a, b$ 都是字符串 $$C(a, b) =\left\{\begin{matrix}a, & \left | b \right | = 0 \\b, & \left | a \right | = 0 \\C(a_{1 \dots \left | a \right | - 1}, b_{2 \dots \left | b \right |}), & a_{\left | a \right |} = b_1 \\a + b, & others\end{matrix}\right.$$ 问$$\sum_{i=1}^{n} \sum_{j=1}^{n} \left | C(s_i, s_j) \right |$$ 思路 其实就是要找出任意两个字符串之间,前后重叠的部分即可 可以考虑用字典树做,每个字典树的节点上记录下当前节点有多少字符串在这里结束了,又有多少字符串经过了这个节点,总共有多少个字符在其子节点上即可 然后再扫一遍全部的字符串即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void solve() { int n; cin >> n; string str; str.resize(1e6 + 10, -1); struct node { int arr[26]{}; int len = 0, cnt = 0, end = 0; node() { memset(arr, -1, sizeof arr); } }; vector<node> tree(1e6 + 10); constexpr int root = 0; int nxt = 1; vector<string> data; for (int i = 0; i < n; ++i) { cin >> str; int cur = root; for (int l = 0; l < str.size(); ++l) { if (!~tree[cur].arr[str[l] - 'a']) { tree[cur].arr[str[l] - 'a'] = nxt++; } cur = tree[cur].arr[str[l] - 'a']; tree[cur].len += static_cast<int>(str.size()) - l; ++tree[cur].cnt; } ++tree[cur].end; data.push_back(str); } long long ans = 0; for (int i = 0; i < n; ++i) { string &s = data[i]; int cur = root; for (int l = static_cast<int>(s.size()) - 1; l >= 0; --l) { for (int x = 0; x < 26; ++x) if (x != s[l] - 'a' && tree[cur].arr[x] != -1) ans += tree[tree[cur].arr[x]].len + 1LL * (l + 1) * tree[tree[cur].arr[x]].cnt; ans += 1LL * (l + 1) * tree[cur].end; cur = tree[cur].arr[s[l] - 'a']; if (!~cur) break; } if (cur != -1) for (const int x : tree[cur].arr) if (x != -1) ans += tree[x].len; } cout << ans << endl; }

2024/2/13
articleCard.readMore

Codeforces Round 912 (Div. 2)

A. Halloumi Boxes 大致题意 有一个数组,每次允许选择其中一段长度不超过 $k$ 的字串进行翻转,问是否可能将其排序好 思路 最有用的翻转就是两个值,那就是冒泡了,所以要么本身有序,要么 $k \geq 2$ 就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (auto& i: data) cin >> i; bool sorted = true; for (int i = 1; i < n; ++i) if (data[i] < data[i - 1]) sorted = false; cout << (sorted || k >= 2 ? "YES" : "NO") << endl; } } B. StORage room 大致题意 有一个矩阵,其中的每一项 $M_{i,j} = a_i | a_j$,问是否能得到原始数组的其中一种可能 思路 即然是 $M_{i,j} = a_i | a_j$,那么反过来可以得到 $a_i = M_{i,0} & M_{i,1} & M_{i,2} \dots$ 这不一定是原始解,但是一定是正确的解。还原后再验证一下矩阵对不对就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<vector<int>> data(n); for (auto& i: data) i.resize(n); for (auto& i: data) for (auto& j: i) cin >> j; vector ans(n, -1); for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) if (j != i) ans[i] &= data[i][j]; bool check = true; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (i == j) continue; if (data[i][j] != (ans[i] | ans[j])) check = false; } } if (!check) cout << "NO" << endl; else { cout << "YES" << endl; for (int i = 0; i < n; ++i) cout << (ans[i] == -1 ? 1 : ans[i]) << " \n"[i == n -1]; } } } C. Theofanis’ Nightmare 大致题意 一个数组,其中有负值,将其拆成多个字串,然后按照原始顺序从左往右编号,从 1 开始编号 然后将每个字串内求和,乘上其的编号,最后再相加,问如何拆最大 思路 从最后一个值看,假如其为 $c$,那么如果其独立出去,和不独立出去,和前面的段合并,那么得到的差值就是 $c$,因为 $(n + 1) \times c - (n) \times c$ 所以,如果 $c > 0$ 那么这样做是有意义的,反之则应该尽力避免拆 类推可以得到,如果当前值即之后的所有值加起来是 $< 0$,那么不要拆,反之则拆即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int sum = 0; for (auto& i: data) sum += i; int seg = 0, ans = 0; for (auto& i: data) { if (sum < 0) { if (seg == 0) seg = 1; ans += seg * i; } else { ++seg; ans += seg * i; } sum -= i; } cout << ans << endl; } } D1. Maximum And Queries (easy version) 大致题意 有一个数组,允许每次给其中一个值 $+1$,最多执行 $k$ 次,问最终得到的数组的每一项进行位与运算,问最大值可以是多少 思路 要从位运算上下思路 如果一个值的某个位不是 1,那么如果通过 $+1$ 的方式把它变成 1,带来的后果就是更低位置的都会归零 所以我们需要从高位开始枚举位置,假设把这个位置大家都变成 1,那么就可以带来结果上的增长,但是会需要消耗一定的 $k$ 注意一旦消耗过 $k$,那么再试图对这个值进行累加的时候,要把低位都认为是 0 了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #define int long long void solve() { int n, q; cin >> n >> q; vector<int> data(n); for (auto& i: data) cin >> i; while (q--) { int k, ans = 0; cin >> k; vector flag(n, false); for (int i = 62; i >= 0; --i) { // try this bit int cost = 0; const int p = 1LL << i; vector tmp(n, false); for (int j = 0; j < n; ++j) { if (flag[j]) cost += p; else { if (data[j] & p) continue; cost += p - data[j] % p; tmp[j] = true; } if (cost < 0) { cost = k + 1; break; } } if (k >= cost) { k -= cost; ans += p; for (int j = 0; j < n; ++j) flag[j] = tmp[j] | flag[j]; } } cout << ans << endl; } } E. Geo Game 大致题意 有两个人博弈,二维屏幕上有一些点,其中一个固定为起始点,两个人轮流指定下一个点,不可以是之前选中的点,直到所有点都被走到 然后求算路径的欧几里得距离的平方和,问能否保证是偶数或者是奇数 思路 仔细思考就会发现很像是一笔画问题 如果某个点与开始点的欧几里得距离是奇数,我将其归为一类 A,另外的点为另外一类 B。那么显然,同一个类内的点相互走并不会变化结果的奇偶性。 但是不同类的路径就会变化奇偶性。非常巧的是,因为每个点都会被进入一次和出去一次, 所以理论如果要回到开始点,那么最终一定是偶数,因为一旦进入 A 类,就一定要回去 B 类,即每个 A 类的点会产生两条路径,也就是最终没有变化奇偶性 那么我们可以得到,如果 A 类点作为最后一个点,那么路径就是奇数的,否则就是偶数,然后再根据博弈再去模拟即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, sx, sy; cin >> n >> sx >> sy; set<int> data[2]; for (int i = 0; i < n; ++i) { int u, v; cin >> u >> v; const int cost = abs(u - sx) + abs(v - sy); data[cost % 2].insert(i + 1); } if (data[0].size() >= data[1].size()) { // chose first cout << "First" << endl; for (int i = 0; i < n; ++i) { if (data[1].empty()) { auto iter = data[0].begin(); cout << *iter << endl; data[0].erase(iter); } else { auto iter = data[1].begin(); cout << *iter << endl; data[1].erase(iter); } ++i; if (i < n) { // read int tmp; cin >> tmp; data[0].erase(tmp); data[1].erase(tmp); } } } else { // chose second cout << "Second" << endl; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; data[0].erase(tmp); data[1].erase(tmp); ++i; if (i < n) { if (data[0].empty()) { auto iter = data[1].begin(); cout << *iter << endl; data[1].erase(iter); } else { auto iter = data[0].begin(); cout << *iter << endl; data[0].erase(iter); } } } } } }

2024/2/11
articleCard.readMore

Codeforces Round 911 (Div. 2)

A. Cover in Water 大致题意 有一个区域,有些地方有空有些地方有墙,可以进行如下操作 选择一个空的区域,放上水 选择已经已经有水的区域,把水移动到别的地方 如果一个地方是空的,且它的两边都是水,那么这个地方自然也有水 问最少操作多少次 1 操作,能让所有空的地方都有水 思路 由于 2 操作不计入成本,所以要多移动,而因为两个水中间会无限生成水,所以可以不断移走中间的水来实现 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; int tot = 0, ma = 0, cur = 0; for (const auto& item: str) { if (item == '.') { ++tot; ++cur; } else { ma = max(ma, cur); cur = 0; } } ma = max(ma, cur); if (ma >= 3) cout << 2 << endl; else cout << tot << endl; } } B. Laura and Operations 大致题意 有三个值,$a, b, c$,每次允许减少其中两个 $x$,然后增加剩下的那个 $x$,问是否可能最后只剩下其中一个值 思路 反过来思考,一个值可以变成剩下两个值。 如果希望最后留下的是 $a$,那么必然 $b = c$,那么可以考虑不断减少 $b/c$ 使得 $b/c$ 相同,也就是说,只要 $b+c$ 是偶数即可 同理可以得到剩下两个的解 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int a, b, c; cin >> a >> b >> c; cout << ((b + c) % 2 ? 0 : 1) << ' ' << ((a + c) % 2 ? 0 : 1) << ' ' << ((b + a) % 2 ? 0 : 1) << endl; } } C. Anji’s Binary Tree 大致题意 有一颗二叉树,每个节点上都有 LRU 三个字母的其中一个,分别表示当到达这个节点的时候如何移动,比如 L 表示移动到它的左孩子 问至少修改几个,可以从根节点出发到任意一个叶子节点 思路 树上找路径就行,每个节点选择左边和右边的代价里选较小的即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); vector<pair<int, int>> node(n); cin >> str; for (auto& [fst, snd]: node) cin >> fst >> snd; for (auto& [fst, snd]: node) { --fst; --snd; } function<int(int)> dfs = [&](const int x) { if (node[x].first == -1 && node[x].second == -1) return 0; if (node[x].first == -1) { return dfs(node[x].second) + (str[x] != 'R'); } if (node[x].second == -1) { return dfs(node[x].first) + (str[x] != 'L'); } const int l = dfs(node[x].first) + (str[x] != 'L'), r = dfs(node[x].second) + (str[x] != 'R'); return min(l, r); }; cout << dfs(0) << endl; } }

2024/2/3
articleCard.readMore

CodeTON Round 7 (Div. 1 + Div. 2, Rated, Prizes!)

A. Jagged Swaps 大致题意 有一个数组,允许选择一个值,其左右两边都是大于当前值的情况下,将当前值和后面的那个值交换一下位置。问是否可能把整个数组排序好 思路 可以从插入排序的方式去考虑,只需要第一个值是对的就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; cout << (data.front() == 1 ? "YES" : "NO") << endl; } } B. AB Flipping 大致题意 有一个 AB 组成的数组,若存在 AB 这样的子字符串,则翻转 AB,且每个下标只能被翻转一次,问最多可以翻转多少次 思路 若存在一堆连续的 AAAABBB 这样的字符串,那么仅最后一个不能进行翻转,其他所有位置都能发生翻转,所以只需要找出这样的连续对数量即可 需要注意的是,如果是 AABBAABB 这种两组的,虽然对于每一个单独的组而言,最后一个不能翻转,但是整体上,除了最后的最后那个,其他也都可以翻转 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; int ans = 0, cntB = 0, flag = 0; for (auto iter = str.rbegin(); iter != str.rend(); ++iter) { if (*iter == 'B') { flag = 1; cntB++; } else { ans += flag + cntB; cntB = 0; } } cout << max(ans - 1, 0) << endl; } } C. Matching Arrays 大致题意 有 AB 两个数组,每次任意排序 B 数组,问是否存在一个排列,使得 $a_i > b_i$ 的 $i$ 的数量恰好是 $x$ 思路 很简单的一个想法:两个数组排序后,然后将 B 数组的前 $x$ 个值放到后面去即可。然后再判断是否符合预期 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, x; cin >> n >> x; vector<pair<int, int>> a(n); vector<int> b(n); for (auto& [fst, snd]: a) cin >> fst; for (int i = 0; i < n; ++i) a[i].second = i; for (auto& i: b) cin >> i; sort(b.begin(), b.end()); sort(a.begin(), a.end()); int res = 0; for (int i = 0; i < n; ++i) { res += a[i].first > b[(i + x) % n]; } if (res == x) { cout << "YES" << endl; for (int i = 0; i < n; ++i) a[a[i].second].first = b[(i + x) % n]; for (int i = 0; i < n; ++i) cout << a[i].first << " \n"[i == n - 1]; } else cout << "NO" << endl; } } D. Ones and Twos 大致题意 有一个仅有 $1, 2$ 组成数组,每次有两种操作:将其中一个值改成 $1, 2$,问是否存在一个子串,满足求和等于 $x$(x 是每次询问给出的值) 思路 因为数组仅有 $1, 2$ 组成,那么必然,如果总值是 $s$,那么必然可以得到 $s - 2$ 的字串(删掉一侧的值或者删掉两侧的值), 同理也可以得到 $s-4, s-6, \dots$ 直到等于 $0 or 1$ 所以只需要维护总和就能解决一般的值了。 而如果需要奇偶性和之和不同,那么必然需要减去一个 $1$,那么只需要找到左右两边最近的 $1$, 然后减去那一侧的 $2$ 和第一个 $1$,就可以得到最大可以满足的和全部之和奇偶性不同的值了,然后可以继续按照上面的推论 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; vector<int> data(n); for (auto& i: data) cin >> i; int sum = 0; set<int> s; for (const auto& i: data) sum += i; for (int i = 0; i < n; ++i) if (data[i] == 1) s.insert(i); for (int qs = 0; qs < q; ++qs) { int op; cin >> op; if (op == 1) { int x; cin >> x; if (x > sum) cout << "NO" << endl; else if ((x & 1) == (sum & 1)) cout << "YES" << endl; else { if (s.empty()) { cout << "NO" << endl; continue; } const int m = min(*s.begin(), n - *s.rbegin() - 1); const int tmp = sum - m * 2 - 1; cout << (x <= tmp ? "YES" : "NO") << endl; } } else { int a, b; cin >> a >> b; if (data[a - 1] == 1) s.erase(a - 1); if (b == 1) s.insert(a - 1); sum -= data[a - 1] - b; data[a - 1] = b; } } } } E. Permutation Sorting 大致题意 有一个 $n$ 的排列的数组,每次按照如下操作进行 选出其中 $a_i \neq i$ 的 $i$,得到一个由 $i$ 组成的数组 $s$ $a_{s_{i \space mod \space k+1}} \leftarrow a_{s_i}$ 重复进行后,直到整个数组排序完成,问每一个下标完成排序需要操作几次 思路 从题意来看,其实就是每次将没有满足条件的值,右移一格,直到大家都满足了 由于在正常情况下,每次只移动一格,所以需要的成本就等于位置差值 但是因为有别的值会因为满足位置了,就不需要再路过这个节点了,也就可以省去一次移动成本,例如 index12345 start35412 turn 1235(4)1 turn 2(1)(2)(3)(4)(5) 注意到,本来初始为 $5$ 的值,本应该走 $3$ 次才能到目标地点的,但是因为 $4$ 提前到达目标地点, 所以 $5$ 只需要走两次即可,为了方便表示,我们把 $4$ 的移动描述成 $[3,4]$,同理,那么 $5$ 就是 $[2,5]$ 故我们需要找到的是,每个值要进行横跨的时候,会同时跨越的其他区间数量,即有哪些节点,他们开始的位置比当前值晚的同时,结束位置还比当前值早 我们可以用线段树来维护这样的值,如果存在一个 $[l,r]$,那么必然可以对所有 $[r]$ 的区间产生减少一次移动成本的效果 那么我们可以将 $[r+1,n]$ 的区间都 $+1$,而如何表示 $<l$,则可以通过访问顺序来控制,我们保证从左往右访问即可, 每次访问后,将当前节点带来的区间进行删除 需要注意的是,因为移动是环形的,所以需要维护两倍区间长度,不然可能会出错 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 struct SegTree { vector<int> s, laz; explicit SegTree(int n): s((n << 1) + 10), laz((n << 1) + 10) {} int static get(const int l, const int r) { return (l + r) | (l != r); } void up(const int l, const int r) { const int mid = (l + r) >> 1; s[get(l, r)] = s[get(l, mid)] + s[get(mid + 1, r)]; } void build(const int l, const int r) { laz[get(l, r)] = 0; if (l == r) { s[get(l, r)] = 0; return; } const int mid = (l + r) >> 1; build(l, mid); build(mid + 1, r); up(l, r); } void push(const int l, const int r) { const int k = get(l, r); if (laz[k]) { const int mid = (l + r) >> 1; s[get(l, mid)] += laz[k] * (mid - l + 1); s[get(mid + 1, r)] += laz[k] * (r - mid); laz[get(l, mid)] += laz[k]; laz[get(mid + 1, r)] += laz[k]; laz[k] = 0; } } void update(const int l, const int r, const int x, const int y, const int w) { if (l == x && y == r) { s[get(l, r)] += w * (r - l + 1); laz[get(l, r)] += w; return; } push(l, r); if (const int mid = (l + r) >> 1; y <= mid) { update(l, mid, x, y, w); } else if (x > mid) { update(mid + 1, r, x, y, w); } else { update(l, mid, x, mid, w); update(mid + 1, r, mid + 1, y, w); } up(l, r); } int query(const int l, const int r, const int x) { if (l == r) { return s[get(l, r)]; } push(l, r); const int mid = (l + r) >> 1; if (x <= mid) { return query(l, mid, x); } return query(mid + 1, r, x); } }; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n), ans(n); SegTree tree(2 * n); for (auto& i: data) cin >> i; for (int i = 0; i < n; ++i) { const int target = data[i] > i ? data[i] : data[i] + n; tree.update(1, 2 * n, target, 2 * n, 1); if (target < n) tree.update(1, 2 * n, target + n, 2 * n, 1); } for (int i = 0; i < n; ++i) { const int target = data[i] > i ? data[i] : data[i] + n; tree.update(1, 2 * n, target, 2 * n, -1); ans[data[i] - 1] = target - (i + 1) - tree.query(1, 2 * n, target); } for (int i = 0; i < n; ++i) cout << ans[i] << " \n"[i == n - 1]; } }

2024/1/28
articleCard.readMore

Educational Codeforces Round 158 (Rated for Div. 2)

A. Line Trip 大致题意 有一辆车,需要开到某个目的地,然后再回来,路上有几个加油站,初始的时候或者经过加油站的时候,油可以加满,问油箱的容量最小为多少 思路 注意一下最后回来那段是两段折返的路就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, x; cin >> n >> x; vector<int> data(n); for (auto& i: data) cin >> i; int ans = max(data.front(), 2 * (x - data.back())); for (int i= 1; i < n; ++i) ans = max(ans, data[i] - data[i - 1]); cout << ans << endl; } } B. Chip and Ribbon 大致题意 有一个数组,开始的每一个值都是 0,除了第一个值是 1,有一个指针指向其中一个数值 每次允许将当前指针指到下一个值,或者直接传送到另外一个任意位置,必须要进行一次移动,然后将移动后的值 $+1$ 问最小的传送次数 思路 注意每次移动只能移动到下一个值,也就是要回来删必须传送 所以对于每一个递减的子串,只取决于第一个值的代价,而第一个值的代价又和它前一个值相关,因为只要减少到和前一个值一样就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int begin = data[0], ans = data[0] - 1; for (int i = 1; i < n; ++i) if (data[i] > data[i - 1]) ans += data[i] - data[i - 1]; cout << ans << endl; } } C. Add, Divide and Floor 大致题意 有一个数组,每次允许将每一个值都加上任选的一个 $x$,然后再向下取整的方式除以 $2$。问最少需要操作多少次才能让所有值一样 思路 其实只需要考虑最大和最小的那两个值即可 考虑公式$\left \lfloor \frac{a+x}{2} \right \rfloor = \left \lfloor \frac{a}{2} + \frac{x}{2} \right \rfloor$ 可以得到 实际上 $x$ 应该尽可能小才是,否则差值并不能很快缩小 因为是向下取整,所以当最小的值是奇数的时候,且最大值是偶数的时候,这个时候全部的值加上 $1$ 就可以非常有效的降低差值, 而在其他的时候 $x$ 取 $0$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int mi = INT_MAX, ma = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mi = min(mi, tmp); ma = max(ma, tmp); } vector<int> ans; while (mi != ma) { if (mi % 2 && !(ma % 2)) { mi = (mi + 1) / 2; ma = (ma + 1) / 2; ans.push_back(1); } else { mi /= 2; ma /= 2; ans.push_back(0); } } cout << ans.size() << endl; if (ans.size() <= n) { for (const auto& i: ans) cout << i << ' '; } cout << endl; } } D. Yet Another Monster Fight 大致题意 有一组怪物,允许选择一个初始的怪物进行攻击, 攻击后,伤害会连锁伤害到其他的怪物上,连锁的顺序是随机选择一个被攻击过的怪物附近的一个没有被攻击的怪物,最终所有怪物都会被连锁到。 而连锁的伤害则是逐步递减 问在可以指定直接攻击的怪物的情况下,最小的初始攻击应该是多少,才能将所有怪都干掉 思路 由于连锁的顺序是随机的,所以对于每一个怪物而言,它的最晚承受伤害的时间就是它左边的所有怪都受到过伤害了,或者是它右边所有的怪都受到过伤害了, 至于应该是左边还是右边,那就取决于第一个怪是在它左边还是右边。 那么对于它而言,无论选择哪一个初始的怪,其需要的初始伤害是确定的,即它自身的生命值 + 它左边/右边的怪的数量 那么就可以枚举所有的初始的怪,然后找出左边所有怪里面,最大的需要是多少,和其右边里面,最大的需要是多少,然后在和当前怪的生命值取较大值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; map<int, int> l, r; int ans = INT_MAX; for (int i = 0; i < n; ++i) ++r[data[i] + i]; for (int i = 0; i < n; ++i) { if (const auto iter = r.find(data[i] + i); iter->second == 1) r.erase(iter); else --iter->second; if (i != 0) ++l[data[i - 1] + n - i]; const int ls = l.empty() ? 0 : l.rbegin()->first, rs = r.empty() ? 0 : r.rbegin()->first; ans = min(ans, max(data[i], max(ls, rs))); } cout << ans << endl; }

2024/1/19
articleCard.readMore

Codeforces Round 910 (Div. 2)

A. Milica and String 大致题意 有一个 A/B 组成的字符串,每次允许选择前 $n$ 个字母,将他们都变成 A/B,问最少的操作次数 思路 简单题,最简单的方式就是枚举每一种可能,计算结果是否符合预期就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; string str; str.reserve(n); cin >> str; int cntB = 0; for (int i = 0; i < n; ++i) cntB += str[i] == 'B'; if (cntB == k) { cout << 0 << endl; continue; } const bool useA = cntB > k; for (int i = 0; i < n; ++i) { if (cntB > k) { cntB -= str[i] == 'B'; } else { cntB += str[i] != 'B'; } if (cntB == k) { cout << 1 << endl; cout << i + 1 << ' ' << (useA ? 'A' : 'B') << endl; break; } } } } B. Milena and Admirer 大致题意 允许不断的差分一个数组中的值,问至少需要拆多少次,才能让数组非递减 思路 也比较简单,从后往前遍历,如果当前值比后面的值大,则均匀的拆成 $x$ 份,使得恰好比后面的值小或者相同即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int ans = 0; for (int i = n - 2; i >= 0; --i) { if (data[i] > data[i + 1]) { const int cut = data[i] / data[i + 1] + (data[i] % data[i + 1] == 0 ? 0 : 1); data[i] /= cut; ans += cut - 1; } } cout << ans << endl; } } C. Colorful Grid 大致题意 有一个棋盘,允许在棋盘的边上染色,然后使得从最左上角到最右下角的某一条路径长度恰好为 $k$ 的同时,路径上一定上红蓝间隔染色的 思路 即然可以重复点,那么最好的办法是找一个可以旋转的环,在里面转到足够多次就行。 因为需要红蓝间隔,那么必然最小的环就是 $4$ 个单位长度的格子,所以如果 $k$ 恰好是最短的距离加上 $4n$ 的话,就可以这样解决 但是还有一种情况,也就是不满足 $4n$ 的时候,例如 $3 \times 2$ 的方格,走 5 步,也是可以到达的(可以自己绘制一下) 故所以需要兼容上面的两种情况,我给出的一种解法如下 首先根据矩形的长边,旋选择左边或者右边的,然后固定将左上角绘制成上述形状,然后再补充移动到右下角的路径即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; const int mi = n - 1 + m - 1; if (k < mi) { cout << "NO" << endl; continue; } if (const int cut = k - mi; cut != 0 && cut % 2 == 1) { cout << "NO" << endl; continue; } if (n == 2 && m == 2) { if (k % 4 != 2) cout << "NO" << endl; else cout << "B\nB\nR R" << endl; continue; } vector<vector<char>> a(n), b(n - 1); for (auto& i: a) i.resize(m - 1); for (auto& i: b) i.resize(m); if (n >= m) { a[0][0] = a[1][0] = a[2][0] = b[0][0] = 'B'; b[0][1] = b[1][0] = b[1][1] = 'R'; for (int i = 1; i < m - 1; ++i) a[2][i] = a[2][i - 1] == 'R' ? 'B' : 'R'; const char last = b[1][m - 1]; b[1][m - 1] = a[2][m - 2]; for (int i = 2; i < n - 1; ++i) b[i][m - 1] = b[i - 1][m - 1] == 'R' ? 'B' : 'R'; b[1][m - 1] = last; } else { a[0][0] = b[0][0] = b[0][1] = b[0][2] = 'R'; a[0][1] = a[1][0] = a[1][1] = 'B'; for (int i = 1; i < n - 1; ++i) b[i][2] = b[i - 1][2] == 'R' ? 'B' : 'R'; const char last = a[n - 1][1]; a[n - 1][1] = b[n - 2][2]; for (int i = 2; i < m - 1; ++i) a[n - 1][i] = a[n - 1][i - 1] == 'R' ? 'B' : 'R'; a[n - 1][1] = last; } cout << "YES" << endl; for (const auto& i: a) { for (const auto& j: i) cout << (j ? j : 'R') << ' '; cout << endl; } for (const auto& i: b) { for (const auto& j: i) cout << (j ? j : 'R') << ' '; cout << endl; } } } D. Absolute Beauty 大致题意 有两个数组,允许交换 $b$ 数组中的两个值一次,问使得 $\sum^n_{i=1} \left | a_i - b_i \right |$ 最大的可能是多少 思路 首先得画几个图来理解,为了方便,此处先假定 $a_i < b_i$(后续可以证明可以恒定满足此等式) 那么可以得到主要有两种情况 可见,只有右边的情况是能够真正有意义的,有意义的部分是 $a_j - b_i$ 那么就需要找到最大的 $a_j - b_i$ 即可 然后我们再来看看如何证明最开始说的 $a_i < b_i$。你可以尝试将图里的 $a,b$ 交换位置,你会发现对最终的结果没有影响 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> a(n), b(n); for (auto& i: a) cin >> i; for (auto& i: b) cin >> i; for (int i = 0; i < n; ++i) if (a[i] > b[i]) swap(a[i], b[i]); int mib = 0, maa = 0; for (int i = 0; i < n; ++i) { if (b[i] < b[mib]) mib = i; if (a[i] > a[maa]) maa = i; } int ans = 0; for (int i = 0; i < n; ++i) ans += abs(a[i] - b[i]); ans += max(0LL, 2 * (a[maa] - b[mib])); cout << ans << endl; } } E. Sofia and Strings 大致题意 有一个字符串 $a$,允许你无数次操作如下的两个方法其中之一 选择其中一个片段,进行排序 删掉一个指定的字符 问是否能够变成 $b$ 字符串 思路 这里的选择片段排序,实际上最有用的就是选择两个相邻的字母排序,这样就可以最小的改动的情况下,将一个值往前移动 而需要达成这个目标,就意味着每次移动的时候,前面的值都需要比当前值大,否则前面的值只能删除。 故可以考虑遍历 $b$ 的字母,找到当前可用的最小的在 $a$ 中的位置,并将前面的那些字符移动到后面(比当前字母大),或者删除 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; string sn, sm; sn.reserve(n); sm.reserve(m); cin >> sn >> sm; vector<queue<int>> v(26); for (int i = 0; i < n; ++i) v[sn[i] - 'a'].push(i); bool flag = true; for (int i = 0; i < m; ++i) { if (v[sm[i] - 'a'].empty()) { flag = false; break; } int t = v[sm[i] - 'a'].front(); v[sm[i] - 'a'].pop(); for (int j = 0; j < sm[i] - 'a'; ++j) while (!v[j].empty() && v[j].front() < t) v[j].pop(); } cout << (flag ? "YES" : "NO") << endl; } }

2024/1/17
articleCard.readMore

Codeforces Round 909 (Div. 3)

A. Game with Integers 大致题意 有两个人 A/B 博弈,每次操作可以使一个值 $+1/-1$ 问在 A 先操作的情况下,A 操作后恰好值可以被 3 整除,则 A 获胜,给出初始值,问 A 是否可能获胜 思路 初始是 3 的倍数就不能获胜,很简单 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; cout << (n % 3 ? "First" : "Second") << endl; } } B. 250 Thousand Tons of TNT 大致题意 有 $n$ 箱 TNT,不同重量但顺序固定,有 $k$ 辆卡车,每辆卡车从第一箱 TNT 开始取,每辆车恰好分到 $\frac{n}{k}$ 个 TNT 箱。 问在所有可能的 $k$ 下,什么时候可以使得最重的卡车和最轻的卡车差值最大。 (不过,题意中题到了 MrBeast,有意思) 思路 在可能的 $k$ 下,说明必须是 $n$ 的因子,因为一个数的因子不可能很多,所以暴力扫就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; auto cal = [&](const int x) { int mi = LONG_LONG_MAX, ma = 0; for (int i = 0; i < n; i += x) { int tmp = 0; for (int j = 0; j < x; ++j) tmp += data[i + j]; mi = min(mi, tmp); ma = max(ma, tmp); } return ma - mi; }; int ans = 0; for (int i = 1; i < n; ++i) { if (n % i) continue; ans = max(cal(i), ans); } cout << ans << endl; } } C. Yarik and Array 大致题意 类似最大的连续字串和,只不过还要求必须奇偶间隔开 思路 稍微改一下 dp 转移方程即可,非常简单 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; vector<int> ans(n); ans[0] = data[0]; int res = ans[0]; for (int i = 1; i < n; ++i) { if (abs(data[i]) % 2 ^ abs(data[i - 1]) % 2) ans[i] = max(data[i], ans[i - 1] + data[i]); else ans[i] = data[i]; res = max(res, ans[i]); } cout << res << endl; } } D. Yarik and Musical Notes 大致题意 有一个数组,问有多少个对满足 $(2^{b_i})^{2^{b_j}} = (2^{b_j})^{2^{b_i}}$ 思路 $$\begin{cases}& (2^{b_i})^{2^{b_j}} & = & (2^{b_j})^{2^{b_i}} \\\Rightarrow & 2^{b_i \times 2^{b_j}} & = & 2^{b_j \times 2^{b_i}} \\\Rightarrow & b_i \times 2^{b_j} & = & b_j \times 2^{b_i} \\\Rightarrow & \frac{b_i}{b_j} & = & \frac{2^{b_i}}{2^{b_j}} \\\Rightarrow & \frac{b_i}{b_j} & = & 2^{b_i - b_j}\end{cases}$$ 设 $x = b_i - b_j$,得 $b_i = x + b_j$ 得到 $$\begin{cases}& \frac{b_j + x}{b_j} & = & 2^x \\\Rightarrow & b_j + x & = & b_j \times 2^x \\\Rightarrow & x & = & b_j \times (2^x - 1) \\\Rightarrow & b_j & = & \frac{x}{2^x - 1} \\\end{cases}$$ 绘图可以得到 仅有 $x=0,x=1$ 有正整数解,所以显然,只能恰好相同或者恰好为 $1, 2$ 可以成对 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; map<int, int> cnt; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ++cnt[tmp]; } int ans = 0; for (auto [fst, snd]: cnt) ans += snd * (snd - 1) / 2; ans += cnt[1] * cnt[2]; cout << ans << endl; } } E. Queue Sort 大致题意 每次可以把第一个值,从后往前找到第一个严格小于它的值,然后放到它后面,问操作几次可以让数组有序 思路 如果说当前值已经是最小的那个,那么每次移动一定会回到第一个,所以就会无法操作,即需要保证最小的那个出现的时候,后面的都得是有序的即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; int mi = INT_MAX; for (const auto& i: data) mi = min(mi, i); int t = 0; while (data[t] != mi) ++t; bool flag = true; for (int i = t + 1; i < n; ++i) if (data[i] < data[i - 1]) flag = false; cout << (flag ? t : -1) << endl; } } F. Alex’s whims 大致题意 有一棵树,每次允许操作其中一条边(保证还是树的情况下)使得每次操作后,存在两个叶子节点(仅有一条边即为叶子节点)的距离恰好为给出的值 给出一种初始的树以及相关的操作方式 思路 简单题,都串成链,然后把最后的一个点,要多少,就连到哪,这样距离 $1$ 节点的距离恰好就是给出的值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; vector<int> data(q); for (auto& i: data) cin >> i; for (int i = 1; i < n; ++i) cout << i << ' ' << i + 1 << endl; int cur = n - 1; for (const auto& i: data) { if (i == cur) { cout << "-1 -1 -1" << endl; continue; } cout << n << ' ' << cur << ' ' << i << endl; cur = i; } } } G. Unusual Entertainment 大致题意 有一个 $n$ 的排列的数组 $p$,以及一棵节点数为 $n$ 的树,根为 $1$ 节点 每次询问 $l, r, x$,数组中 $[l, r]$ 区间内,是否存在至少一个点 $y$,满足 $y$ 是 $x$ 的一个孩子节点或者是 $x$ 本身 思路 我的思路和大部分人的思路不太一样,有一点比较暴力的味道。 看起来这道题就要离线处理了,那么可以考虑在树上做一遍操作把所有答案都算出来 首先,如何找到一个节点全部的孩子节点,那么就可以考虑使用树上 dfs 的方式来查找,在进入 dfs 到离开 dfs 的期间,那么遇到的点都是它的孩子 如果说此时在遍历到某个节点 $n$,这个节点在上面的数组 $p$ 的位置是 $m$,且这个 $m$ 恰好出现在了它的父节点的某个询问中,即父节点询问的区间包含 $m$ 那么这个父节点的这个询问就是成功的,命中的。 那么我们需要维护的就是这个节点上面所有的父节点的询问。由于询问都是区间的模式,那么可以考虑用线段树维护,每个线段树的节点保存所遇到的询问的集合。 当遍历到某个树上的节点 $n$ 的时候,找出它所在 $p$ 中的 $m$,然后再看看这个 $m$ 在哪些父节点的询问中,对遇到的询问都标记为有结果即可。 通过这个方式,在进入某个节点的时候,将对这个节点的询问都放进线段树,离开的时候,都从线段树里取走,就可以实现在树上 dfs 期间,通过线段树完成搜索 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; struct node { int v, n; }; vector<node> edge(n * 2 - 2); vector<int> head(n + 1, -1), pos(n + 1);; vector<vector<tuple<int, int, int>>> query(n + 1); vector<pair<int, int>> qs; for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; edge[i << 1] = {v, head[u]}; edge[i << 1 | 1] = {u, head[v]}; head[u] = i << 1; head[v] = i << 1 | 1; } for (int i = 1; i <= n; ++i) { int tmp; cin >> tmp; pos[tmp] = i; } for (int i = 0; i < q; ++i) { int l, r, x; cin >> l >> r >> x; qs.emplace_back(l, r); query[x].emplace_back(l, r, i); } vector<set<int>> tree(n * 2 + 10); auto get = [](const int l, const int r) { return (l + r) | (l != r); }; function<void(int, int, int, int, int)> _add = [&](const int l, const int r, const int x, const int y, const int v) { const int mid = l + r >> 1; if (x == l && y == r) { tree[get(l, r)].insert(v); return; } if (y <= mid) _add(l, mid, x, y, v); else if (x > mid) _add(mid + 1, r, x, y, v); else { _add(l, mid, x, mid, v); _add(mid + 1, r, mid + 1, y, v); } }; function<bool(int, int, int, int, int)> _del = [&](const int l, const int r, const int x, const int y, const int v) { const int mid = l + r >> 1; if (x == l && y == r) { return tree[get(l, r)].erase(v) ? true : false; } if (y <= mid) return _del(l, mid, x, y, v); if (x > mid) return _del(mid + 1, r, x, y, v); return _del(l, mid, x, mid, v) && _del(mid + 1, r, mid + 1, y, v); }; function<void(int, int, int, vector<int>&)> _find = [&](const int l, const int r, const int v, vector<int>& res) { for (const auto& i: tree[get(l, r)]) res.push_back(i); if (l == r) return; if (const int mid = l + r >> 1; v <= mid) { _find(l, mid, v, res); } else if (v > mid) { _find(mid + 1, r, v, res); } }; vector ans(q, false); vector<int> res; res.reserve(n); function<void(int, int)> dfs = [&](const int u, const int p) { for (const auto [l, r, i]: query[u]) _add(1, n, l, r, i); _find(1, n, pos[u], res); for (const auto& i: res) { ans[i] = true; _del(1, n, qs[i].first, qs[i].second, i); } res.clear(); for (int i = head[u]; ~i; i = edge[i].n) { if (edge[i].v == p) continue; dfs(edge[i].v, u); } for (const auto [l, r, i]: query[u]) if (_del(1, n, l, r, i)) ans[i] = false; }; dfs(1, 0); for (const auto& i: ans) cout << (i ? "YES" : "NO") << endl; cout << endl; } }

2024/1/7
articleCard.readMore

Codeforces Round 908 (Div. 2)

A. Secret Sport 大致题意 有 A 和 B 两个人,他们在比赛,每一局比赛中,率先赢得 $n$ 小场的人获胜,最终赢得 $m$ 局的人获胜,给出每一小场的获胜情况,问最终谁获胜了 思路 没有那么麻烦,说白了最后一个获胜的人,必定是最终获胜的人 AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; cout << str.back() << endl; } } B. Two Out of Three 大致题意 有一个数组 $a$,希望构建一个数组 $b$,满足下面三条中的任意两条,且仅满足两条 存在一个 $i,j \in [1, n]$,满足 $a_i = a_j, b_i = 1, b_j = 2$ 存在一个 $i,j \in [1, n]$,满足 $a_i = a_j, b_i = 1, b_j = 3$ 存在一个 $i,j \in [1, n]$,满足 $a_i = a_j, b_i = 2, b_j = 3$ 思路 注意是要仅满足两条,所以只需要满足任意两组相同的数值对即可。即存在两个数字,他们出现次数至少两次,即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; map<int, int> cnt; for (const auto& i: data) ++cnt[i]; vector<int> two; for (auto [fst, snd]: cnt) { if (snd >= 2) two.push_back(fst); } if (two.size() > 1) { int flag[2] = {0, 0}; for (int i = 0; i < n; ++i) { if (data[i] == two[0]) { cout << (flag[0] == 0 ? 1 : 2) << ' '; ++flag[0]; } else if (data[i] == two[1]) { cout << (flag[1] == 0 ? 1 : 3) << ' '; ++flag[1]; } else cout << 1 << ' '; } cout << endl; } else { cout << -1 << endl; } } } C. Anonymous Informant 大致题意 有一个初始的数组,未知长什么样子,但是经过 $n$ 次,操作后得到了当前数组,问是否存在原来的数组 操作的方式是,选择一个 $i$,满足 $a_i = i$,并将整个数组左移 $i$ 次 思路 每个值,当其恰好满足 $a_i = i$ 的时候,即可完成一次固定的移动,从最终结果我们来看,说白了就是可以从某个固定的旋转次数到另外某个固定的移动次数 那么说白了就是一个图,这样我们就可以根据旋转次数作为图的下标,建图 接下来需要找的就是拓扑后,最终旋转次数为 0 次的时候,拓扑长度最多为多少次,或者存在包含 0 节点的环即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (auto& i: data) cin >> i; struct node { int v, n; }; vector<node> edge(n); vector head(n, -1), deg(n, 0); for (int i = 0; i < n; ++i) { if (data[i] > n) continue; const int u = (i + 1 - data[i] + n) % n; const int v = (u + data[i]) % n; edge[i] = {v, head[u]}; head[u] = i; ++deg[v]; } vector<int> vis(n + 1, false); bool circle = false; int maxLen = -1; queue<pair<int, int>> q; for (int i = 0; i < n; ++i) if (!deg[i]) q.emplace(i, 0); while (!q.empty()) { auto [fst, snd] = q.front(); q.pop(); if (fst == 0) maxLen = max(maxLen, snd); vis[fst] = true; for (int i = head[fst]; ~i; i = edge[i].n) { --deg[edge[i].v]; if (!deg[edge[i].v]) q.emplace(edge[i].v, snd + 1); } } if (deg[0]) circle = true; cout << (circle || maxLen >= k ? "YES" : "NO") << endl; } } D. Neutral Tonality 大致题意 两个数组,数组 $a$ 是固定顺序,数组 $b$ 可以按照任意顺序插入到 $a$ 数组中,使得整个数组的 LIS 最短 思路 这题应该才是 C 题,很简单,插入的顺序按照从大到小插入,每次插入的时候,插入到右边没有比当前值小的值处即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<int> a(n), b(m), ma(n); for (auto& i: a) cin >> i; for (auto& i: b) cin >> i; sort(b.begin(), b.end(), greater<>()); ma[n - 1] = a[n - 1]; for (int i = n - 2; i >= 0; --i) ma[i] = max(ma[i + 1], a[i]); int j = 0; for (int i = 0; i < n; ++i) { while (j < m && b[j] >= ma[i]) cout << b[j++] << ' '; cout << a[i] << ' '; } while (j < m) cout << b[j++] << ' '; cout << endl; } }

2024/1/3
articleCard.readMore

Educational Codeforces Round 157 (Rated for Div. 2)

A. Treasure Chest 大致题意 有一条路上,有一个宝箱(x)和一个钥匙(y),开始人在 0 号位置,可以携带钥匙、宝箱进行移动,但是最多只能同时携带宝箱走 $k$ 步 问最多需要走几步才能同时拥有宝箱和钥匙 思路 说白了就是钥匙可以在 $[y, \inf]$ 范围内移动,而宝箱只能在 $[x-k, x+k]$ 范围内,所以需要至少走到 $max(x, y)$ 后 ,再回到 $[x-k, x+k]$ 区间内 AC code 1 2 3 4 5 6 7 8 9 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int x, y , k; cin >> x >> y >> k; cout << max(x, y) + max(0, y - x - k) << endl; } } B. Points and Minimum Distance 大致题意 有一堆的数字,需要两两组合,变成平面上的点,然后在再逐个通过棋盘距离来连接所有点,要尽可能让线短 思路 就是要避免反复走,即一会往左一会往右,另外要尽可能短。 我的构造方法是这样的,将数组按照大小拆成两半,小一点的一半和大一点的一半,然后 x 只取小一点的,然后从最小开始取,y 则从大一点的开始,然后从最小开始取即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n * 2); for (auto& i: data) cin >> i; sort(data.begin(), data.end()); vector<pair<int, int>> res(n); for (int i = 0; i < n; ++i) res[i] = {data[i], data[n + i]}; int ans = 0; for (int i = 1; i < n; ++i) ans += abs(res[i].first - res[i - 1].first) + abs(res[i].second - res[i - 1].second); cout << ans << endl; for (const auto& [fst, snd]: res) cout << fst << ' ' << snd << endl; } } C. Torn Lucky Ticket 大致题意 有一个字符串组,每个字母都是数字,每个字符串的最大长度不超过 $5$ 个。 任选两个字符串连在一块,使得整个字符串的长度恰好为偶数,且拆成两半之后,再把两边的每一个数字加起来,使得两边相等 问有多少种可能 思路 看数组量级,说明需要查找的方式来找匹配的值 两个字符串相同长度的时候,非常的简单统计一下即可 如果不等长,那么就有几种组合 $1+3,1+5,2+4,3+5$ 最简单的方式就是枚举所有的拆法,去枚举那些长度较长的部分,并将结果写入到统计表里,然后查找即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define int long long void solve() { int n; cin >> n; vector<string> data(n); for (auto& i: data) cin >> i; map<pair<int, int>, int> cnt; for (auto& i: data) { int sum = 0; for (const char l: i) sum += l - '0'; cnt[{sum, i.size()}]++; if (i.size() == 3 || i.size() == 5 || i.size() == 4) { cnt[{sum - (i.front() - '0') * 2, i.size() - 2}]++; cnt[{sum - (i.back() - '0') * 2, i.size() - 2}]++; } if (i.size() == 5) { cnt[{sum - (i[4] - '0' + i[3] - '0') * 2, 1}]++; cnt[{sum - (i[0] - '0' + i[1] - '0') * 2, 1}]++; } } int ans = 0; for (auto& i: data) { int sum = 0; for (const char l: i) sum += l - '0'; if (auto iter = cnt.find({sum, i.length()}); iter != cnt.end()) ans += iter->second; } cout << ans << endl; } D. XOR Construction 大致题意 有一个数组 $a$,长度 $n-1$,需要构造出一个数组长度为 $n$ 的数组,满足数组是 $[0,n-1]$ 的排列,且 $b_i \oplus b_{i+1} = a_i$ 思路 显然可以得到,$b_i = b_1 \oplus a_1 \oplus a_2 \oplus a_3 \dots \oplus a_{i-1}$ 那么只需要枚举 $b_1$ 的值,然后去判断是否能够保证所有的 $b_i \le n$ 即可 如果找到了,那么就是答案,并不需要判断是否满足是 $[0,n-1]$ 的排列。 原因也很简单 题目说明必定有答案,那么必然有一个解 假如当前值能够满足所有结果值都在 $[0, n-1]$ 后并不是正确的解(即存在重复的值了) 那么必然还存在另外一个解不存在重复的解,假定错误的解为 $b_1$,正确的为 $b_1’$ 那么必定存在一个 $i,j(i \le j)$,满足 $b_i = b_j$ 那么可以得到 $b_j = b_i \oplus a_{i} \oplus a_{i+1} \oplus a_{i+2} \dots \oplus a_{j-1} = b_i$ 即 $a_{i} \oplus a_{i+1} \oplus a_{i+2} \dots \oplus a_{j-1}$ 那么必然就可以得到 $b_i’ \oplus a_{i} \oplus a_{i+1} \oplus a_{i+2} \dots \oplus a_{j-1} = b_j’$ 即两个解都不是正确的解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void solve() { int n; cin >> n; vector<int> data(n - 1); for (auto& i: data) cin >> i; struct node { int cnt, n[2]; }; vector<node> tree(n * 32); int len = 0; auto build = [&] { tree[len] = {0, -1, -1}; return len++; }; const auto root = build(); auto add = [&](const int x) { int cur = root; for (int i = 30; i >= 0; --i) { const auto v = x & (1 << i) ? 1 : 0; if (tree[cur].n[v] == -1) tree[cur].n[v] = build(); cur = tree[cur].n[v]; } }; auto find = [&](const int x) { int cur = root, ans = 0; for (int i = 30; i >= 0; --i) { const auto v = x & 1 << i ? 1 : 0; if (const auto u = v ^ 1; tree[cur].n[u] != -1) { cur = tree[cur].n[u]; ans += 1 << i; } else cur = tree[cur].n[v]; } return ans; }; int tmp = 0; for (const auto& i: data) { tmp ^= i; add(tmp); } for (int i = 0; i < n; ++i) { tmp = find(i); if (tmp >= n) continue; tmp = i; cout << tmp; for (const auto& j: data) { tmp ^= j; cout << ' ' << tmp; } cout << endl; return; } }

2024/1/1
articleCard.readMore

C++自定义的字面量

最近在看 CPP 的一些东西的时候,发现了一个非常有意思的特性。不得不说,CPP 的特性总是朝着一些奇奇怪怪的方向发展,整出了一堆奇奇怪怪的语法糖 这个特性叫 User-defined literals 非常简单的一种特性,例如很多时候我们会用一些结构体来表示内存/磁盘等大小,比如这种 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static size_t B = 1; static size_t KB = 1000 * B; static size_t MB = 1000 * KB; static size_t GB = 1000 * MB; static size_t KiB = 1024 * B; static size_t MiB = 1024 * KiB; static size_t GiB = 1024 * MiB; struct Size { size_t value; }; inline std::ostream& operator<<(std::ostream& o, const Size& size) { if (size.value < KiB) o << size.value << 'B'; // NOLINTNEXTLINE(*-narrowing-conversions) else if (size.value < MiB)o << std::fixed << std::setprecision(2) << 1.0 * size.value / KiB << "KiB"; // NOLINTNEXTLINE(*-narrowing-conversions) else if (size.value < GiB)o << std::fixed << std::setprecision(2) << 1.0 * size.value / MiB << "MiB"; // NOLINTNEXTLINE(*-narrowing-conversions) else o << std::fixed << std::setprecision(2) << 1.0 * size.value / GiB << "GiB"; return o; } 这样就可以有一个表示占用大小的类型了,但是这样并不方便,因为初始化的时候还需要明确表示这个类型。但是用上这个特性之后就可以非常简单,只需要再定义几个方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 inline Size operator ""_B(unsigned long long v) { return {v * B}; } #pragma region 1000 inline Size operator ""_KB(const unsigned long long v) { return {v * KB}; } inline Size operator ""_MB(const unsigned long long v) { return {v * MB}; } inline Size operator ""_GB(const unsigned long long v) { return {v * GB}; } #pragma endregion #pragma region 1024 inline Size operator ""_KiB(const unsigned long long v) { return {v * KiB}; } inline Size operator ""_MiB(const unsigned long long v) { return {v * MiB}; } inline Size operator ""_GiB(const unsigned long long v) { return {v * GiB}; } #pragma endregion 这样之后就可以直接用字面量初始化 1 2 const auto v = 123435245_KiB; std::cout << v << std::endl;

2024/1/1
articleCard.readMore

Codeforces Round 907 (Div. 2)

A. Sorting with Twos 大致题意 每次可以从前往后选择 $2^x$ 个值,每个值减少一,问进行无数次操作后,是否可能让整个数组变成无递减 思路 简单题,只要原数组中,那些盲区仍然保持非递减即可。例如 $[3, 4]$ 这种区间,要么一起减少要么一起不减少 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; bool flag = true; pair<int, int> arr[4] = {{2, 3}, {4, 7}, {8, 15}, {16, 31}}; for (auto [l, r]: arr) { for (; l < min(r, n - 1); ++l) { if (data[l] > data[l + 1]) { flag = false; break; } } } cout << (flag ? "YES" : "NO") << endl; } } B. Deja Vu 大致题意 给出两个数组,对于第一个数组 $a$ 的每一个值,进行如下操作: 从前往后遍历数组 $b$ 若 $a_i \space mod \space 2^{b_i} = 0$ 则 $a_i \leftarrow a_i + 2^{b_i - 1}$ 求最终数组 思路 数据量很大,但是有技巧 因为一旦满足 $a_i \space mod \space 2^{b_i} = 0$ 之后,会加上的是 $2^{b_i - 1}$。 这也就意味着,如此操作之后,其必然可以被 $2^{b_i - 1}$ 整除,且最大只能被它整除了。 也就是说,每次能够加上的值一定是不断变小的 题目中给出的 $b_i \in [1, 30]$ 所以其实第二个数组最多只能有 30 个有效值。处理之后暴力即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; vector<int> data(n), query; query.reserve(30); for (auto& i: data) cin >> i; for (int i = 0; i < q; ++i) { int tmp; cin >> tmp; if (i == 0 || tmp < query.back()) query.push_back(tmp); } for (auto& i: data) for (long long j: query) if (i % (1 << j) == 0) i += 1 << j - 1; for (int i = 0; i < n; ++i) cout << data[i] << " \n"[i == n - 1]; } } C. Smilo and Monsters 大致题意 有一堆怪物窝,每个怪物窝里有一定数量的怪物。你有一个累计的技能点数,初始值为 0,每次可以选择不同的技能 找一个怪物窝,打死里面的一只怪物,积累一点技能点数 找一个怪物窝,里面的怪物数量不大于你的技能点数,消耗全部的技能点数释放大招,消灭这个窝里的全部怪物 问最少需要几次操作 思路 对于一个窝而言,只需要打死里面的一半的怪物,再加上一次使用技能,就可以实现打败这个窝了,此时成本为 $\left \lceil \frac{x}{2} \right \rceil + 1$ 如果有两个窝,假设都这样操作,那么代价就是 $\left \lceil \frac{x}{2} \right \rceil + \left \lceil \frac{y}{2} \right \rceil + 2$ 假如我将小一点的那个窝全部一只只打死,然后打几只大窝里的怪,再对大一点对窝释放大招,也就是只使用一次技能,代价就是 $x + (\left \lceil \frac{y+x}{2} \right \rceil - x) + 1 = \left \lceil \frac{y+x}{2} \right \rceil + 1$ 显然,后者价值更高,所以要考虑按照后者的操作进行,即多用小窝的怪刷技能点,然后对大窝放技能。用双指针做就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto& i: data) cin >> i; sort(data.begin(), data.end()); int l = 0, r = n - 1, x = 0, ans = 0; while (l <= r) { if (x >= data[r]) { data[r] = 0; x = 0; --r; ++ans; } else { const int tmp = l == r ? (x + data[l] + 1) / 2 - x : min(data[l], data[r] - x); x += tmp; ans += tmp; data[l] -= tmp; if (!data[l]) ++l; } } cout << ans << endl; } } D. Suspicious logarithms 大致题意 定义两个函数,$f(x) = y, g(x) = z$,满足 $2^y \leq x, y^z \leq z$,且 $y, z$ 都尽可能大 给出一个区间,求 $\sum_{i=l}^{r} g(i)$ 思路 虽然看起来很难,但是观察可以发现,$y \in [1, 64]$,而 $z \in [0, 10]$,所以只需要枚举所有的 $y, z$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #define int long long void solve() { map<pair<int, int>, int> mp; for (int i = 2; i < 60; ++i) { const __int128 ml = 1ll << i, mr = 1ll << i + 1; __int128 base = 1; for (int j = 0; j <= 10; ++j) { if (base >= mr) break; if (base * i <= ml) { base *= i; continue; } mp.insert({{max(ml, base), min(mr, base * i) - 1}, j}); base *= i; } } constexpr int mod = 1e9 + 7; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int l, r; cin >> l >> r; int ans = 0; for (auto& [fst, snd]: mp) { const int len = min(fst.second, r) - max(fst.first, l) + 1; if (len <= 0) continue; ans = (ans + len * snd % mod) % mod; } cout << ans << endl; } }

2023/12/24
articleCard.readMore

Codeforces Round 916 (Div. 3)

这场本来是想在比赛的时候写的,可惜写到一半突然有公司的电话进来了,就只好先去处理点事情了 A. Problemsolving Log 大致题意 写出 A 题需要思考 1min,写出 B 题需要思考 2min,以此类推,给出一个人每分钟在思考的题,问最终有几道题能写出来 思路 简单题,统计一下每个字母出现次数就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; map<char, int> mp; for (auto&c: str) mp[c]++; int ans = 0; for (char i = 'A'; i <= 'Z'; ++i) if (mp[i] >= i - 'A' + 1) ans++; cout << ans << endl; } } B. Preparing for the Contest 大致题意 需要构造一个长度为 n 的序列,使得其满足所有相邻两个值中,存在恰好 k 个前者大于后者的对数 思路 也是简单题,想清楚怎么构造就行。比如我的思路是把最大的 k 个值按照递增序放在后面 AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; for (int i = n - k; i > 0; --i) cout << i << " "; for (int i = n - k + 1; i <= n; ++i) cout << i << " "; cout << endl; } } C. Quests 大致题意 有一堆问题,第一次写出来可以得到 $a_i$ 的分数,第二次及之后写出来可以得到 $b_i$ 的分数 每道题写出来的前提是前面的所有题至少都写出来一次 问最多写 $x$ 道题,最多可以得到多少分 思路 由于每道题必须要前面的所有题都写出来才能写,所以可以考虑枚举最终写到了哪道题,然后把剩下的所有写题的机会给那个 $b_i$ 最大的题即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> a(n), b(n); for (auto&i: a) cin >> i; for (auto&i: b) cin >> i; int ma = 0, ans = 0, suf = 0; for (int i = 0; i < min(n, k); ++i) { suf += a[i]; ma = max(ma, b[i]); ans = max(ans, suf + (k - i - 1) * ma); } cout << ans << endl; } } D. Three Activities 大致题意 有三种活动,且有 $n$ 天,每一天最多参加一个活动,每一个活动最多只能有一天参加。 每个活动每天都有其他伙伴一起来参加,数量都不相同。问应该挑哪三天去参加活动,能够使得一起参加的伙伴数量最多 思路 由于总共就只有三种活动,所以只需要挑选三天。对于每一个活动而言,他们只需要考虑参加伙伴最多的那三天即可 所以在每一个活动下单独排序参加的伙伴数量,选择每个活动下伙伴最多的三天,暴力找下答案即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<pair<int, int>> a(n), b(n), c(n); for (auto&i: a) cin >> i.first; for (auto&i: b) cin >> i.first; for (auto&i: c) cin >> i.first; for (int i = 0; i < n; ++i) a[i].second = i; for (int i = 0; i < n; ++i) b[i].second = i; for (int i = 0; i < n; ++i) c[i].second = i; sort(a.begin(), a.end(), greater<>()); sort(b.begin(), b.end(), greater<>()); sort(c.begin(), c.end(), greater<>()); int ans = 0; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { if (b[j].second == a[i].second) continue; for (int k = 0; k < 3; ++k) { if (c[k].second == a[i].second || c[k].second == b[j].second) continue; ans = max(ans, a[i].first + b[j].first + c[k].first); } } } cout << ans << endl; } } E2. Game with Marbles (Hard Version) 大致题意 直接看 Hard Version Alice 和 Bob 做游戏,有 $n$ 种颜色的石头,每个人都有所有颜色的石头若干个 每轮,当前的选手需要选择一种颜色,然后当前选手丢掉一个这种颜色的石头,对方丢掉全部的 目标是让自己的石头数量之和尽可能多,问最后剩下多少个 思路 假如一种颜色的石头,Alice 有 $a$ 个,Bob 有 $b$ 个。如果是 Alice 选择,那么 Alice 会剩下 $a-1$ 个,差值是 $a-1$。如果是 Bob 选择,则是 $-(b-1)$ 这样可以得到,不选择这个颜色的代价就是 $(a-1)-(-(b-1)) = (a+b-2)$,同时选择它的收益也是这个 按照代价排序,从最大的代价依次开始操作即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> a(n), b(n); for (auto&i: a) cin >> i; for (auto&i: b) cin >> i; vector<pair<int, int>> d(n); for (int i = 0; i < n; ++i) { d[i].first = a[i] + b[i] - 2; d[i].second = i; } sort(d.begin(), d.end(), greater<>()); for (int i = 0; i < n; ++i) { if (i % 2) { b[d[i].second]--; a[d[i].second] = 0; } else { a[d[i].second]--; b[d[i].second] = 0; } } int ans = 0; for (int i = 0; i < n; ++i) ans += a[i] - b[i]; cout << ans << endl; } } F. Programming Competition 大致题意 一个公司,其上级或者间接的上级都是他老板,现在需要两两组队,但是有老板关系的不能组,问最多可以组几个队伍 思路 树上 dfs 搜索即可,每次传给当前节点,外面不属于他的员工的节点数量还有多少,有就用掉即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> p(n), d(n); vector<vector<int>> tree(n); for (int i = 1; i < n; ++i) { int tmp; cin >> tmp; tree[tmp - 1].push_back(i); } function<int(int)> deep = [&](const int v) { d[v] = 1; for (auto& i: tree[v]) d[v] += deep(i); return d[v]; }; int ans = 0; function<void(int, int)> dfs = [&](const int v, int cnt) { ans += cnt > 0; if (cnt > 0) --cnt; int sum = 0; for (auto &i: tree[v]) sum += d[i]; for (auto &i: tree[v]) dfs(i, sum - d[i] + cnt); }; // ReSharper disable once CppExpressionWithoutSideEffects deep(0); dfs(0, 0); cout << ans / 2 << endl; } } G2. Light Bulbs (Hard Version) 大致题意 这题还是挺有意思的 有一排灯,其中每种颜色的灯恰好有两个,可以选择开始的时候,点亮一部分灯,之后可以无限次进行如下操作 点亮一盏灯,前提是和他相同颜色的另一盏灯已经点亮了 选择一种颜色,其两盏灯都已经点亮了,将这两盏灯中间的所有灯点亮 问开始的时候最少选择几盏灯,就可以把所有灯都点亮。同时给出可以选择的数量 思路 首先,如果存在一个 $a$ 夹在两个 $b$ 之间,那么只需要点亮 $b$ 就可以点亮 $a$,我们可以进行建图来做 但是如果这样完整建图的成本会很高,所以得考虑优化一下 例如 $1,2,3,4,1,2,3,4$ 这种,就会变成一个全连接的图,而点数量有 $2 \times 10^5$,故不太行 由于这个图并不是传统的拓扑排序,而是只要上游任意一个点满足则满足(传统拓扑是上游均满足才满足)所以可以做反向路径压缩,即“别压缩” 例如上面的例子,仅实现与最后出现的那个区间进行连接,例如上图,则仅有 $1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 1$, 容易的得到这样建图也是可以满足要求的,因为不是传统拓扑 那么只需要在创建完成图之后,将拓扑的首个节点序列拿出来即可,这些节点是必选。因为有两个相同颜色的灯,所以是 $2^x$ 种选择 接下来考虑成环的情况,我们只需要找出最初试图进入环的那个节点(最左边的节点,因为它必然可以连接到剩下所有的节点) 看剩下环内的节点是否能够反向到达它即可。可以反向建图来做。至于最初进入环的节点,可以用并查集 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; struct node { int v, n; }; stack<int> st; vector<node> edge, redge; vector head(n + 1, -1), rhead(n + 1, -1), deg(n + 1, 0), fa(n + 1, 0), vis(n + 1, 0); edge.reserve(n); redge.reserve(n); for (int i = 0; i < fa.size(); ++i) fa[i] = i; function<int(int)> finds = [&](int x) { return fa[x] == x ? x : fa[x] = finds(fa[x]); }; function join = [&](const int x, const int y) { const int rx = finds(x), ry = finds(y); if (rx == ry) return; fa[rx] = ry; }; for (int i = 0; i < n * 2; ++i) { int tmp; cin >> tmp; while (!st.empty() && vis[st.top()] == 2) st.pop(); if (!st.empty() && st.top() != tmp) { join(tmp, st.top()); edge.push_back({tmp, head[st.top()]}); head[st.top()] = static_cast<int>(edge.size()) - 1; deg[tmp]++; redge.push_back({st.top(), rhead[tmp]}); rhead[tmp] = static_cast<int>(redge.size()) - 1; } st.push(tmp); ++vis[tmp]; } queue<int> q; for (int i = 1; i <= n; ++i) if (deg[i] == 0) q.push(i); constexpr long long mod = 998244353; const int res = static_cast<int>(q.size()); long long ans = 1; for (int i = 0; i < q.size(); ++i) { ans <<= 1; ans %= mod; } while (!q.empty()) { const int cur = q.front(); q.pop(); if (vis[cur] == 3) continue; ++vis[cur]; for (int i = head[cur]; ~i; i = edge[i].n) q.push(edge[i].v); } map<int, int> superCnt; for (int i = 1; i <= n; ++i) if (vis[i] != 3 && finds(i) == i) q.push(i); while (!q.empty()) { const int cur = q.front(); q.pop(); if (vis[cur] == 3) continue; ++vis[cur]; superCnt[finds(cur)]++; for (int i = rhead[cur]; ~i; i = redge[i].n) q.push(redge[i].v); } for (auto [k, v]: superCnt) ans = ans * 2 * v % mod; cout << (res + superCnt.size()) << ' ' << ans << endl; } }

2023/12/21
articleCard.readMore

关于 LRU map 的一些灵感

最近在折腾一些小项目,让我突然有了一些对 LRU map 的想法 传统的 LRU map 的实现 一般常见的 LRU map 的实现大概是长这样 通过一个 HashMap 来实现对值的快速访问,但是 Map 中记录的值并不是原始的值, 当然也有可能包含原始的值,但是至少会记录一个链表的节点地址 每次进行读取/写入操作的时候, 需要将对应链表的那个节点,移动到链表的尾部 当需要进行逐出值的时候,就从链表的头部取出值进行逐出,因为链表的头处的值必然是最久没有访问过的值了 一些新的想法 最近想到了一种新的解决 LRU map 的方法,即不再需要链表来做关联映射。而是准备一个队列。HashMap 中的值不再携带链表的地址,而是记录实际的最后访问时间 每次进行读取/写入操作的时候,都修改 HashMap 中的此节点的最后操作时间,然后同时将这次读取/写入操作的 key 和时间写入队列 每次需要逐出值的时候,就重复从队列里取值,然后判断一下队列中节点记录的操作时间和实际在 HashMap 中的时间是否一致,如果一致的话那就可以逐出,否则就不逐出,继续从队列中取值

2023/12/18
articleCard.readMore

2023 杭州站 ICPC 现场赛

背景故事 故事的开始 大概在 7-8 月份的时候,得知了杭州站要开的消息,开玩笑的问我的一个退役了好多好多年的同事说,要不要去申请一下当志愿者去玩一下。结果同事说:要不我们申请个外卡名额去玩一场吧 而后,就组成了三个已经退役多年的队伍,甚至已经有人退役了 6 年之久 嗯,就是这样一个没有板子没有脑子没有训练的队伍,甚至没有人会数论,准备参加 2023 的杭州站 开赛前的准备 虽然说都是退役多年,但我最近也一直在刷 codeforces,虽然也就周末写两场 codeforces,而且也是点到为止,不过多深入写题,难题就直接摆烂的那种,毕竟我写题的目的也只是为了消遣 但是我们没有板子,我倒是可以折腾一些板子出来,但是毕竟是一个数论基础为 0 的队伍,得整点数论的板子吧,于是开始找学弟找找我当年的板子去哪了,然后就领到了一份他们多余的纯英文的板子……可惜我英语也不好啊,本来还想让他们帮忙带一份字典之类的 有一个比较重要的插曲就是,这两天恰好是双十二,于是热身赛打到一半,就又回公司去值班双十二了,结果就导致杭师大给我打的 84 块钱一分钱没花 比赛 比赛开始之后就是找签到题,我从 A 题开始看起来的,看了一眼 A 题感觉就是个简单的模拟题,想法是把每一个队伍改到最好或者改到最差两种情况下,金牌队伍有哪些,然后统计就行 直到我看到了样例:TS1 只能说不愧是出题人,还很好的暗示了一下宁理 M 题面 然后就看到开始有人过了 M 题,果断刷了一眼 M 题的题意,发现其实很简单,只需要考虑三种情况就行了 整个数组都要 V 图左边只取最小的,右边取全部 V 图右边只取最小的,左边取全部 然后对这三种情况找最优解就行了 D 题面 很快就发现了 D 题也有很多人过了,于是决定投入一个人去解决,剩下的继续看看别的题 J 题面 然后就两个人去跟榜了 J 题,一开始确实很难有头绪,不过自从有了热身赛的 C 题的经验之后,很快就有一个解决想法: 每两个相邻节点做一次询问,比如 1, 2 询问一下,然后再 3, 4 如此询问 如果都没有边,说明了绝对不可能是菊花 毕竟菊花一定有一个点与所有点相连,那么必然在这次遍历中一定会出现菊花中心点和其他点的连接情况询问 如果有任何一条边的情况下,那么就可以考虑一下是否是菊花了(因为我们找到一条边了) 随便再找两个点,看看是否和得到的这条边的某个点是否都连接,如果是的话,那么就是菊花 如果有任何一个点不是的话,那么就看看另外一个点是否有这个特性 如果都没有这个特性,那么就是链 H 题面 H 题队友看了之后表示是一道概率题,大概率是写不出来的,毕竟三个完全不会数论了,加之过的人也不多。所以决定都去刷刷 D 题,看看能不能写出来 研究了一个多小时之后,我放弃了,决定去瞄一眼 H 题看看怎么样。稍微看了一会,感觉好像不是很难,是一道图论题 建图 若 A 依赖 B,且当前 A 的糖果比 B 多,但是在 B 获得糖果之后,A 就比 B 少的情况下,建立 B 到 A 的边 若 A 依赖 B,且无论 B 是否获得糖果,当前 A 的糖果比 B 多的情况下,将 A 的概率(p)标记为 0 若 A 依赖 B,且无论 B 是否获得糖果,当前 A 的糖果比 B 少的情况下,将 A 的概率(p)标记为 1 拓扑遍历所有的点 若这个点已经被标记为 0,则不再继续拓扑接下来的节点 若这个节点已经被标记为 x,则为其下面的节点的概率(p)标记为 (x+1) 最后,再求解每一个值,为 $(a_i + w_i * \frac{1}{A^p_p}) mod M$ 为什么是这样算的: 假定 A 通过和 B 进行比较糖果,才能确定其是否能拿到糖果的场景下 无论 B 是否获得糖果,当前 A 的糖果比 B 多的情况下,那么必然一定拿不到糖果,那么 A 的拿到概率就是 0,那么答案就是 $a_i$ 无论 B 是否获得糖果,当前 A 的糖果比 B 少的情况下,那么必然一定拿得到糖果,那么 A 的拿到概率就是 1,那么答案就是 $a_i + w_i$ 上面两种情况都挺容易解决的,接下来是剩下的那种情况:B 要是拿到了糖果,那么 A 也可以拿到糖果,否则 A 也拿不到糖果 在这种情况下,我们希望得到的是,让整个随机排列顺序的时候,尽可能让 B 在 A 之前被处理,这样的话,我们就可以得到 A 的概率就是 $\frac{1}{A^2_2}$(这非常容易证明,因为这需要最终排序的结果中,仅出现 B 在 A 前面的排列) 当然,一般依赖也不可能只有那么简单,B 可能也会依赖 C,这个时候 A 的结果就变成了必须要出现 C、B、A 这样的顺序,也就是 $\frac{1}{A^3_3}$ 依次类推,可以得到距离最终的根节点有 $x$ 步,则就是 $\frac{1}{A^x_x}$ 的概率 如果成环了怎么办?也非常简单,当真的成环了之后,那么必然环内有一个节点是最终排序后,最先被处理的,那么必然不能拿到糖果,然后其依赖的节点也就拿不到糖果了,也就等于所有人都拿不到糖果了 G 题面 写出 H 后感觉状态大好,看到 G 题也有不少人过了,决定也热热手,稍后再去看 D 题 去了个厕所回来看了一会 G 题,感觉就非常的简单,出题人很巧的把题意隐藏了,实际上是非常简单的题目,可以说是最简单的 BFS 练习题了 这道题最重要的是理解蛇缩短的能力,因为蛇本体会阻碍移动,所以可以通过缩短的方式去“等待”蛇身体离开想要到达的点,然后再走过去 接下来就是要考虑等多久的问题,同时需要注意的是,蛇的每一步移动都是会让蛇往前走一步。即每经过一个时间单位,无论做什么,蛇身就会缩短一节,使得释放出一个格子被允许走到,释放的顺序就是蛇的反向顺序 为什么不用考虑因为额外移动带来新的节点被阻塞的问题:因为新的节点如果被阻塞了,那么必然是已经到达过的节点了,无需重复到达 故可以得到如下结论:蛇身初始所在的每一个格子都有一个基本的费用,其费用等于这个点距离蛇尾巴的蛇上距离,即需要到达这些个节点就必须要满足实际时间大于等于这个基本费用才能到达,其他格子毫无限制,可以直接 bfs 到达,搞个优先队列即可 D 最后还是回到 D 题 最终想到了一个问题,对于 $a_1 \times (a_2 + a_3) \times (a_4 + a_5) \dots$ 这个式子,如果这其中存在两对相加起来的值不为 $1, -1$ 的值的情况下,那么必然就会越乘越大,无法救回来,也难以构造 那么必然,$(a_{x} + a_{x+1})$ 的结果一定是 $1, -1$ 的,或者至少最多只有一个不是! 那么继续构造,如果要满足这种情况下,最简单的就是 $2, -1$ 和 $1, -2$ 这两组了,这个时候,可以得到乘积结果都是在 $2, -2$ 之间了,那么不妨就尽可能相加的结果接近 $0$,这样的情况下,$0 + x = 1 \times x$,这是最优雅的解决方案 所以给出下面的解决方案: 若给出的 $n$ 是奇数,则最开头的两个值是 $1, 1$ 若给出的 $n$ 是偶数,则最开头的四个值是 $1, 1, 1, 1$ 接下来根据情况进行补充 如果前一个值是 $-2$,那么为了保证下面的和是 $1, -1$ 的话,就必须补充 $1$ 如果前一个值是 $2$,那么为了保证下面的和是 $1, -1$ 的话,就必须补充 $-1$ 如果前一个值是 $-1$,那么为了保证上面的乘积是 $2/-2$ 的话,就应当看情况补充 $-1,1$,比如之前构造了一个 $2$ 出来,那么就要补充一个 $2$ 来生成一个乘积为 $-2$ 的结果去抵消前一个构造得到的 $2$ 如果前一个值是 $1$,那么为了保证上面的乘积是 $2/-2$ 的话,就应当看情况补充 $-1,1$,比如之前构造了一个 $2$ 出来,那么就要补充一个 $-2$ 来生成一个乘积为 $-2$ 的结果去抵消前一个构造得到的 $2$ A 题面 最后还是非常可惜没有能在时间内写出 A 题,本来还是有机会金的,但是毕竟大家都是很久没有写过 C 的人了,最终的结果还是让人非常满意的

2023/12/11
articleCard.readMore

反复横跳的 Clang-Tidy(cert-dcl21-cpp)

今天早上有些发烧,就没去上班,下午稍微好点了之后就爬起来折腾会代码,写着写着就发现了一个奇怪的东西。把代码抽出核心部分类似如下的代码 1 2 3 4 5 struct A { int v; A operator++(int) noexcept { return A(v++); } } 这里返回的值是一个自身为 A 的右值,理论上并不需要明确指明右值(即返回值写 A&&),因为返回值本身肯定是右值,在原来的栈中如果进行赋值操作的时候,会先尝试进行移动构造,如果没有提供移动构造的情况下才会尝试进行引用构造甚至是复制构造 但是这段代码,Clang-Tidy 却给我报了 Warning: Clang-Tidy: Overloaded 'operator++' returns a non-constant object instead of a constant object type 大致意思是说,operator++ 方法返回了一个非 const 的变量。看了下样例,大概意思是说,因为返回值是一个”临时”的类型(毕竟是一个右值,用完应该就要丢掉的)故需要 const 一下,避免会出现 (i++)++; 这种离谱的代码(后者的 ++ 是作用在返回的右值上的,实际上不会对原来的值生效) 一听,好像有点道理,自动修复一下 1 2 3 4 5 struct A { int v; const A operator++(int) noexcept { return A(v++); } } 嗯,这样应该就行了吧。然后 Clang-Tidy 又给我报了个 Warning:Clang-Tidy: Return type 'const A' is 'const'-qualified at the top level, which may reduce code readability without improving const correctness 蛤?又告诉我不能带 const,因为这可能会导致不必要的代码理解?看了下例子,也非常好理解,因为返回值是一个右值,最终(指代通常情况下)都应该被移动构造,这样的话,实际上 const 仅仅是对右值进行了 const 标识,并没有什么用处 也就是说,加也不对,不加也不对…… 在翻找了一堆资料后,发现了这两个 checker 的文档: cert-dcl21-cpp readability-const-return-type 其中,在前者有这样一句: It will be removed in clang-tidy version 19. 嗯,再一看自己的 clang-tidy,恰好是 18.0.0

2023/12/4
articleCard.readMore

Codeforces Round 906 (Div. 2)

A. Doremy’s Paint 3 大致题意 有一个数组,重排之后,是否能够满足任意两个相邻值的只和都相同 思路 只有两种可能,只有两个值,且数量相同或者恰好差一个,或者只有一个值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; map<int, int> mp; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mp[tmp]++; } if (mp.size() == 1) cout << "YES" << endl; else if (mp.size() > 2) cout << "NO" << endl; else { cout << (abs(mp.begin()->second - mp.rbegin()->second) <= 1 ? "YES" : "NO")<< endl; } } } B. Qingshan Loves Strings 大致题意 有两个 $01$ 字符串,希望把 $A$ 字符串变成任意相邻两个字母不同的,每次操作允许将 $B$ 字符串插入到 $A$ 字符串的任意位置,问是否有可能 思路 首先,如果 $A$ 本来就是,那么就不用插入了 其次,若 $B$ 本身不是,那肯定不行,毕竟最后插入的字符串一定是完整的,那么最终必然会不是 然后,如果要插入,那必然是插入到两个相邻的字符内,那么必然 $B$ 的前后必须相同,且与要插入的部分不同 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; string str1, str2; str1.reserve(n); str2.reserve(m); cin >> str1 >> str2; // check str 1 bool f[2] = {false, false}; for (int i = 1; i < n; ++i) if (str1[i] == str1[i - 1]) f[str1[i] - '0'] = true; if (f[0] && f[1]) { cout << "NO" << endl; continue; } if (!f[0] && !f[1]) { cout << "YES" << endl; continue; } // check str 2 bool flag = true; for (int i = 1; i < m; ++i) if (str2[i] == str2[i - 1]) flag = false; if (!flag) { cout << "NO" << endl; continue; } if (f[str2[0] - '0']) { cout << "NO" << endl; continue; } if (f[str2[m - 1] - '0']) { cout << "NO" << endl; continue; } cout << "YES" << endl; } } C. Qingshan Loves Strings 2 大致题意 有一个 $01$ 字符串,希望将这个字符串的中间对称位置字符不同,每次操作允许往任何位置插入一个 $01$ 字符串 思路 用 list 模拟一下就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.reserve(n); cin >> str; if (n % 2) { cout << -1 << endl; continue; } list<char> l; for (auto &item : str) l.push_back(item); int lp = 0, rp = n; auto li = l.begin(), ri = l.end(); --ri; bool flag = true; vector<int> ans; while (li != ri && lp < rp) { if (*li != *ri) { ++li; --ri; ++lp; --rp; continue; } if (*li == '0') { ans.push_back(rp); l.insert(++ri, '0'); l.insert(ri, '1'); --ri; rp += 2; } else if (*ri == '1') { ans.push_back(lp); l.insert(li, '1'); --li; l.insert(li, '0'); --li; rp += 2; } if (ans.size() > 300) { flag = false; break; } } if (!flag) { cout << -1 << endl; } else { cout << ans.size() << endl; for (int i = 0; i < ans.size(); ++i) cout << ans[i] << " \n"[i == ans.size() - 1]; } } } D. Doremy’s Connecting Plan 大致题意 有一个城市,每个城市都有一定的人数,现在希望在城市之间建立连接,如果满足 $\sum_{k \in S} a_k \geq i \times j \times c$,则可以建立链接,其中 $S$ 为节点 $i$ 和 $j$ 已经连通部分的所有节点集合 思路 考虑所有的节点按照一定顺序和 $1$ 城市建连 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, c; cin >> n >> c; vector<pair<int, int>> data(n); for (auto&item: data) cin >> item.first; for (int i = 0; i < n; ++i) data[i].second = i + 1; int tot = data[0].first; sort(data.begin(), data.end(), [&](const pair<int, int>&lhs, const pair<int, int>&rhs) { return lhs.first - lhs.second * c > rhs.first - rhs.second * c; }); bool flag = true; for (auto&item: data) { if (item.second == 1) continue; if (tot + item.first >= item.second * c) tot += item.first; else flag = false; } cout << (flag ? "YES" : "NO") << endl; } } E1. Doremy’s Drying Plan (Easy Version) 大致题意 有一个城市,天气预报会预报未来 $m$ 天的下雨情况,允许选择其中两天不下雨,问最多可以有多少个城市这 $m$ 天不下雨 思路 根据起始和结束位置的下雨,标记数组,然后统计即可。因为只能选择两天,所以基本上是半暴力即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; vector<vector<int>> start(n + 1), end(n + 1); for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; start[u].push_back(i); end[v].push_back(i); } set<int> s; map<pair<int, int>, int> ans1; map<int, int> ans2; int ans3 = 0; for (int i = 1; i <= n; ++i) { for (auto&item: start[i]) s.insert(item); if (s.empty()) ans3++; else if (s.size() == 1) ans2[*s.begin()]++; else if (s.size() == 2) ans1[{*s.begin(), *s.rbegin()}]++; for (auto&item: end[i]) s.erase(item); } int res = ans3; for (auto&item: ans1) { int tmp = item.second + ans3; auto l = ans2.find(item.first.first), r = ans2.find(item.first.second); if (l != ans2.end()) tmp += l->second; if (r != ans2.end()) tmp += r->second; res = max(res, tmp); } vector<int> ans2v; ans2v.reserve(ans2.size()); for (auto&item: ans2) ans2v.push_back(item.second); sort(ans2v.begin(), ans2v.end(), greater<>()); if (ans2v.size() == 1) res = max(res, ans2v[0] + ans3); else if (ans2v.size() > 1) res = max(res, ans2v[0] + ans2v[1] + ans3); cout << res << endl; } }

2023/12/3
articleCard.readMore

一段奇怪的 CPP 代码

最近发现了一个奇怪的代码,在 C++17 下。使用的 cmake 命令是 1 "~/Applications/CLion Nova.app/Contents/bin/cmake/mac/aarch64/bin/cmake" -DCMAKE_BUILD_TYPE=Debug "-DCMAKE_MAKE_PROGRAM=~/Applications/CLion Nova.app/Contents/bin/ninja/mac/aarch64/ninja" -G Ninja -S ~/Code/ClionProject -B ~/Code/ClionProject/cmake-build-debug 而这段代码则是 1 2 3 4 5 list<int> l; for (int i = 0; i < 10; ++i) l.push_back(i); auto iter = l.begin(); for (int i = -1; i >= -10; --i) l.insert(iter--, i); for (int&v: l) cout << v << ' '; 这段代码的结果却是 1 -1 0 1 -10 2 -9 3 -8 4 -7 5 -6 6 -5 7 -4 8 -3 9 -2 如果稍微调整一下,比如这样的代码 1 2 3 4 5 6 7 8 list<int> l; for (int i = 0; i < 10; ++i) l.push_back(i); auto iter = l.begin(); for (int i = -1; i >= -10; --i) { l.insert(iter, i); --iter; } for (int&v: l) cout << v << ' '; 得到的结果却是 1 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 如果调整成这样 1 2 3 4 5 6 7 8 list<int> l; for (int i = 0; i < 10; ++i) l.push_back(i); auto iter = l.begin(); ++iter; for (int i = -1; i >= -10; --i) { l.insert(--iter, i); } for (int&v: l) cout << v << ' '; 得到的结果也是 1 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 这似乎有点不太符合预期。至少后两个是符合预期的,而第一个就有点奇怪了。第一反应是不是踩到 UB 了,但是很快在文档里找到了不符合预期的描述 既然如此,那么就写一段测试代码看看 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 list<int> l; void f(list<int>::iterator iter, int v) { cout << *iter << ' '; l.insert(iter, v); cout << *iter << endl; } void solve() { for (int i = 0; i < 10; ++i) l.push_back(i); auto iter = l.begin(); for (int i = -1; i >= -10; --i) { f(iter--, i); cout << (iter == l.begin()) << endl; for (int&v: l) cout << v << ' '; cout << endl; } } 此处进行了一下代理,将每次试图写入钱,通过 f 函数进行代理后,再执行插入操作。结果发现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 0 0 0-1 0 1 2 3 4 5 6 7 8 9 11 12 0-1 0 1 2 3 4 5 6 7 8 9 -2 9 9 0-1 0 1 2 3 4 5 6 7 8 -3 9 -2 8 8 0-1 0 1 2 3 4 5 6 7 -4 8 -3 9 -2 7 7 0-1 0 1 2 3 4 5 6 -5 7 -4 8 -3 9 -2 6 6 0-1 0 1 2 3 4 5 -6 6 -5 7 -4 8 -3 9 -2 5 5 0-1 0 1 2 3 4 -7 5 -6 6 -5 7 -4 8 -3 9 -2 4 4 0-1 0 1 2 3 -8 4 -7 5 -6 6 -5 7 -4 8 -3 9 -2 3 3 0-1 0 1 2 -9 3 -8 4 -7 5 -6 6 -5 7 -4 8 -3 9 -2 2 2 0-1 0 1 -10 2 -9 3 -8 4 -7 5 -6 6 -5 7 -4 8 -3 9 -2 仍然有这个奇奇怪怪的问题。 正当我想要搜索一些文档来看看是不是什么奇奇怪怪的 bug 的时候,突然意识到一个问题:i++; 等价于下面这三行代码 1 2 3 auto tmp = i; ++i; return tmp; 这似乎就能解释为什么了!因为在试图进行 iter-- 操作的时候,又进行了插入操作,实际上导致了 iter 本身先移动到了前一个指针的位置,而在 STL 标准库实现的 list 中,这个链表是一个双向带头循环链表,故实际上此时 iter 是先被移动到了 end() 的位置,然后再返回了 begin() 的位置,并在 begin() 前插入了一个值,使得实际上的 begin() 发生了更新。而实际上我们的 iter 早就被移动到 end() 的位置。

2023/12/3
articleCard.readMore

Codeforces Round 905 (Div. 3)

A. Morning 大致题意 需要依次打出四个数字,键盘上有十个按钮,每个按钮对应一个数字,每次允许按下当前按钮,或者移动到相邻的按钮上,问至少需要多少次才能打出来 思路 暴力扫都行,模拟顺序然后找路径即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int x; cin >> x; int d[5], ans = 0; for (int &i: d) { i = x % 10; if (i == 0) i = 10; x /= 10; } d[4] = 1; for (int i = 3; i >= 0; --i) ans += abs(d[i] - d[i + 1]) + 1; cout << ans << endl; } } B. Chemistry 大致题意 给你一个字符串,在恰好删除掉 $k$ 个字母之后,再重新排列,能否得到一个回文串 思路 只要 $k$ 至少比字母出现次数为基数次的字母个数 $- 1$ 还要多就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; string str; str.reserve(1e5 + 10); for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k >> str; map<char, int> cnt; for (auto &item: str) cnt[item]++; int edd = 0; for (auto &item: cnt) edd += item.second % 2; cout << ((k >= edd - 1) ? "YES" : "NO") << endl; } } C. Raspberries 大致题意 有一个数组,和一个 $k$,每次操作可以将数组上的某一个值 $+ 1$,问至少需要操作几次,才能让数组的所有值乘积是 $k$ 的倍数 思路 注意题目给出的 $k$ 的范围,仅有可能是 $[2, 5]$,这其中还恰好基本都是素数,仅 $4$ 不是,所以基本上都是其中单个值满足倍数关系了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (auto &i: data) cin >> i; if (k != 4) { int mi = k, ma = 0; for (auto &i: data) { mi = min(mi, i % k); ma = max(ma, i % k); } if (mi == 0) cout << 0 << endl; else cout << (k - ma) << endl; } else { int even = 0, ma = 0; for (auto &i: data) { even += i % 4 == 0 ? 2 : i % 2 == 0; ma = max(ma, i % 4); } cout << min(max(0, 2 - even), 4 - ma) << endl; } } } D. In Love 大致题意 有一个线段的集合,每次往里面加一个线段或者删除一个线段,问每次操作后,是否存在两个线段他们不重叠 思路 也很简单,维护好线段右边最小的和线段左边最大的两个堆即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void solve() { int n; cin >> n; priority_queue<pair<int, int>, vector<pair<int, int>>, less<>> prq1; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> prq2; map<int, int> mvd1, mvd2; for (int i = 0; i < n; ++i) { char op; int l, r; cin >> op >> l >> r; if (op == '+') { prq1.emplace(l, i); prq2.emplace(r, i); } else { mvd1[l]++; mvd2[r]++; } while (!prq1.empty()) { auto item = prq1.top(); auto iter = mvd1.find(item.first); if (iter != mvd1.end()) { while (iter->second--) prq1.pop(); mvd1.erase(iter); } else break; } while (!prq2.empty()) { auto item = prq2.top(); auto iter = mvd2.find(item.first); if (iter != mvd2.end()) { while (iter->second--) prq2.pop(); mvd2.erase(iter); } else break; } if (prq1.empty() || prq2.empty() || prq1.size() == 1 || prq2.size() == 1) cout << "NO" << endl; else cout << (prq1.top().first > prq2.top().first ? "YES" : "NO") << endl; } } E. Look Back 大致题意 有一个数组,每次可以让其中一个值翻倍,问至少操作多少次,才能让整个数组不递减 思路 这道题从二进制角度考虑就很简单。 翻倍其实就是左移一位,所以如果两个值本身的最高的比特位置相同,那么如果这两个值仍然存在前者大于后者的情况,那么后者需要在前者左移的基础上,再左移一位即可。反之则和前者左移次数相同即可 问题是如何构造最高比特位相同的数组。我们可以先人工把所有值都左移到某个位置,到时候再右移回来即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n), p(n, 0), ans(n, 0); for (auto &i: data) cin >> i; for (int i = 0; i < n; ++i) { while (data[i] <= INT_MAX) { p[i]++; data[i] <<= 1; } } for (int i = 1; i < n; ++i) ans[i] = ans[i - 1] + (data[i] >= data[i - 1] ? 0 : 1); int last = p[0] + ans[0], tot = 0; for (int i = 0; i < n; ++i) { ans[i] += p[i]; last = min(ans[i], last); ans[i] -= last; tot += ans[i]; } cout << tot << endl; } } F. You Are So Beautiful 大致题意 给你一个数组,问是否存在这样的一个子串,满足不同时存在两个子序列和这个子串相同,问有多少个这样的子串 思路 注意题目要找的是子串。说白了也很简单,只需要这个子串最左边的值是原串中这个值最左边的,右边也同样,即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; map<int, int> cnt[2]; for (auto &i: data) cnt[1][i]++; int ans = 0; for (auto &i: data) { auto iter = cnt[1].find(i); iter->second--; if (iter->second == 0) cnt[1].erase(iter); // left has if (cnt[0].count(i)) continue; ans += (int) cnt[1].size(); // single if (iter->second == 0) ans++; cnt[0][i]++; } cout << ans << endl; } } G2. Dances (Hard Version) 直接看 hard 版本吧,其实都挺简单的,感觉这场最有意思的应该是是 E 题 大致题意 有两个数组,其中数组 $a$ 的某一个值可以是 $[1, m]$ 中任意一个。允许你每次操作同时从数组中删除掉一个值,操作完成后再重新排列数组,问是否可以满足数组 $b$ 的每一项都 $> a$ 思路 贪心一下就行了,说白了就是从 $b$ 里找第一个大于等于每一个 $a$ 的值即可。然后剩下的值都删掉 至于那个特殊的值?可以在剩下要删掉的里面取最大的那个,如果那个特殊值小于它的时候,则不用删,否则删除 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<int> a(n - 1), b(n); for (auto &i: a) cin >> i; for (auto &i: b) cin >> i; sort(a.begin(), a.end()); sort(b.begin(), b.end()); int lastB = -1, base = 0, cur = 0; for (int &i: a) { while (cur < n && i >= b[cur]) lastB = b[cur++]; if (cur >= n) base++; cur++; } if (lastB == -1) lastB = b.back(); cout << 1LL * base * m + max(0, m - lastB + 1) << endl; } }

2023/11/27
articleCard.readMore

Codeforces Round 904 (Div. 2)

D 题有点难,数论确实不会,本着只是为了练习回复脑子的角度考虑,就不写了 A. Simple Design 大致题意 有两值,$x, k$,找到最小的 $y$ 满足 $y \geq x, y \space mod \space k = 0$ 思路 因为 $k$ 很小,所以暴力枚举就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int x, k; cin >> x >> k; auto cal = [&](int x) { int ans = 0; while (x) { ans += x % 10; x /= 10; } return ans; }; while (true) { int tmp = cal(x); if (tmp % k == 0) { cout << x << endl; break; } x++; } } } B. Haunted House 大致题意 有一个 $01$ 字符串,每次允许交换两个相邻的值,问交换多少次,就可以是 $2^i$ 的倍数(对于所有可能的 $i$) 思路 都告诉你二进制了,保证最后几个为 $0$ 的方案而已,简单模拟一下就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { string str; str.reserve(1e5); int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n >> str; int l = (int) str.size(), ans = 0; for (int r = (int) str.size() - 1; r >= 0; --r) { l--; while (l >= 0 && str[l] == '1') l--; if (l >= 0) ans += r - l; if (l < 0) cout << -1 << ' '; else cout << ans << ' '; } cout << endl; } } C. Medium Design 大致题意 有一堆区间,可以选出其中一部分,对这些区间内的值 +1 问这样操作之后的区间的最大值降去最小值的最大差值可以是多少 思路 排序一下,然后遍历,因为最小值一定出现在第一个值或者最后一个值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 vector<pair<int, int>> v; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; v.resize(n); for (auto &i: v) cin >> i.first >> i.second; struct cmp { bool operator()(const int &lhs, const int &rhs) const { return v[lhs].second > v[rhs].second; } }; priority_queue<int, vector<int>, cmp> prq; sort(v.begin(), v.end()); int l = 0, r = 0, cur = 0, ans = 0; for (int i = 0; i < v.size(); ++i) { auto &item = v[i]; prq.push(i); cur++; if (item.first <= 1) l++; if (item.second >= m) r++; while (!prq.empty()) { if (v[prq.top()].second < item.first) { if (v[prq.top()].first <= 1) l--; prq.pop(); cur--; } else break; } ans = max(ans, max(cur - l, cur - r)); } cout << ans << endl; } }

2023/11/25
articleCard.readMore

Codeforces Round 903 (Div. 3)

A. Don’t Try to Count 大致题意 给出两个字符串 $n, m$,允许 $n$ 每次往自己拼接在自己后面,使得 $n$ 中出现 $m$ 字符串,问最少需要几次操作 思路 因为 $n, m$ 都很小,所以直接保留就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; string str1, str2; str1.reserve(25); str2.reserve(25); cin >> n >> m >> str1 >> str2; int i = 0, flag = false; for (i = 0; i <= 5; ++i) { if (str1.find(str2) != -1) { flag = true; break; } str1 += str1; } cout << (flag ? i : -1) << endl; } } B. Three Threadlets 大致题意 有三根木棍,允许最多切三刀,问能否使得所有木棍都一样长 思路 因为只能切三刀,所以就很局限了 如果一个棍子切一刀的方案下,那么必然只能中间切开,那么肯定最初大家都是一样的,故不可能存在这个情况 如果分别为切 $0, 1, 2$ 的方案下,那么必然第二根棒子的长度得是第一根的两倍,第三根则为三倍 如果分别为切 $0, 0, 3$ 的方案下,那么必然第三个棒子的长度得是前两根的四倍,同时第二根和第一个等长 如果分别为切 $0, 1, 1$ 的方案下,那么必然第二个和第三个棒子的长度得是第一根的两倍 如果分别为切 $0, 0, 2$ 的方案下,那么必然第三个棒子的长度得是前两根的三倍,同时第二根和第一个等长 如果分别为切 $0, 0, 1$ 的方案下,那么必然第三个棒子的长度得是前两根的两倍,同时第二根和第一个等长 嗯,枚举所有情况即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { vector<int> data(3); cin >> data[0] >> data[1] >> data[2]; sort(data.begin(), data.end()); if (data[0] == data[2]) { cout << "YES" << endl; continue; } if (data[0] == data[1]) cout << (data[2] == data[0] * 2 || data[2] == data[0] * 3 || data[2] == data[0] * 4 ? "YES" : "NO") << endl; else cout << (data[1] == data[0] * 2 && (data[2] == data[0] * 2 || data[2] == data[0] * 3) ? "YES" : "NO") << endl; } } C. Perfect Square 大致题意 矩阵旋转 $90$ 度后仍然相同,每次允许把矩阵中的一个值加一,问最少需要改多少次 思路 搞清楚旋转后,的每个位置映射的地方即可,很简单 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<string> data(n); for (auto &item : data) item.resize(n); for (auto &item : data) cin >> item; auto trans = [&](int &x, int &y) { swap(x, y); y = n - y - 1; }; int ans = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { char tmp[4]; int x = i, y = j; for (int l = 0; l < 4; ++l) { tmp[l] = data[x][y]; trans(x, y); } sort(tmp, tmp + 4); ans += tmp[3] * 3 - tmp[0] - tmp[1] - tmp[2]; } } cout << ans / 4 << endl; } } D. Divide and Equalize 大致题意 有 $n$ 个数字,每次允许你从中挑选两个数字,将其中一个数字除以 $x$,另外一个数字乘以 $x$。注意操作后两数仍然是正整数,问是否能让所有数字相同 思路 简单题,把所有数字质因子分解了,然后看看每个质因子的出现次数是不是数组长度的倍数就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #define int long long void solve() { vector<bool> notPrime(1e6 + 10, false); vector<int> prime; notPrime[1] = true; for (int i = 2; i < 1e6 + 10; ++i) { if (notPrime[i]) continue; prime.push_back(i); for (int j = i * i; j <= 1e6 + 10; j += i) notPrime[j] = true; } int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; map<int, int> mp; for (auto item: data) { for (auto p : prime) { while (item % p == 0) { mp[p]++; item /= p; } if (item == 1) break; if (!notPrime[item]) { mp[item]++; break; } } } bool flag = true; for (auto iter = mp.begin(); iter != mp.end(); ++iter) if (iter->second % n != 0) flag = false; cout << (flag ? "YES" : "NO") << endl; } } E. Block Sequence 大致题意 有一个数组,希望能删掉一些值,使得整个数组满足一个特征: 整个数组可以拆分成几个连续的块,每个块第一个数字表示这个块内后面的数字个数 问最少需要删掉几个 思路 很容易想到用 dp 解决 假定当前位置为某个块的开头,那么带来的价值就是 dp[i + a[i]] = dp[i] AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int i = 0; i < _; ++i) { int n; cin >> n; vector<int> data(n), ans(n + 1, INT_MAX); for (auto &item : data) cin >> item; ans[0] = 0; if (data[0] + 1 <= n) ans[data[0] + 1] = 0; for (int i = 1; i < n; ++i) { ans[i] = min(ans[i], ans[i - 1] + 1); if (i + data[i] + 1 <= n) ans[i + data[i] + 1] = min(ans[i], ans[i + data[i] + 1]); } ans[n] = min(ans[n], ans[n - 1] + 1); cout << ans[n] << endl; } } F. Minimum Maximum Distance 大致题意 有一棵树,有些节点是红色的。求算树上的每一个节点到达最远的那个红色节点所需要的距离中,最小的那个值是多少 思路 树上做两次 dfs 就行了,第一次求出每个节点它下面最深的红色节点距离的位置,第二次做类似换根操作即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, ans = INT_MAX; cin >> n >> k; vector<int> deep(n + 1), mDeep(n + 1); set<int> mark; vector<vector<int>> edge(n + 1); for (int i = 0; i < k; ++i) { int tmp; cin >> tmp; mark.insert(tmp); } for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; edge[u].push_back(v); edge[v].push_back(u); } if (n == 1) { cout << 0 << endl; continue; } function<void(int, int)> dfs1 = [&](int x, int p) { for (auto &i: edge[x]) { if (p == i) continue; deep[i] = deep[x] + 1; dfs1(i, x); } mDeep[x] = mark.count(x) ? 0 : INT_MIN; for (auto &i: edge[x]) { if (p == i) continue; mDeep[x] = max(mDeep[x], mDeep[i] + 1); } }; function<void(int, int, int)> dfs2 = [&](int x, int p, int v) { ans = min(ans, max(v, mDeep[x])); if (edge[x].size() == 1 && p != -1) return; if (p != -1 && edge[x].size() == 2) { dfs2(edge[x][0] == p ? edge[x][1] : edge[x][0], x, mark.count(x) ? max(v + 1, 1) : v + 1); return; } if (p == -1 && edge[x].size() == 1) { dfs2(edge[x][0], x, mark.count(x) ? max(v + 1, 1) : v + 1); return; } sort(edge[x].begin(), edge[x].end(), [&](const int &u, const int &v) { if (u == p) return false; if (v == p) return true; return mDeep[u] > mDeep[v]; }); int base = mark.count(x) ? 1 : INT_MIN; for (int i = 0; i < edge[x].size() - 1; ++i) dfs2(edge[x][i], x, max(base, max(v, mDeep[(i == 0 ? edge[x][1] : edge[x][0])] + 1) + 1)); }; deep[1] = 0; dfs1(1, -1); dfs2(1, -1, INT_MIN); cout << ans << endl; } } G. Anya and the Mysterious String 大致题意 有一个字符串,每次可以选择其中一段区间,为每一个字母加上一个值,即 a + 1 = b, b + 2 = d 这种循环编码,然后同时询问某一段内是否存在回文串 思路 回文串可以考虑最小单位,即两个相邻的相同字母就是回文,或者间隔一个的相同字母,目的就是查找到这些 由于有区间加法操作,所以考虑到线段树 在线段树上每个节点,都记录它下面最前面两个字母和最后两个字母,然后合并的时候可以计算因为合并,贴在一起的那一段内是否出现了回文即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 struct SegTree { vector<int> data1, data2, data3, data4, lazy; vector<bool> flag; int atom; explicit SegTree(int n) : data1((n << 1) + 10), data2((n << 1) + 10), data3((n << 1) + 10), data4((n << 1) + 10), lazy((n << 1) + 10), flag((n << 1) + 10, false), atom(-1) {} static inline int get(int l, int r) { return (l + r) | (l != r); } void up(int l, int r) { if (l == r) return; int mid = (l + r) >> 1; int cur = get(l, r), lx = get(l, mid), rx = get(mid + 1, r); flag[cur] = flag[lx] || flag[rx]; data1[cur] = data1[lx]; data2[cur] = data2[lx]; data3[cur] = data3[rx]; data4[cur] = data4[rx]; if (data2[cur] < 0) data2[cur] = data1[rx]; if (data3[cur] < 0) data3[cur] = data4[lx]; if (flag[cur]) return; if (data4[lx] == data1[rx] || data4[lx] == data2[rx] || data3[lx] == data1[rx]) flag[cur] = true; else flag[cur] = false; } void build(int l, int r) { int cur = get(l, r); lazy[cur] = 0; if (l == r) { data2[cur] = atom--; data3[cur] = atom--; return; } int mid = (l + r) >> 1; build(l, mid); build(mid + 1, r); up(l, r); } void push(int l, int r) { int cur = get(l, r); if (!lazy[cur]) return; int mid = (l + r) >> 1; int lx = get(l, mid), rx = get(mid + 1, r); data1[lx] = (data1[lx] + lazy[cur]) % 26; data2[lx] = data2[lx] < 0 ? data2[lx] : (data2[lx] + lazy[cur]) % 26; data3[lx] = data3[lx] < 0 ? data3[lx] : (data3[lx] + lazy[cur]) % 26; data4[lx] = (data4[lx] + lazy[cur]) % 26; lazy[lx] = (lazy[lx] + lazy[cur]) % 26; data1[rx] = (data1[rx] + lazy[cur]) % 26; data2[rx] = data2[rx] < 0 ? data2[rx] : (data2[rx] + lazy[cur]) % 26; data3[rx] = data3[rx] < 0 ? data3[rx] : (data3[rx] + lazy[cur]) % 26; data4[rx] = (data4[rx] + lazy[cur]) % 26; lazy[rx] = (lazy[rx] + lazy[cur]) % 26; lazy[cur] = 0; } void update(int l, int r, int x, int y, int w) { if (l == x && y == r) { int cur = get(l, r); data1[cur] = (data1[cur] + w) % 26; data2[cur] = data2[cur] < 0 ? data2[cur] : (data2[cur] + w) % 26; data3[cur] = data3[cur] < 0 ? data3[cur] : (data3[cur] + w) % 26; data4[cur] = (data4[cur] + w) % 26; lazy[cur] = (lazy[cur] + w) % 26; return; } push(l, r); int mid = (l + r) >> 1; if (y <= mid) update(l, mid, x, y, w); else if (x > mid) update(mid + 1, r, x, y, w); else { update(l, mid, x, mid, w); update(mid + 1, r, mid + 1, y, w); } up(l, r); } bool query(int l, int r, int x, int y) { if (l == x && y == r) { return flag[get(l, r)]; } push(l, r); int mid = (l + r) >> 1; if (y <= mid) return query(l, mid, x, y); else if (x > mid) return query(mid + 1, r, x, y); else { bool tmp = query(l, mid, x, mid) || query(mid + 1, r, mid + 1, y); if (tmp) return true; int lx = get(l, mid), rx = get(mid + 1, r); if (data4[lx] == data1[rx]) return true; if (x <= mid - 1 && data3[lx] == data1[rx]) return true; if (y > mid + 1 && data4[lx] == data2[rx]) return true; } return false; } void debug(int l, int r) { #ifdef ACM_LOCAL int cur = get(l, r); cerr << '[' << l << '-' << r << "]: " << flag[cur] << "\t" << (data1[cur] >= 0 ? char(data1[cur] + 'a') : ' ') << (data2[cur] >= 0 ? char(data2[cur] + 'a') : ' ') << (data3[cur] >= 0 ? char(data3[cur] + 'a') : ' ') << (data4[cur] >= 0 ? char(data4[cur] + 'a') : ' ') << endl; if (l == r) return; int mid = (l + r) >> 1; debug(l, mid); debug(mid + 1, r); #endif } }; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; string str; str.reserve(n); SegTree tree(n); cin >> str; for (int i = 0; i < n; ++i) tree.data4[(i + 1) << 1] = tree.data1[(i + 1) << 1] = (str[i] - 'a'); tree.build(1, n); for (int i = 0; i < q; ++i) { int o, l, r, w; cin >> o >> l >> r; if (o == 1) { cin >> w; tree.update(1, n, l, r, w % 26); } else cout << (tree.query(1, n, l, r) ? "NO" : "YES") << endl; } tree.debug(1, n); } }

2023/11/21
articleCard.readMore

Educational Codeforces Round 156 (Rated for Div. 2)

A. Sum of Three 大致题意 将一个数拆成三个数,要求这三个数不同且都不是 $3$ 的倍数,给出一种拆法即可 思路 要拆成三个不同的数,且都不是 $3$ 的倍数,那么最小之能拆成 $1, 2, x$ 且 $x \geq 4$,而且还得保证 $x$ 不是 $3$ 的倍数。若这样拆了之后 $x$ 还是 $3$ 的倍数,那就只能 $1, 4, x$ 这样拆 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; if (n <= 6 || n == 9) { cout << "NO" << endl; } else if (n % 3) { cout << "YES" << endl; cout << "1 2 " << n - 3 << endl; } else { cout << "YES" << endl; cout << "1 4 " << n - 5 << endl; } } } B. Fear of the Dark 大致题意 笛卡尔坐标系上有两个灯,一个目标点,现在需要从 $(0, 0)$ 出发,走到目标点,路径完全任意,但是必须在灯光下走,问这两盏灯的最小灯光范围是多少 思路 比较简单,只有两种可能:1、只用一盏灯,2、同时用两盏灯 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int px, py, ax, ay, bx, by; cin >> px >> py >> ax >> ay >> bx >> by; auto dist = [&](int a, int b, int x, int y) { return sqrt((a - x) * (a - x) + (b - y) * (b - y)); }; double a0 = dist(0, 0, ax, ay); double b0 = dist(0, 0, bx, by); double ap = dist(px, py, ax, ay); double bp = dist(px, py, bx, by); double ab = dist(ax, ay, bx, by); double ans = max(ap, a0); ans = min(ans, max(bp, b0)); ans = min(ans, max(max(min(a0, b0), min(ap, bp)), ab / 2)); cout << setprecision(10) << ans << endl; } } C. Decreasing String 大致题意 有一个初始的字符串,每次删除一个,使其每次都保证是字典序最小的方案,将每一次的结果字符串拼接,得到一个最终结果字符串,问这个字符串的第 $x$ 位的字母是什么 思路 也是比较简单的题,要保证字典序最小,那就得使得字符串前缀尽可能保证非递减即可。用单调栈模拟一下就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void solve() { int _; cin >> _; string str; str.reserve(1e6 + 10); for (int ts = 0; ts < _; ++ts) { int pos; cin >> str >> pos; // bs if (pos <= str.size()) { cout << str[pos - 1]; continue; } int l = 0, r = str.size(); while (l + 1 < r) { int mid = (l + r) >> 1; int tot = (str.size() + (str.size() - mid)) * (mid + 1) / 2; if (tot < pos) l = mid; else r = mid; } pos -= (str.size() + (str.size() - l)) * (l + 1) / 2 + 1; vector<char> st; int cur = 0; l++; while (l--) { while (cur < str.size() && (st.empty() || st.back() <= str[cur])) st.push_back(str[cur++]); if (cur == str.size()) st.pop_back(); else if (!st.empty() && st.back() > str[cur]) st.pop_back(); } while (cur < str.size()) st.push_back(str[cur++]); cout << st[pos]; } }

2023/11/20
articleCard.readMore

Codeforces Round 902 (Div. 2, based on COMPFEST 15 - Final Round)

A. Goals of Victory 大致题意 有 $n$ 只队伍,所有队伍两两对战,两个队伍因此得分之和一定为 0。将每个队伍的所有得分之和相加,现在已知其中 $n-1$ 个队伍的得分情况,问最后一个队伍的得分情况是 思路 简单题,说白了就是所有人分数之和肯定是 $0$,那就把剩下的人都加起来,取负数就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int tot = 0; for (int i = 0; i < n - 1; ++i) { int tmp; cin >> tmp; tot += tmp; } cout << -tot << endl; } } B. Helmets in Night Light 大致题意 村长需要通知村里的所有人一个消息,村长每通知一个人,就要花费 $k$ 的成本,而听到消息的村民也可以相互通知,每个村民最多再通知 $a_i$ 个其他村民,同时每通知一个其他村民就要花费 $b_i$ 的费用,问最少的费用 思路 村长可以无限通知,但是成本固定,所以能让成本低的村民通知的情况下,村长只需要通知一次即可,当成本低的村民的通知机会都用完了之后,剩下的都村长通知一下就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, p; cin >> n >> p; vector<pair<int, int>> data(n); for (auto &i: data) cin >> i.second; for (auto &i: data) cin >> i.first; int cost = p, cnt = 1; sort(data.begin(), data.end()); for (auto &i: data) { if (i.first >= p) break; int c = min(n - cnt, i.second); cnt += c; cost += c * i.first; if (cnt == n) break; } cost += p * (n - cnt); cout << cost << endl; } } C. Joyboard 大致题意 有一个数组 $a$,其长度为 $n+1$,现在需要往第 $n+1$ 这个位置上放上一个在 $[0, m]$ 的整数,然后对于每一个 $a_i = a_{i+1} % i$ 问,如果希望整个数组中恰好有 $k$ 种不同的数字,则 $n+1$ 这个位置上的选择有多少 思路 首先,无论最终填上的值有多大,最终只要经过第一层 $mod$ 操作后,其值一定在 $[0, n)$ 之间了,且经历第二次 $mod$ 操作后,一定会变成 $0$(因为计算是从大到小进行计算的,所以一定会遇到相同的值,否则都是大于当前值的,不会改变 $mod$ 的结果)。所以最终一定最多只能存在 $3$ 个不同的数字。 那么什么时候为 $1$ 个呢,也很简单,因为最终一定为 $0$,故开始值也必须为 $0$ 然后是两个的情况,那么如果我一开始的值就是在 $[0, n)$ 之内,那么必然也可以满足,因为遇到的第一个可以 $mod$ 的值就是它自己 但是要考虑一种特殊情况,那就是恰好是 ${n, 2 \times n, 3 \times n \dots}$ 的情况,因为虽然不在 $[0, n)$ 之内,但是第一次 $mod$ 会直接变成 $0$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; if (k == 1) cout << 1 << endl; else if (k == 2) cout << min(m, n) + (m > n ? (m / n) - 1 : 0) << endl; else if (k == 3) cout << max(0, m - n) - (m > n ? (m / n) - 1 : 0) << endl; else cout << 0 << endl; } } D. Effects of Anti Pimples 大致题意 有一个数组 $a$,开始的时候都是白色,每次可以任意选择一个或者几个数字,将其染上黑色。然后将每个黑色的位置下标的倍数处,染成绿色。最后从不是白色的块中选出值最高的。问所有的选择情况下,所有得到的值之和是多少 思路 看起来很简单,但是实际上也非常简单的题 如果一个值被染成黑色,那么其能够带来的效果是确定的,即其倍数上的节点中最大的那个,我们将其先成为可能最大值 接下来是统计每个最大值的出现的次数,简单来说就是相同的可能最大值中挑选任意几个(至少一个),然后在剩下的可能最大值小于当前值中挑选 $0$ 个或者多个即可,即 $\sum{i=1}^{t} \binom{i}{t} * \sum{i=0}^{n-t} \binom{i}{n-t}$ 而这里的求和又非常的简单,因为 $\sum_{i=0}^{x} \binom{i}{x} = 2^x$,所以,就非常简单了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define int long long void solve() { const int mod = 998244353; vector<int> c0(1e5+10), c1(1e5+10); c0[0] = 1; c1[0] = 0; for (int i = 1; i < c0.size(); ++i) { c0[i] = (c0[i - 1] * 2) % mod; c1[i] = (c0[i] + mod - 1) % mod; } int n; cin >> n; vector<int> data(n), top(n); for (auto &i: data) cin >> i; for (int i = 0; i < n; ++i) { top[i] = data[i]; for (int j = i + 1; j <= n; j += i + 1) top[i] = max(top[i], data[j - 1]); } map<int, int> cnt; for (auto &i: top) cnt[i]++; int last = n, ans = 0; for (auto iter = cnt.rbegin(); iter != cnt.rend(); ++iter) { last -= iter->second; ans = (ans + ((iter->first * ((c1[iter->second] * c0[last]) % mod)) % mod)) % mod; } cout << ans << endl; }

2023/10/29
articleCard.readMore

Codeforces Round 901 (Div. 2)

最近双十一加班严重,难得有一个完整的周末假期,来写点题稍微恢复一下脑子吧 A. Jellyfish and Undertale 大致题意 有一个炸弹,有倒计时在缓慢减少,你有 $n$ 个道具,每次你可以花费 1s 的时间来使用,使得倒计时增加 $v_i$ 秒,但是由于一些故障,每次加完后,不能超过上限 $a$,否则就会变成 $a$。问最多可以让炸弹坚持到几秒 思路 注意操作可以是任何时候进行的,所以当每次只剩下 1s 的时候操作就是最好的,不然就炸了,因为是先完成加时间,再扣除当前操作的 1s,故只需要考虑每个都在 1s 的时候操作即可,即对每个值取 $min(v_i, a - 1)$ 然后求和就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int a, b, n; cin >> a >> b >> n; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; b += min(tmp, a - 1); } cout << b << endl; } } B. Jellyfish and Game 大致题意 A 有 $n$ 个苹果,每个都有重量,B 有 $m$ 个,每次交换,A 或者 B 可以选择自己的一个苹果给对方,同时从对方那边拿来一个苹果,两人都希望自己的苹果重量之和最大,问依次交换 $x$ 次后,$A$ 的苹果重量之和是多少 思路 模拟就行了,说白了交换了两次之后,就是纯粹的互换相同的那两个苹果,只需要考虑最开始的两次即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; vector<int> a(n), b(m); for (auto &i: a) cin >> i; for (auto &i: b) cin >> i; auto sort_all = [&]() { sort(a.begin(), a.end()); sort(b.begin(), b.end()); }; sort_all(); if (a.front() < b.back()) { swap(a.front(), b.back()); } if (k >= 2) { sort_all(); if (b.front() < a.back()) { swap(b.front(), a.back()); } if (k % 2) { sort_all(); if (a.front() < b.back()) { swap(a.front(), b.back()); } } } int tot = 0; for (auto &i : a) tot += i; cout << tot << endl; } } C. Jellyfish and Green Apple 大致题意 有 $n$ 个苹果,要平均分给 $m$ 个人,每次可以把一片苹果平均切成两份,问至少要切几刀才能平分 思路 其实是一个小数二进制问题,根据小数二进制方式去解决,从高位开始,一步步减去需要的苹果块,每一步减完之后,就可以将剩下来的苹果块全部对切开,因为不会再用到更大的苹果块了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; n %= m; if (n == 0) { cout << 0 << endl; continue; } // check m is or not the power of 2 int tmp = gcd(m, n); tmp = m / tmp; bool flag = true; while (tmp != 1) { if (tmp % 2 == 1) { flag = false; break; } tmp >>= 1; } if (!flag) { cout << -1 << endl; continue; } int ans = n; n <<= 1; while (n) { n %= m; ans += n; n <<= 1; } cout << ans << endl; } } D. Jellyfish and Mex 大致题意 有一个数组,每次可以从中删除一个值,然后得到对应的 $mex$,问直到整个数组被完整删除后,所有得到的 $mex$,相加最小可能是多少 思路 举个例子来看 0 0 0 1 1 2 3 3 3 4 4 4 5 5 5 首先要让 $mex$ 尽可能小,那么就应该尽量挑小的开始删除,显然,如果我把 $0$ 删除完那就会使得后面所有的操作都是无代价的,即随便删的 $mex$ 都是 $0$。但是直接删除 $0$ 的代价非常大,因为前两次删除都会导致代价为 $6$ 的 $mex$。这是因为 $0$ 出现了 $3$ 次。如果我们先删除 $2$,然后再删除 $0$ 那么就会发现,只需要额外增加 $2$ 的代价,就能让后面删除 $0$ 的两次操作的代价从 $6$ 减少到 $2$。 所以可以得到,我们尽量应该删除越少越小的值,即如果值增加的情况下,数量还不减少,那么肯定没有必要优先做删除了,可以等 $mex$ 变成 $0$ 之后再动手。而对于这些值,当然也应该从较大者开始删除,这样可以尽快减小 $mex$ 的值(因为在上面的前提下,最大值的出现次数一定比较小值少)但是不能每个值都要操作,例如例子中的 $1$ 就是不需要操作的,即使其恰好在这条单调栈上,即需要从一个序列中取出最优的子序列 我们考虑最多会出现多少个这样的需要考虑的数字,假设刚好递减的情况,且数量为 $n$,那么总占用的数字数量就是 $n * (n + 1) / 2$。故对于长度为 $5000$ 的数组,实际上 $n < 100$。即 $n^2$ 暴力去找子序列是可以的 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; map<int, int> mp; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mp[tmp]++; } int mex = 0; while (mp.count(mex)) mex++; if (mex == 0) { cout << 0 << endl; continue; } vector<pair<int, int>> data; for (auto &iter: mp) { if (iter.first > mex) break; if (data.empty() || data.back().second > iter.second) data.emplace_back(iter); } reverse(data.begin(), data.end()); vector<int> dp(data.size()); for (int i = 0; i < data.size(); ++i) { dp[i] = (data[i].second - 1) * mex + data[i].first; for (int j = 0; j < i; ++j) dp[i] = min(dp[i], (data[i].second - 1) * data[j].first + data[i].first + dp[j]); } cout << dp.back() << endl; } }

2023/10/29
articleCard.readMore

Codeforces Round 900 (Div. 3)

A. How Much Does Daytona Cost? 大致题意 给出一个数组和一个数字,问数组内是否存在某个子区间,使得给出的值出现次数最多 思路 只有一个值也是子区间,只有它出现也是出现,所以只需要判断是否存在即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; bool flag = false; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp == k) flag = true; } cout << (flag ? "YES" : "NO") << endl; } } B. Aleksa and Stack 大致题意 要求你构造一个数组,长度为 $n$,严格递增,且满足 $3 \times a_{i+2} \space mod \space (a_{i+1} + a_{i+2}) \neq 0$ 思路 随便递增就行,要是成立了,就把 $a_{i+2}$ 再加一不就行了。注意开头两个值不可以是 $1, 2$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int a = 1, b = 3; cout << "1 3"; for (int i = 2; i < n; ++i) { int cur = b + 1; while ((cur * 3) % (a + b) == 0) cur++; cout << ' ' << cur; a = b; b = cur; } cout << endl; } } C. Vasilije in Cacak 大致题意 给出一个 $n$,问能否在 $[1, n]$ 内选出 $k$ 个值,其和恰好为 $x$ 思路 初始区间是 $[1, n]$,那么肯定最终能够构造出的值必定也是完全连续的,所以只需要考虑最大可能和最小可能即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, x; cin >> n >> k >> x; int mi = (1 + k) * k / 2, ma = (n + (n - k + 1)) * k / 2; cout << (x >= mi && x <= ma ? "YES" : "NO") << endl; } } D. Reverse Madness 大致题意 这题的题意隐藏得很好,就不写大致题意了,看原文会更有味道一些 思路 首先要理清楚题目到底要我们做什么,如果直接按照题目的要求去做,反而完全不知道如何下手 在给出一个 $x$ 后,查找到一个对应的 $i$ 满足 $l_i \leq x \leq r_i$,然后翻转 $[min(x, l_i + r_i - x), max(x, l_i + r_i - x)]$ 这个区间的字符串,问最终结果 首先回来看给出的两个数组的特点,比较有意思的一条 $l_i = r_{i-1}+1$。如果把每个 $i$ 对应的区间 $[l_i, r_i]$ 单独拎出来看,会发现这些所有的区间是不会重合的,是恰好完美覆盖整个字符串的,所以,查找到一个对应的 $i$ 满足 $l_i \leq x \leq r_i$,实际上就是要找到此时的 $x$ 处于哪个区间上 再来看后面的 $[min(x, l_i + r_i - x), max(x, l_i + r_i - x)]$。因为我们已经知道 $l_i \leq x \leq r_i$,所以设 $x = l + m, r = x + n$,故可以得到 $l_i + r_i - x \rightarrow l_i + x + n - x \rightarrow l_i + n \rightarrow r_i - m - n + n \rightarrow r_i - m$,而注意到 $x = l_i + m$,所以实际上最终的区间恰好是 $[l + m, r - m]$(假定 $m$ 较小的情况下,反之也可以得到类似结果) 很显然,实际上无论怎么翻转,大家的翻转区间要么不会有交集,要么是基于同一个点进行的翻转,所以实际上只需要记录下每个点被翻转了几次即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; string str; str.reserve(n); cin >> str; vector<int> a(k), b(k); for (auto &i: a) cin >> i; for (auto &i: b) cin >> i; int q; cin >> q; vector<bool> flag(n + 1, false); for (int i = 0; i < q; ++i) { int x; cin >> x; int index = int(upper_bound(a.begin(), a.end(), x) - a.begin()) - 1; int l = min(a[index] + b[index] - x, x), r = max(a[index] + b[index] - x, x); flag[l] = !flag[l]; if (r != b[index]) flag[r + 1] = !flag[r + 1]; } for (int i = 0; i < k; ++i) { bool f = false; for (int l = a[i]; l <= b[i]; ++l) { f ^= flag[l]; if (!f) cout << str[l - 1]; else cout << str[b[i] - (l - a[i]) - 1]; } } cout << endl; } } E. Iva & Pav 大致题意 有一个数组,每次给出一个下标 $l$ 和一个目标值 $x$,问能够找到另外一个尽可能大的下标 $r$,满足 $[l, r]$ 内所有值进行 $\&$ 计算后仍然大于 $x$ 思路 建了一棵线段树,然后二分答案,然后没了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 struct SegTree { vector<int> data; explicit SegTree(int size) : data((size << 1) + 10) {} static inline int get(int l, int r) { return (l + r) | (l != r); } void up(int l, int r) { int mid = (l + r) >> 1; data[get(l, r)] = data[get(l, mid)] & data[get(mid + 1, r)]; } // NOLINTNEXTLINE(*-no-recursion) void build(int l, int r) { if (l == r) return; int mid = (l + r) >> 1; build(l, mid); build(mid + 1, r); up(l, r); } // NOLINTNEXTLINE(*-no-recursion) int query(int l, int r, int x, int y) { if (l == x && r == y) return data[get(l, r)]; int mid = (l + r) >> 1; if (y <= mid) return query(l, mid, x, y); else if (x > mid) return query(mid + 1, r, x, y); else return query(l, mid, x, mid) & query(mid + 1, r, mid + 1, y); } }; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; SegTree tree(n); for (int i = 0; i < n; ++i) cin >> tree.data[(i + 1) << 1]; tree.build(1, n); int q; cin >> q; for (int i = 0; i < q; ++i) { int l, k; cin >> l >> k; if (tree.data[l << 1] < k) { cout << -1 << ' '; continue; } int ll = l, rr = n + 1; while (ll + 1 < rr) { int mid = (ll + rr) >> 1; if (tree.query(1, n, l, mid) >= k) ll = mid; else rr = mid; } cout << ll << ' '; } cout << endl; } } F. Vasilije Loves Number Theory 大致题意 初始有一个值 $n$,有两种操作 给出一个 $x$,使得 $n \leftarrow n \times x$,然后询问是否存在数字 $a$,满足 $gcd(a, n) = 1$ 的同时 $d(n \times a) = n$ 将 $n$ 改为初始值 其中 $d(x)$ 表示 $x$ 的因子数(非质因子数) 思路 首先,$n$ 几乎可以增长到非常大,所以肯定不能保存下来,当然,麻烦的事情肯定不止这个。 首先是 $d(n \times a)$ 如何计算,已知两个数的所有因子,且因子没有重复($gcd(a, n) = 1$)则此时 $d(n \times a) = d(n) \times d(a)$,而这个等式还需要 $=n$,故得到 $d(a) = \frac{n}{d(n)}$,而因为 $d(a)$ 一定是正整数,所以只需要满足右边的分数是整数即可。至于 $a$ 具体如何构造,可以直接拿一个足够大的素数的 $\frac{n}{d(n)} - 1$ 次幂即可 计算 $d(n)$ 则只需要对 $n$ 进行质因子分解即可,根据每次的乘法,进行累加质因子。而需要计算 $\frac{n}{d(n)}$ 是否是整数,则可以再对 $d(n)$ 进行质因子分解,看看两边的质因子是否有包含关系即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #define int long long void solve() { vector<int> prime; vector<int> flag(1e6 + 10, true); flag[0] = flag[1] = false; for (int i = 2; i < flag.size(); ++i) { if (!flag[i]) continue; for (int j = i * i; j < flag.size(); j += i) flag[j] = false; } for (int i = 2; i < flag.size(); ++i) if (flag[i]) prime.push_back(i); auto div = [&](int x, const function<void(int, int)> &callback) { for (int i: prime) { if (x % i != 0) continue; int cnt = 0; while (x % i == 0) { cnt++; x /= i; } callback(i, cnt); if (flag[x]) { callback(x, 1); return; } } }; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n >> q; map<int, int> cnt; int dn = 1; auto add = [&](int v, int c) { auto iter = cnt.find(v); if (iter == cnt.end()) { cnt.insert({v, 0}); iter = cnt.find(v); } dn /= iter->second + 1; iter->second += c; dn *= iter->second + 1; }; div(n, add); for (int i = 0; i < q; ++i) { int o; cin >> o; switch (o) { case 1LL: { int x; cin >> x; div(x, add); bool f = true; div(dn, [&](int v, int c) { if (cnt[v] < c) f = false; }); cout << (f ? "YES" : "NO") << endl; break; } case 2LL: cnt.clear(); dn = 1; div(n, add); break; default: break; } } cout << endl; } } G. wxhtzdy ORO Tree 大致题意 有一棵树,树上每个点上都有值,每次询问给出两个点 $u, v$,需要寻找一个 $z$,使得 $z$ 在 $u, v$ 的树上路径上,即最短路径上 $g(u, z) + g(z, v)$ 的值尽可能大 其中 $g(x, y)$ 表示在树上的 $x, y$ 两点的最短路径上的所有值进行或运算,得到的结果的值在二进制上,有多少个 bit 位是 1 思路 首先要找树上路径,大概率会用到 lca 算法。接下来因为要求算树上路径的或和,所以还可以在 lca 上加一个父节点的或运算的倍增表 而要寻找的点一定在两者的倍增节点上,由于目标节点一定是恰好可以产生一个比特位从 $o \rightarrow 1$ 的变化的,所以可以再记录下一个点的所有比特位,在其的最近的哪个父亲那完成了 $o \rightarrow 1$ 的变化 然后遍历所有的可能计算即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; ++i) cin >> a[i]; struct node { int v, n; }; vector<node> edge(n * 2); vector<int> head(n + 1, -1); for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; edge[i << 1] = {v, head[u]}; edge[(i << 1) | 1] = {u, head[v]}; head[u] = i << 1; head[v] = (i << 1) | 1; } // dep is the depth of node vector<int> dep(n + 1); // fa for lca, orFa for calculate 'or sum' faster, miB for the deepest node `x` which (x & (1 << i)) is true vector<vector<int>> fa(n + 1), orFa(n + 1), miB(n + 1); for (auto &i: fa) i.resize(20); for (auto &i: orFa) i.resize(20); for (auto &i: miB) i.resize(30); // build lca function<void(int, int)> dfs = [&](int u, int p) { dep[u] = dep[p] + 1; fa[u][0] = p; orFa[u][0] = a[p]; for (int i = 1; i < 20; ++i) { fa[u][i] = fa[fa[u][i - 1]][i - 1]; orFa[u][i] = orFa[u][i - 1] | orFa[fa[u][i - 1]][i - 1]; } for (int i = 0; i < 30; ++i) miB[u][i] = (a[p] & (1 << i)) ? p : miB[p][i]; for (int i = head[u]; ~i; i = edge[i].n) if (edge[i].v != p) dfs(edge[i].v, u); }; // lca function<int(int, int)> lca = [&](int u, int v) { if (dep[u] < dep[v]) swap(u, v); int diff = dep[u] - dep[v]; for (int i = 0; i < 20; ++i) if (diff & (1 << i)) u = fa[u][i]; if (u == v) return u; for (int i = 19; i >= 0; --i) { if (fa[u][i] == fa[v][i]) continue; u = fa[u][i]; v = fa[v][i]; } return fa[u][0]; }; // calculate the 'or sum' from u to v function<int(int, int)> ors = [&](int u, int v) { int p = lca(u, v); // -1 to avoid 'or' the parent double times int ld = dep[u] - dep[p] - 1, rd = dep[v] - dep[p] - 1; int ls = 0, rs = 0, ru = u, rv = v; if (ld > 0) for (int i = 0; i < 20; ++i) if (ld & (1 << i)) { ls |= orFa[ru][i]; ru = fa[ru][i]; } if (rd > 0) for (int i = 0; i < 20; ++i) if (rd & (1 << i)) { rs |= orFa[rv][i]; rv = fa[rv][i]; } return ls | rs | a[p] | (p == u ? 0 : a[u]) | (p == v ? 0 : a[v]); }; // calculate the bit count function<int(int)> bitCount = [&](int u) { int res = 0; while (u) { res += u & 1; u >>= 1; } return res; }; // find on the path (u -> p) function<int(int, int, int)> cal = [&](int u, int v, int p) { int ans = bitCount(a[u]) + bitCount(ors(u, v)); for (int i = 0; i < 30; ++i) { if (miB[u][i] == 0 || miB[u][i] == u) continue; if (dep[p] > dep[miB[u][i]]) continue; ans = max(ans, bitCount(ors(u, miB[u][i])) + bitCount(ors(v, miB[u][i]))); } return ans; }; dfs(1, 0); int q; cin >> q; for (int i = 0; i < q; ++i) { int u, v; cin >> u >> v; int p = lca(u, v); cout << max(cal(u, v, p), cal(v, u, p)) << ' '; } cout << endl; } }

2023/10/5
articleCard.readMore

Codeforces Round 899 (Div. 2)

A. Increasing Sequence 大致题意 有一个初始的数组,要求构造另外一个数组,使得新数组严格递增,同时任何一项不与原来的数组的对应项相同,问这个数组最后一个值最小是多少 思路 由于严格递增,所以最小的方式就是 $1, 2, 3, 4 \dots$,再考虑一下不能相同的这个情况,容易得到如果按照上述的方式撞上了相同,那么就再加一即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; int cur = 0; for (int i: data) { cur++; if (cur == i) cur++; } cout << cur << endl; } } B. Sets and Union 大致题意 有一堆集合,需要选出几个集合,使得这几个集合合并后的集合的元素数量尽可能多的同时,与全部集合直接合并的结果不同,问最大的集合元素数量 思路 因为数据量比较小,所以可以暴力解决,最简单的方式就是直接遍历所有可能的最终不出现在答案中的某个值,然后尝试最大化最终结果即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<vector<int>> data(n); for (auto &i: data) { int s; cin >> s; i.resize(s); for (auto &j: i) cin >> j; } int cnt[52] = {0}; for (auto &i: data) for (auto &j: i) cnt[j]++; int ans = 0; for (int i = 0; i < 52; ++i) { if (cnt[i] == 0) continue; set<int> st; for (auto &a: data) { bool flag = true; for (auto &b: a) if (b == i) flag = false; if (!flag) continue; for (auto &b: a) st.insert(b); } ans = max(ans, (int) st.size()); } cout << ans << endl; } } C. Card Game 大致题意 有一个数组,每一个值可能是任何整数,允许你每次选择一个当前在奇数位置的值,并得到对应的分数,或者删除掉一个当前在偶数位置的值。数组被取走值后会重新获得新的下标(不留出空位)问最多可以得到多少总分 思路 假定某个位置,我们对它进行了操作,无论是删除 or 得到对应的分数,总之其后面的所有值的下标都会经历奇数和偶数的两种可能,意味着对于后面的值,如果它是负数,那我一定能找到一个时间将其直接删除,否则我一定可以计算进入我的分数 所以只要知道第一个被操作的值是谁就行,其后面的所有正数都可以加入到分数中,而负数都可以删除。故直接从后往前遍历即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 ``` # D. Tree XOR ## 思路 快可以说是树上换根 dp 模版题了,不解释了 ## AC code ```cpp #define int long long using namespace std; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; ++i) cin >> a[i]; struct node { int v, n; }; vector<node> edge(n * 2); vector<int> head(n + 1, -1); for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; edge[i << 1].v = u; edge[i << 1].n = head[v]; head[v] = i << 1; edge[(i << 1) | 1].v = v; edge[(i << 1) | 1].n = head[u]; head[u] = (i << 1) | 1; } vector<int> cnt(n + 1, 0), cost(n + 1, 0); function<void(int, int)> dfs1 = [&](int u, int f) { cnt[u] = 1; for (int i = head[u]; ~i; i = edge[i].n) { int v = edge[i].v; if (v == f) continue; dfs1(v, u); cnt[u] += cnt[v]; cost[u] += cost[v]; } if (f) cost[u] += (a[u] ^ a[f]) * cnt[u]; }; vector<int> ans(n + 1, 0); function<void(int, int)> dfs2 = [&](int u, int f) { ans[u] = ans[f]; ans[u] -= cnt[u] * (a[u] ^ a[f]); ans[u] += (n - cnt[u]) * (a[u] ^ a[f]); for (int i = head[u]; ~i; i = edge[i].n) { if (edge[i].v == f) continue; dfs2(edge[i].v, u); } }; dfs1(1, 0); ans[1] = cost[1]; for (int i = head[1]; ~i; i = edge[i].n) dfs2(edge[i].v, 1); for (int i = 1; i <= n; ++i) cout << ans[i] << " \n"[i == n]; } }

2023/10/4
articleCard.readMore

Educational Codeforces Round#155 (Div. 2)

A. Rigged! 大致题意 有 $n$ 个人,每个人都可以举起最高一定重量的哑铃 $b_i$ 次,而每个人举的哑铃是同一个,最终举起次数最多的人获胜,裁判希望第一个人获胜,问应该陪多少的哑铃,或者不可能 思路 简单题,只要没有人既能举起比第一个人更重的同时,能够举起更多次就行,重量很直接选第一个人能举起的上线就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, s, e; cin >> n >> s >> e; bool flag = true; for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; if (u >= s && v >= e) { flag = false; } } cout << (flag ? s : -1) << endl; } } B. Chips on the Board 大致题意 有一个棋盘,每个位置的价值是对应的横坐标价格和纵坐标价格相加。现在可以往棋盘上放棋子,费用上位置的价值。放上数个后,使得棋盘上每一个位置,其所在的行或者所在的列中至少存在一个棋子,问最小费用 思路 简单题,容易得出,必定每一行或者每一列都有一个棋子,这是最低的要求,然后就很简单了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int ma = LONG_LONG_MAX, mb = LONG_LONG_MAX, sa = 0, sb = 0, tmp; for (int i = 0; i < n; ++i) { cin >> tmp; ma = min(ma, tmp); sa += tmp; } for (int i = 0; i < n; ++i) { cin >> tmp; mb = min(mb, tmp); sb += tmp; } cout << min(sa + n * mb, sb + n * ma) << endl; } } C. Make it Alternating 大致题意 有一个 $01$ 串,允许按照一定顺序删除掉一些字符,使得整个字符串没有连续的相同字符,问有多少种删除方式 思路 拿一个例子进行考虑,比如 $11000$,明显,我们需要在前两个中删除一个,在后面三个中删除两个,然后这三个值的顺序任意都可,所以就是 $$\begin{pmatrix}2 \\ 1\end{pmatrix} \times\begin{pmatrix}3 \\ 2\end{pmatrix} \timesA^3_3$$ 看懂了公式就能算出来了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { const int mod = 998244353; vector<int> p(2e5 + 10); p[0] = p[1] = 1; for (int i = 2; i < p.size(); ++i) p[i] = (i * p[i - 1]) % mod; int _; cin >> _; string str; for (int ts = 0; ts < _; ++ts) { str.reserve(2e5+10); cin >> str; int ans = 1, tot = 0, cnt = 1; for (int i = 1; i < str.size(); ++i) { if (str[i] != str[i - 1]) { ans = (ans * cnt) % mod; cnt = 1; } else { cnt++; tot++; } } ans = (ans * cnt) % mod; ans = (ans * p[tot]) % mod; cout << tot << ' ' << ans << endl; } } D. Sum of XOR Functions 大致题意 有一个数组,需要求算 $\sum^n_{l=1}\sum^n_{r=l} f(l,r) \times (r-l+1)$,其中 $f(l,r)$ 表示 $[l, r]$ 区间的异或和 思路 首先要明确异或和的本质,对于任意一个比特位而言,若所有值中此比特位为 1 的数量为奇数,则最终为奇数。所以需要统计任意区间内的某个比特位的奇偶情况,故可以先做一次前缀异或和,这样得到的就是累积的奇偶情况了。 对于一个区间,如果其左区间(前一个)的前缀异或和的某个位的结果是 1(奇数),而右区间则为 0(偶数),则说明这个区间内这个比特位出现奇数次,那么就可以计算其贡献,为 $(r-l+1) \times 2^p)$,拆解一下公式可以得到 $r \times 2^p - (l-1) \times 2^p$。 对于每一个比特位,我们考虑遍历所有可能的右区间,对于每一个右区间,要找出左边出现了几次和右区间的前缀和的比特位结果不同的次数,那么就是这个 $r \times 2^p$ 部分的价值出现的次数,同时减去左边所有不同的 $(l-1) \times 2^p$ 的价值,就可以得到当前位置作为右区间的时候,能够带来的价值。而计算后的 $r \times 2^p$ 部分,恰好是其作为左区间的时候的 $(l-1) \times 2^p$ 的价值。 所以只需要遍历一遍,记录左边出现了几次 $0$ 和几次 $1$,同时对于 $0$ 而言,产生了多少价值,同理对 $1$ 也一样,然后作为下一个节点的计算输入 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define int long long void solve() { int n; cin >> n; vector<int> data(n); for (auto &i: data) cin >> i; for (int i = 1; i < n; ++i) data[i] = data[i] ^ data[i - 1]; const int mod = 998244353; int ans = 0; for (int e = 0; e <= 32; ++e) { int s1 = 0, c1 = 0, s0 = 0, c0 = 1; for (int i = 0; i < n; ++i) { int rp = ((i + 1) * (1LL << e)) % mod; if (data[i] & (1LL << e)) { ans = (ans + rp * c0 - s0) % mod; s1 = (s1 + rp) % mod; c1++; } else { ans = (ans + rp * c1 - s1) % mod; s0 = (s0 + rp) % mod; c0++; } } } cout << ans << endl; }

2023/10/2
articleCard.readMore

Codeforces Round 898 (Div. 4)

A. Short Sort 大致题意 有三张卡片,分别为 $a, b, c$,已经在桌面上乱序排好,最多交换两张卡片的位置,问是否能够变成有序的 $a, b, c$ 思路 简单题,判断一下是不是至少有一位是保持 $a, b, c$ 的顺序即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str; str.reserve(3); cin >> str; int cnt = 0; for (int i = 0; i < 3; ++i) cnt += str[i] == i + 'a'; cout << (cnt == 1 || cnt == 3 ? "YES" : "NO") << endl; } } B. Good Kid 大致题意 有一个数组,允许你给其中一个值加一,问最终所有值的乘积最大是多少 思路 简单题,如果有两个及以上的 $0$,那么最终结果一定还是 $0$。如果只有一个 $0$,那就等于忽略这个 $0$ 即可,剩下的情况,因为加了之后的效果是 $\frac{x+1}{x}$,所以 $x$ 越小越好,那么让最小的值 $+1$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int p = 1, zero = 0, mi = INT_MAX; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; p *= (tmp == 0 ? 1 : tmp); zero += tmp == 0; mi = min(mi, tmp); } if (zero >= 2) cout << 0 << endl; else if (zero == 1) cout << p << endl; else cout << (p / mi * (mi + 1)) << endl; } } C. Target Practice 大致题意 有一个飞镖靶,根据结果计算总分 思路 简单题,根据当前的下标距离四个边最小值是多少即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str; str.reserve(10); int ans = 0; for (int i = 0; i < 10; ++i) { cin >> str; for (int j = 0; j < 10; ++j) { if (str[j] == '.') continue; int code = min(min(i + 1, 10 - i), min(j + 1, 10 - j)); ans += code; } } cout << ans << endl; } } D. 1D Eraser 大致题意 有一个串,其中有白色和黑色方块,每次可以选择连续 $k$ 个块让其变成白色,问最少几步可以全部变成白色 思路 简单题,从左往右考虑即可,毕竟最左边遇到的第一个黑色方块肯定需要消耗一次操作,为了最大化使用必定会让 $k$ 个的左边界是当前的黑色方块 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; string str; str.reserve(n); cin >> str; int last = -k - 1, ans = 0; for (int i = 0; i < n; ++i) { if (str[i] == 'W') continue; if (i - last + 1 <= k) continue; last = i; ans++; } cout << ans << endl; } } E. Building an Aquarium 大致题意 有一个线性水池,水池底部形状已知,最多可以使用 $x$ 个单位的水,问水池两边应该造多高才能尽可能容纳更多的水的同时,在水池满的时候不会使用超过给出的水 思路 简单题,二分答案即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, x; cin >> n >> x; vector<int> data(n); for (auto &item: data) cin >> item; int l = 0, r = 2e9 + 10LL; while (l + 1 < r) { int mid = (l + r) >> 1; int sum = 0; for (auto &item: data) sum += item >= mid ? 0 : mid - item; if (sum > x) r = mid; else l = mid; } cout << l << endl; } } F. Money Trees 大致题意 给出一个数组,其每个值都有两个属性:$a, h$,需要找到一个连续的子数组,使得这个连续的子数组 $[l, r]$的 $h$ 值满足 $\forall i \in [l, r], h_{i-1} \space mod \space h_i = 0$,同时 $\sum_{i=l}^{r} a_i \leq x$ 问最长的子数组的长度 思路 也是二分答案即可,毕竟在确定要找的最终串的长度的情况下,只需要 $O(n)$ 即可求出是否符合预期,注意平移区间的时候状态的转化 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, ans = 0; cin >> n >> k; vector<int> a(n), h(n); for (auto &item: a) cin >> item; for (auto &item: h) cin >> item; for (auto &item: a) if (item <= k) ans = 1; int l = 0, r = n + 1; while (l + 1 < r) { int mid = (l + r) >> 1; if (mid == 1) { if (ans >= 1) l = mid; else r = mid; continue; } int x = 0, y = 0, sum = 0; bool flag = false; auto findNext = [&]() { x = y; y += 1; sum = a[x]; while (y - x != mid && x < n && y < n) { if (h[y - 1] % h[y] != 0) { x = y; y += 1; sum = a[x]; } else { sum += a[y]; y++; } } if (y - x == mid && sum <= k) { ans = max(ans, mid); flag = true; } return y - x == mid; }; auto move = [&]() { if (y == n) return false; if (h[y - 1] % h[y] != 0) return false; sum -= a[x]; sum += a[y]; y++; x++; if (sum <= k) { ans = max(ans, mid); flag = true; } return true; }; while (findNext()) while (move()); if (flag) l = mid; else r = mid; } cout << ans << endl; } } G. ABBC or BACB 大致题意 有一个字符串,由 $A, B$ 两个字母组成,每次可以将 $AB$ 转为 $BC$,或者将 $BA$ 转为 $CB$,问最多可以操作几次 思路 很显然,如果是一串联系的 $A$,然后其中一侧有一个 $B$,那么这种情况下的答案就是 $A$ 的数量 那么将整个数组拆成所有连续的 $A$ 段,然后为每个 $A$ 段找合理的 $B$ 即可。比如如果整个字符串开头或者结尾是 $B$,那么必然可以为每个 $A$ 段找到一个 $B$。除开上面的情况,只需要看看 $B 的数量是否大于等于 $A$ 段的数量即可,因为如果等于或者超过也必然可以分割。如果还不行,那么只能舍弃价值最低的 $A$ 串了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void solve() { int _; cin >> _; string str; str.reserve(2e5 + 10); for (int ts = 0; ts < _; ++ts) { cin >> str; if (str.front() == 'B' || str.back() == 'B') { int cnt = 0; for (auto &item: str) cnt += item == 'A'; cout << cnt << endl; continue; } vector<int> part(1, 0); int cntB = 0; for (char &i: str) { cntB += i == 'B'; if (i == 'B') { if (part.back() != 0) part.push_back(0); } else part.back()++; } int tot = 0, mi = INT_MAX; for (auto &item: part) { tot += item; mi = min(mi, item); } if (cntB < part.size()) tot -= mi; cout << tot << endl; } } H. Mad City 大致题意 有一个图,$n$ 个点,$n$ 条边,有两个人分别从 $a, b$ 出发,其中前者希望追赶后者,而后者希望摆脱前者的追捕,问能否追上 思路 首先,$n$ 个点和 $n$ 条边,那就意味着必然图中必然存在环。而两人速度相同,如果同时都在环上,那肯定追不到。所以必须要在后者进入环之前抓到,也就是提前或者刚好到达环上的某一个点。所以只需要找出后者刚进入环的时间点和位置,看看前者能否在指定时间内达到即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, a, b; cin >> n >> a >> b; vector<int> deg(n + 1, 0); vector<bool> vis(n + 1, false); struct node { int v, n; }; vector<node> edge(n * 2); vector<int> head(n + 1, -1); for (int i = 0; i < n; ++i) { int u, v; cin >> u >> v; edge[i << 1] = {v, head[u]}; edge[(i << 1) | 1] = {u, head[v]}; head[u] = i << 1; head[v] = (i << 1) | 1; deg[u]++; deg[v]++; } // find circle queue<int> q; for (int i = 1; i <= n; ++i) if (deg[i] == 1) q.push(i); while (!q.empty()) { int cur = q.front(); q.pop(); vis[cur] = true; for (int i = head[cur]; i != -1; i = edge[i].n) if ((--deg[edge[i].v]) == 1 && !vis[edge[i].v]) q.push(edge[i].v); } // begin for b run away int cost1, cost2 = INT_MAX, target; queue<pair<int, int>> qs; vector<bool> flag(n + 1, false); qs.emplace(b, 0); flag[b] = true; while (!qs.empty()) { auto cur = qs.front(); qs.pop(); if (!vis[cur.first]) { cost1 = cur.second; target = cur.first; break; } for (int i = head[cur.first]; i != -1; i = edge[i].n) if (!flag[edge[i].v]) { qs.emplace(edge[i].v, cur.second + 1); flag[edge[i].v] = true; } } while (!qs.empty()) qs.pop(); for (int i = 0; i <= n; ++i) flag[i] = false; qs.emplace(a, 0); flag[a] = true; while (!qs.empty()) { auto cur = qs.front(); qs.pop(); if (cur.first == target) { cost2 = cur.second; break; } for (int i = head[cur.first]; i != -1; i = edge[i].n) if (!flag[edge[i].v]) { qs.emplace(edge[i].v, cur.second + 1); flag[edge[i].v] = true; } } cout << (cost1 < cost2 ? "YES" : "NO") << endl; } }

2023/9/24
articleCard.readMore

CodeTON Round 6 (Div. 2)

A. MEXanized Array 大致题意 构造一个数组,其长度为 $n$,最大值为 $x$,$MEX$ 为 $k$,问这个数组的所有值的和最大是多少 思路 简单题,在 $k > x + 1$ 或 $n < k$ 的场景下无解(不可能构造一个 $MEX$ 不可达的数组),然后就随便构造就行了,保证 $MEX$ 之后,剩下所有值取最大就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, x; cin >> n >> k >> x; if (k > x + 1 || n < k) { cout << -1 << endl; continue; } int sum = 0; for (int i = 0; i < k; ++i) sum += i; for (int i = k; i < n; ++i) sum += (x == k ? x - 1 : x); cout << sum << endl; } } B. Friendly Arrays 大致题意 给出两个数组 $a, b$,允许选择任意次的 $b$ 数组中的任意一个 $b_j$,然后让 $\forall i \in [1, len(a)], a_i = a_i | b_j$,问最终得到的数组 $a$ 中所有的异或和最大和最小的可能 思路 某个比特为是 $1$ 的情况下,在奇数个值异或和的结果则也是 $1$,而偶数个则为 $0$。而或运算可以让 $a$ 数组的每一个值的某些个位都变成 $1$。基于此,只需要关心 $a$ 的长度即可,若 $a$ 为奇数,那么选尽可能多的 $b$ 使得每个位都尽可能是 $1$,反之则尽可能不选,这样才能达到最大,同理可以得到最小的方案 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; int sa = 0, sb = 0, tmp; for (int i = 0; i < n; ++i) { cin >> tmp; sa ^= tmp; } for (int i = 0; i < m; ++i) { cin >> tmp; sb |= tmp; } cout << (n % 2 ? sa : sa & ~sb) << ' ' << (n % 2 ? sa | sb : sa) << endl; } } C. Colorful Table 大致题意 有一个数组 $a$,长度为 $n$,然后有一个对应的矩阵 $b$,为 $n \times n$,其每一个位置的值 $b_{i,j}=min(a_i, a_j)$ 问对于每个数字 $x$,在矩阵 $b$,中能够找到对应一个最小的矩形,此矩形包含了所有出现 $x$ 的位置,求出这个矩形的大小 思路 对于任意一个值,假定其第一次在 $a$ 中出现的位置为 $i$,它第一次出现在 $b$ 地点一定是 $b_{i,i}$,同时其最后一次在矩阵中的位置一定是 $b_{j,j}$,其中 $j$ 是在数组 $a$,中出现的,最后一个比当前值更大的下标 根据上面的规律,可以求出实际上每个值的位置,一定可以包裹比他大的那个值对应的矩阵,所以只需要根据值的大小排序一下他们在数组中第一次出现的位置,和最后一次出现的位置,然后从大到小遍历,保证小的值的区间能够覆盖到大的值的区间即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<bool> flag(k, false); vector<pair<int, int>> data(k, {-1, -1}); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; data[tmp - 1].first == -1 ? data[tmp - 1].first = data[tmp - 1].second = i : data[tmp - 1].second = i; flag[tmp - 1] = true; } for (int i = k - 2; i >= 0; --i) { data[i].first = !flag[i] ? data[i + 1].first : (data[i + 1].first != -1 ? min(data[i].first, data[i + 1].first) : data[i].first); data[i].second = !flag[i] ? data[i + 1].second : (data[i + 1].first != -1 ? max(data[i].second, data[i + 1].second) : data[i].second); } for (int i = 0; i < k; ++i) cout << (!flag[i] ? 0 : (data[i].second - data[i].first + 1) + (data[i].second - data[i].first + 1)) << ' '; cout << endl; } } D. Prefix Purchase 大致题意 有一个初始数组,每一个值都是 $0$,每次你可以选择花费 $c_i$ 元,使得这个数组前 $i$ 个元素加一,最多只能花费 $k$ 元,问能够得到最大字典序的数组是什么 思路 首先需把 $c$ 的值进行单调递增栈处理一下,毕竟价格相同或更低的同时 $i$ 更大肯定有优势 回到题目中的字典序,意味着只有越前面的值越大即可,所以要尽可能满足最前面的值最大,所以直接把 $k$ 丢给处理后的第一个值,看看最多第一个值可以到多少 处理完成第一个值后,那就意味着后面无论怎么贪心,第一个值一定要达到这个,否则肯定不如现在更好。另外,对于字典序而言,约前面的值价值越高,所以要尽可能让前面的值大,贪心一下即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n; vector<pair<int, int>> data; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; while (!data.empty() && data.back().first >= tmp) data.pop_back(); data.emplace_back(tmp, i); } data.emplace_back(INT_MAX, n - 1); cin >> k; vector<int> ans(data.size()); ans[0] = k / data.front().first; k %= data.front().first; for (int i = 1; i < data.size(); ++i) { int diff = data[i].first - data[i - 1].first; if (k < diff) { break; } ans[i] = min(ans[i - 1], k / diff); k -= diff * ans[i]; } int cur = 0; for (int i = 0; i < n; ++i) { while (data[cur].second < i) cur++; cout << ans[cur] << " \n"[i == n - 1]; } } }

2023/9/23
articleCard.readMore

Codeforces Round 897 (Div. 2)

A. green_gold_dog, array and permutation 大致题意 已知一个数组 $a$,长度为 $n$,需要给出一个 $n$ 的排列 $b$,使得得到的新数组 $c_i = a_i - b_i$ 中不同的值尽可能多,问数组 $b$ 的结果可能是 思路 简单来说就是要差值差异大,而且没有取 $abs$,所以可以直接排序一下,一个递增一个递减配对即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<pair<int, int>> data(n); for (auto &item: data) cin >> item.first; for (int i = 0; i < n; ++i) data[i].second = i; sort(data.begin(), data.end(), greater<>()); for (int i = 0; i < n; ++i) data[i].first = i + 1; sort(data.begin(), data.end(), [](const pair<int, int> &lhs, const pair<int, int> &rhs) { return lhs.second < rhs.second; }); for (int i = 0; i < n; ++i) cout << data[i].first << " \n"[i == n - 1]; } } B. XOR Palindromes 大致题意 已经存在一个二进制的字符串 $a$,长度为 $n$,若存在另外一个字符串 $b$,其长度也为 $n$,其中 1 的数量恰好为 $x$,使得 $a \oplus b$ 恰好是一个回文串。则称 $x$ 是一个好值,问在 $[0, n]$ 中哪些值是好值 思路 首先,在异或操作中,若其中一方为 1 已知,那么相当于对对方进行了翻转操作。 而要回文串,那么必然可以将初始串先按位分割,只看左右的其中一半,若这个位置本来就会回文(和后半对应的位置相同),那么需要寻找的串必定这两位要相同,否则必须不同,由此可以计算出至少要 1 的数量和最多能够用上的 1 的数量。 另外关注字符串本身是否是奇数长度的,这样意味着如果恰好多一个,就可以放到中间解决问题,如果不是的话,那么满足条件的值会间隔开 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; cin >> str; str.reserve(n); int diff = 0, same = 0; for (int i = 0; i < (n + 1) / 2; ++i) { diff += str[i] != str[n - 1 - i]; same += str[i] == str[n - 1 - i]; } for (int i = 0; i <= n; ++i) { if (i < diff || i > n - diff) cout << 0; else if (n % 2) cout << 1; else { if ((i - diff) % 2 == 0) cout << 1; else cout << 0; } } cout << endl; } } C. Salyg1n and the MEX Game 交互题(这场交互题真多) 大致题意 有一个初始的集合,里面有一些值,你可以每次往里面加一个不存在的值,然后机器会每次往里面删除一个存在的值,且每次删除的值一定比你加入的值小。如果无法删除值了(没有满足条件的值了)那么就结束,问如何使得 MEX 最大化 思路 实际上对于删除方,第一优先级的肯定是删除掉最小的值,因为只有这样能最有效的降低 MEX。 对于你而言,一旦试图添加 0 这种最低值的时候,游戏就会结束,此时 MEX 就取决于往后的第一个空档。可以发现一旦被删除掉的值是最小的,那么就再也不能救回比那个值更小的可能性了。而因为最终一次操作一定是加入的,所以只需要对方删除啥你就加入什么即可,保证不要被删掉小的值。而第一次加入,就可以选择当前的 MEX,来增加最终的 MEX 的结果 题非常简单易懂,但是解释起来又有些难 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; sort(data.begin(), data.end()); int mex = n; for (int i = 0; i < n; ++i) if (data[i] != i) { mex = i; break; } cout << mex << endl; cout.flush(); while (cin >> mex && mex != -1) { cout << mex << endl; cout.flush(); } } } D. Cyclic Operations 大致题意 有一个初始每一个值都是 0 的数组 $a$,长度为 $n$,希望变成目标数组 $b$,可以进行如下操作 构造任意一个长度恰好为 $k$ 的数组 $c$,$k$ 为给出的固定数字 $\forall i \in [1, k], a_{l_i} \rightarrow l_{(i \space mod \space k) + 1}$ 允许进行无数次操作,问是否有可能变成 $b$ 思路 首先仔细模拟一下这个看起来很恐怖的公式,发现其实就是 $a_{l_i} \rightarrow l_{i+1}$ 注意这里可以看成是循环数组,否则会越界 然后考虑一下特殊情况,就是 $k = 1$ 的情况,这个时候必须每个值的下标等于自己,否则肯定不行,这里就不过多解释了,模拟一下就行。 然后是其他情况下,模拟一下就会发现有点类似有向图一样,而且比较显而易见必然会产生环 所以只需要让有向图上的环的大小都恰好等于 $k$ 即可,对于分支链路,他们可以直接临时占用环的一部分,而后通过环本身将其覆盖即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); vector<int> circle(n, 0); vector<int> level(n, 0); for (auto &item: data) cin >> item; if (k == 1) { bool flag = true; for (int i = 0; i < n; ++i) if (data[i] != i + 1) flag = false; cout << (flag ? "YES" : "NO") << endl; continue; } bool flag = true; int cur = 1; for (int i = 0; i < n; ++i) { if (circle[i] != 0) continue; cur++; int len = 0, index = i; while (circle[index] == 0) { circle[index] = cur; level[index] = len++; index = data[index] - 1; } if (circle[index] != cur) { // other circle, no matter how len it is, always OK continue; } if (len - level[index] != k) { flag = false; break; } } cout << (flag ? "YES" : "NO") << endl; } } E2. Salyg1n and Array (hard version) easy version 要求更低,所以直接做 hard 大致题意 有一个未知的数组,长度为 $n$,需要求出整个数组的异或和是多少 每次可以询问一个区间的异或和,这个区间长度一定为给定的 $k$,询问后,整个区间将会翻转 最多只能请求 57 次 思路 很显然,可以知道至多 $50$ 就可以保证覆盖到所有的区间,如果恰好 $k$ 是 $n$ 的因子,那么就可以恰好满覆盖,循环一遍直接可以得到答案了 如果不满足的情况,就需要考虑一种方案了,这里给出我的一个方案,接下来会参考下面的图进行讲解,图中,$a, b$ 两段之和(蓝色部分)是不满足完美分配后,余下的部分,其他的黑色部分则是可以完美分配的部分,其中 $len(a) = len(b) = len(d) = len(e)$(注意题目中描述了 $n, k$ 必定都是偶数,所以肯定可以这样分配),同时 $len(a) + len(b) + len(c) + len(d) = k$,且 同时 $len(b) + len(c) + len(d) + len(e) = k$ 先要求算出 $a,b,c,d$ 这个区间的异或和,假设记为 $x$,如此操作后,必定迎来翻转操作,即区间变成 $d, (c+b), a, e$ 的顺序,其中因为 $c$ 区间长度不确定,故和 $b$ 放在一块,不做区分。 然后再计算 $(c+b), a, e$ 的区间异或和,得到 $y$,同时翻转后得到 $d, e | (a+b+c)$,此处同理,此时无法完美区分 (a+b+c) 的区间长度到底是多少,只是大概知道个顺序罢了,因为我们也不关心顺序,故合并起来写,注意中间的其,其表示原来图中的蓝色和黑色部分的分割线。第一次翻转后,这根线无法进行绘制故没有标出,此时可以标出了 接着计算 $x \oplus y = (a \oplus b \oplus c \oplus d) \oplus (c \oplus b \oplus a \oplus e) = d \oplus e$,这不是正好是翻转后的外面部分的,那么问题好像就解决了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, ans = 0, tmp; cin >> n >> k; if (n % k != 0) { int len = n % k; cout << "? 1" << endl; cout.flush(); cin >> tmp; ans ^= tmp; cout << "? " << (len / 2 + 1) << endl; cout.flush(); cin >> tmp; ans ^= tmp; } for (int i = n % k; i < n; i += k) { cout << "? " << i + 1 << endl; cout.flush(); cin >> tmp; ans ^= tmp; } cout << "! " << ans << endl; } }

2023/9/16
articleCard.readMore

Codeforces Round 896 (Div. 2)

A. Make It Zero 大致题意 有一个数组,允许你每次选择一个区间,然后将这个区间内的所有值变成他们异或和的结果,问给出一种最多只进行 8 次的操作的可能方法使得整个数组变成 0 思路 不要求最小,只要能就行,简单了很多很多 首先,偶数个相同的值进行异或和,结果为 0,如果整个数组长度为偶数,那么直接异或和两次即可 如果为奇数,那么先把前 $n - 1$ 个异或和一下,最后再异或和两次最后两个值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, tmp; cin >> n; for (int i = 0; i < n; ++i) cin >> tmp; if (n % 2) { cout << 4 << endl; cout << 1 << ' ' << n << endl; cout << 1 << ' ' << n - 1 << endl; cout << n - 1 << ' ' << n << endl; cout << n - 1 << ' ' << n << endl; } else { cout << 2 << endl; cout << 1 << ' ' << n << endl; cout << 1 << ' ' << n << endl; } } } B. 2D Traveling 大致题意 有一个棋盘,开始在 $a$ 点,要前往 $b$ 点,只能中途停留在固定的 $n$ 个节点中任意一个 任意两个节点之间的成本是他们的棋盘距离,但是有 $k$ 个节点,他们之间相互的成本是 $0$ 问最少成本是多少 思路 棋盘距离就意味着,其实中间停留是毫无意义的,直接到终点就行了,没必要停留 但是有一些特殊节点,所以只需要找到距离 $a$ 最近的特殊节点,并计算成本,和距离 $b$ 的特殊节点,并计算成本,然后将两个成本相加和直接前往的成本差异,求较小值就行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k, a, b; cin >> n >> k >> a >> b; vector<pair<int, int>> data(n); for (auto &item: data) cin >> item.first >> item.second; auto dist = [&](int l, int r) { return abs(data[l].first - data[r].first) + abs(data[l].second - data[r].second); }; int ans = dist(a - 1, b - 1); int ak = INT_MAX * 2LL, bk = INT_MAX * 2LL; for (int i = 0; i < k; ++i) { ak = min(ak, dist(i, a - 1)); bk = min(bk, dist(i, b - 1)); } cout << min(ans, ak + bk) << endl; } } C. Fill in the Matrix 大致题意 有一个 $n \times m$ 的矩阵,将其每一行填充为 $m$ 的一个排列,求出每一列的 MEX,然后将每一列的 MEX 再求一次 MEX,问最终结果最大是多少,并给出矩阵 思路 构造题,比较简单,如果想看我的构造方案就运行一下打印出来看看吧 需要注意的是,因为矩阵比较大,不能存下来,所以需要提前算出答案输出了,不能等构造完成了再去算 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; if (m == 1) { cout << 0 << endl; } else if (n >= m - 1) { cout << m << endl; } else { cout << n + 1 << endl; } for (int i = 0; i < min(n, m - 1); ++i) { for (int j = 0; j < m; ++j) { cout << (j - i - 1 + m) % m << ' '; } cout << endl; } for (int i = min(n, m - 1); i < n; ++i) { for (int j = 0; j < m; ++j) { cout << (j + 1) % m << ' '; } cout << endl; } } } D1. Candy Party (Easy Version) 大致题意 有 $n$ 个人,每个人手上都有一些糖果,现在每个人都必需要给另外一个人 $2^x$ 个糖果,$x$ 可以是任意自然数,且每个人必须从另外一个人那里拿到他给出的糖果,问是否存在一种可能,经过这样一次操作后,所有人糖果数量相同 思路 首先,糖果没有新增或者丢弃,那么必然总糖果数量不变,所以很容易计算出最终每个人应该是多少糖果。那么就可以算出差值(应该最终增加/减少多少) 其次思考一个问题,由于给出/收到的糖果数量满足 $2^x$ 的形式,那么必然差值定是两个 $2^x$ 之差。例如 $3 = 4 - 1$,$7 = 8 - 1$,$4 = 8 - 4$ 等等就是合法的值,而例如 $5$ 就是一个非法的值。所以这样就可以先排除掉一部分了 很显然,每个值都只有一种可能的拆法,又因为每个人只能从一个人那里拿到糖果,那么必然所有的给出和拿到的可能性只有这些,他们必然完全相等 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #define int long long void solve() { vector<int> mi(40); for (int i = 0; i < 40; ++i) mi[i] = 1LL << i; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; int sum = 0; for (auto &item: data) sum += item; if (sum % n != 0) { cout << "No" << endl; continue; } sum /= n; auto lowBit = [](int x) { return x & -x; }; vector<int> p[2]; p[0].reserve(n); p[1].reserve(n); bool flag = true; for (int i = 0; i < n; ++i) { int diff = data[i] - sum; if (diff == 0) continue; int l = diff > 0 ? 0 : 1, r = 1 ^ l; diff = abs(diff); int b = *upper_bound(mi.begin(), mi.end(), diff); int s = b - diff; if (*lower_bound(mi.begin(), mi.end(), s) != s) { flag = false; break; } p[l].push_back(b); p[r].push_back(s); } sort(p[0].begin(), p[0].end()); sort(p[1].begin(), p[1].end()); cout << (flag && p[0] == p[1] ? "YES" : "NO") << endl; } } D2. Candy Party (Hard Version) 大致题意 和上一题类似,只不过从必须给一个人改成了至多给一个人,从一个人那拿到变成了至多从一个那里拿到。即可以不再一定要给出/收到了 思路 那么唯一的区别就在于那些差值本来就符合 $2^x$ 的人,因为他们既可以选择同时有给出收到,也可以选择只给出/收到。而那些不满足的则必定有给出和收到阶段 而那些本来就符合 $2^x$ 的值,如果它又同时选择有给出收到,那么称其为拆开后的,而拆开后必然得到一个更大的值。 那么可以按照下面的步骤进行模拟 将所有值分成两类:已经拆过了的,没有拆过的 对于每个差值,如果它满足 $2^x$,那么放入到没有拆过的组里,而不满足的,则将拆开后的两个 $2^x$ 值放入已经拆过的组里 判断一下,已经拆过的里面 $abs$ 最大的值和没有拆过的里面 $abs$ 最大的值,哪个大,如果后者大,那么就把那些大的值放入已经拆过的队列(因为他们再拆开就会创造更大的值,没有必要再拆了) 每次取出 $abs$ 最大的拆过的值,然后尝试在已经拆过里面为它找配对上的,即 $abs$ 相同,但是正负号相反的值,并将其消除 如果找不到,那么就再去没有拆过里面找相同的条件的,并消除 如果还找不到,那么再去没有拆过里面找符号相同但是值恰好为 $abs$ 的一半的,将其拆开,将 $abs$ 较大的删除,较小的放入已经拆开的队列中 回到 3 步,除非两个队列都空了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 #define int long long void solve() { vector<int> mi(40); for (int i = 0; i < 40; ++i) mi[i] = 1LL << i; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; int sum = 0; for (auto &item: data) sum += item; if (sum % n != 0) { cout << "No" << endl; continue; } sum /= n; struct cmp { bool operator()(const int &lhs, const int &rhs) const { return abs(lhs) < abs(rhs); } }; priority_queue<int, vector<int>, cmp> depart; map<int, int, greater<>> pos, neg; bool flag = true; for (int i = 0; i < n; ++i) { int dif = data[i] - sum; if (dif == 0) continue; if (abs(dif) == *lower_bound(mi.begin(), mi.end(), abs(dif))) { // good gay (dif > 0 ? pos : neg)[abs(dif)]++; } else { // bad gay int b = *upper_bound(mi.begin(), mi.end(), abs(dif)); b *= dif > 0 ? 1 : -1; int s = dif - b; if (abs(s) != *lower_bound(mi.begin(), mi.end(), abs(s))) { flag = false; break; } depart.push(b); depart.push(s); } } if (!flag) { cout << "NO" << endl; continue; } while (!depart.empty() || !pos.empty() || !neg.empty()) { if (depart.empty()) { auto posIter = pos.begin(), negIter = neg.begin(); if (posIter->first == negIter->first) { int tmp = min(posIter->second, negIter->second); posIter->second -= tmp; negIter->second -= tmp; if (posIter->second == 0) pos.erase(posIter); if (negIter->second == 0) neg.erase(negIter); continue; } auto &iter = posIter->first > negIter->first ? posIter : negIter; auto mx = posIter->first > negIter->first ? 1 : -1; auto &u = posIter->first > negIter->first ? pos : neg; auto &v = posIter->first > negIter->first ? neg : pos; auto tIter = v.find(iter->first / 2); if (tIter == v.end()) { flag = false; break; } int tmp = min(iter->second, tIter->second); for (int i = 0; i < tmp; ++i) depart.push(mx * iter->first / 2); iter->second -= tmp; tIter->second -= tmp; if (iter->second == 0) u.erase(iter); if (tIter->second == 0) v.erase(tIter); } while (!pos.empty() || !neg.empty()) { auto posIter = pos.begin(); auto negIter = neg.begin(); bool posWin = negIter == neg.end() || (posIter != pos.end() && posIter->first > negIter->first); auto &maxIter = posWin ? posIter : negIter; auto &maxLink = posWin ? pos : neg; auto mx = posWin ? 1 : -1; if (maxIter->first > abs(depart.top())) { for (int i = 0; i < maxIter->second; ++i) depart.push(mx * maxIter->first); maxLink.erase(maxIter); } else { break; } } int cnt[2] = {depart.top() < 0, depart.top() > 0}; int cur = abs(depart.top()); depart.pop(); while (!depart.empty() && abs(depart.top()) == cur) { cnt[0] += depart.top() < 0; cnt[1] += depart.top() > 0; depart.pop(); } // receives from not good gay int tmp = min(cnt[0], cnt[1]); cnt[0] -= tmp; cnt[1] -= tmp; if (cnt[0] == 0 && cnt[1] == 0) continue; int left = cnt[0] > 0 ? 0 : 1; auto &link = cnt[0] > 0 ? pos : neg; int mx = cnt[0] > 0 ? -1 : 1; // find in pos which is equals to this gay auto iter = link.find(cur); if (iter != link.end()) { tmp = min(cnt[left], iter->second); iter->second -= tmp; cnt[left] -= tmp; if (iter->second == 0) link.erase(iter); } if (cnt[left] == 0) continue; // not enough, find in pos which is half of this gay iter = link.find(cur / 2); if (iter != link.end()) { tmp = min(cnt[left], iter->second); for (int i = 0; i < tmp; ++i) depart.push(mx * cur / 2); iter->second -= tmp; cnt[left] -= tmp; if (iter->second == 0) link.erase(iter); } if (cnt[left] != 0) { flag = false; break; } } cout << (flag ? "YES" : "NO") << endl; } }

2023/9/16
articleCard.readMore

Codeforces Round 887 (Div. 2)

A. Desorting 大致题意 有一个非递减数列,每次可以选择一个下标 $i$,使得 $\forall i \in [1, i], a_i \rightarrow a_i + 1$,同时 $\forall i \in [i + 1, n], a_i \rightarrow a_i - 1$ 问最少需要几次操作 思路 简单题,找到差一点最小的,和 $2$ 做向上取整的除法就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, last, diff = INT_MAX; cin >> n; cin >> last; for (int i = 0; i < n - 1; ++i) { int tmp; cin >> tmp; diff = min(diff, tmp - last); last = tmp; } cout << max(0, (diff + 2) / 2) << endl; } } B. Fibonaccharsis 大致题意 有一个 $k$ 的长度的数组,已知 $a_k = n$,问这个数组满足斐波那契数列的种数有多少 思路 在长度较长的情况下,$k \le n$ 是显然的,那么就可以排除掉一些 然后暴力计算出,假定初项 $x, y$,计算 $n = ax + by$ 中的 $a, b$,然后在暴力遍历所有可能即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; if (k >= n && k > 10) { cout << 0 << endl; continue; } int x[3] = {0, 1, 0}, y[3] = {0, 0, 1}; for (int i = 2; i < k; ++i) { x[0] = x[1]; x[1] = x[2]; y[0] = y[1]; y[1] = y[2]; x[2] = x[0] + x[1]; y[2] = y[0] + y[1]; } int tx = x[2], ty = y[2], ans = 0; for (int i = 0; i < n; ++i) { if (tx * i > n) break; if ((n - tx * i) % ty == 0 && (n - tx * i) / ty >= i) ans++; } cout << ans << endl; } } C. Ntarsis’ Set 大致题意 有一个无限长的数组,然后每次报数然后去掉其中一部分,去掉的报数下标为给出的 $a$ 数组,总共进行 $k$ 轮,问最终剩下的第一个值是原来下标多少的 思路 模拟一下,假设 $a = [1, 5, 10]$,且 $k$ 无限大,那么可以得到 下标1234567891011121314151617181920 被去掉的报数111151515101510151015101 规律就是当只有一个值的时候,都是 1 的循环,然后两个值的时候变成两个值的循环,依次类推。 因为每次 1 就意味着一个新的开始,所以当 1 不能再覆盖的地方就是答案 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; int j = 0, x = 1; while (k--) { while (j < n && a[j] <= x + j) j++; x += j; } cout << x << endl; } } D. Imbalanced Arrays 大致题意 给出一个数组 $a$,长度为 $n$,构造一个数组 $b$,其长度为 $n$,使得满足如下条件 $\forall i \in [1, n], -n \leq b_i \leq n$ $\forall i \in [1, n], j \in [1, n], b_i + b_j \neq 0$ 对于 $i$,计算 $\forall j \in [1, n]$,满足 $b_i + b_j > 0$ 的数量恰好为 $a_i$ 思路 我也不知道咋过的,只是冥冥之中写了这个构造方案,过了,但是实在没能证明出来我是怎么对的 为了满足第一条和第二条,定下一个简答的原则:$b$ 数组的每一项取 $abs$ 后,得到的新数组恰好是 $n$ 的排列,这样可以直接满足前两项了 而后根据 $a$ 的大小,从大到小排序后遍历,如果当前值和上一个值相同,那么就取上一个值 $-1$,否则再减去它们两个值的差值(因为中间这些跳过的值肯定是负数,这样恰好可以满足对应的整数部分的要求),直到试图给当前值赋值为非正整数时,停止即可 然后再根据 $a$ 的大小,从小到大排序后遍历,取出剩下没有给的值中最大的,将其变成负值后就是对应的值,同时验证一下是否准确,若不准确就是无解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; vector<int> ans(n); vector<int> usePos; set<int> notUse; for (int i = 1; i <= n; ++i) notUse.insert(i); vector<pair<int, int>> sorted(n); for (int i = 0; i < n; ++i) sorted[i] = {data[i], i}; sort(sorted.begin(), sorted.end(), greater<>()); int cur = n, last = n; for (auto &item: sorted) { cur -= last - item.first; if (cur <= 0) break; ans[item.second] = cur; notUse.erase(cur); usePos.push_back(cur); --cur; last = item.first; } sort(usePos.begin(), usePos.end(), greater<>()); sort(sorted.begin(), sorted.end()); int i = 0; bool flag = true; for (auto iter = notUse.rbegin(); iter != notUse.rend(); ++iter) { int cnt = int(upper_bound(usePos.begin(), usePos.end(), *iter, greater<>()) - usePos.begin()); if (cnt != sorted[i].first) { flag = false; break; } ans[sorted[i].second] = -*iter; i++; } if (!flag) { cout << "NO" << endl; continue; } cout << "YES" << endl; for (auto &item : ans) cout << item << ' '; cout << endl; } }

2023/9/9
articleCard.readMore

Codeforces Round 895 (Div. 3)

A. Two Vessels 大致题意 有 A,B 两个水池,用大小为 $c$ 的勺子舀,最少需要几次才能让这两个水池相同 思路 简单题,每次变化 $2c$,不要求平均数,不然精度不好算 AC code 1 2 3 4 5 6 7 8 9 10 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int a, b, c; cin >> a >> b >> c; int diff = abs(a - b); cout << (diff + 2 * c - 1) / (2 * c) << endl; } } B. The Corridor or There and Back Again 大致题意 有一条射线,射线上有一些点有夹子,夹子会在人经过后 $x$ 秒触发,问从顶点出发,然后折返,问最远可以到哪里 思路 也是简单题,每个夹子就意味着单独的最远可达距离,然后取最小就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int ans = INT_MAX; for (int i = 0; i < n; ++i) { int a, b; cin >> a >> b; ans = min(ans, a + (b - 1) / 2); } cout << ans << endl; } } C. Non-coprime Split 大致题意 给出 $[l, r]$ 这个区间,目的找到两个值 $a, b$,满足 $a + b \in [l, r]$ $gcd(a, b) \neq 1$ 思路 第一反应就是偶数,毕竟任意两个偶数很容易达到,而且可以足够小,只要 $[l, r]$ 中存在偶数区间即可。当然,同时 $r \geq 4$,否则肯定没戏 如果还不行怎么办,那此时必然满足 $l = r, r \space mod \space 2 == 1$。这个时候,反正也只有一个数可以选了,强行找因子吧,找不到就算失败 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int l, r; cin >> l >> r; if (r < 4) { cout << -1 << endl; continue; } if (l != r || r % 2 == 0) { cout << 2 << ' ' << ((r >> 1) << 1) - 2 << endl; continue; } int mid = (int) sqrt(r) + 10; int gcd = -1; for (int i = 2; i <= mid; ++i) { if (r - i <= 0) break; if (r % i == 0) { gcd = i; break; } } if (gcd == -1) { cout << -1 << endl; } else { cout << gcd << ' ' << r - gcd << endl; } } } D. Plus Minus Permutation 大致题意 给定 $n, x, y$,你需要找到一个 $n$ 的排列,满足 $$((p_{1x}+p_{2x}+p_{3x}+ \dots + p_{\left \lfloor \frac{n}{x} \right \rfloor x}) - (p_{1y}+p_{2y}+p_{3y}+ \dots + p_{\left \lfloor \frac{n}{y} \right \rfloor y})$$ 尽可能大,问最终结果是 思路 计算出 $x$ 单独占了几个,$y$ 单独占了几个,然后把大数给 $x$,小数给 $y$ 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, x, y; cin >> n >> x >> y; int g = gcd(x, y), l = (x * y) / g; int cntL = n / l, cntX = n / x - cntL, cntY = n / y - cntL; int sumX = (n + (n - cntX + 1)) * cntX / 2, sumY = (1 + cntY) * cntY / 2; cout << sumX - sumY << endl; } } E. Data Structures Fan 大致题意 这题实在不想写题意了,已经把线段树这三个字拍脸上了 思路 维护两个值即可,为 $0$ 的异或和,为 $1$ 的异或和,然后每次改就是交换这两个值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 const int N = 1e5 + 10; struct SegTree { int cnt[2][N << 1]; bool lazy[N << 1]; inline int static get(int l, int r) { return (l + r) | (l != r); } inline void up(int l, int r) { int mid = (l + r) >> 1; cnt[0][get(l, r)] = cnt[0][get(l, mid)] ^ cnt[0][get(mid + 1, r)]; cnt[1][get(l, r)] = cnt[1][get(l, mid)] ^ cnt[1][get(mid + 1, r)]; } void build(int l, int r) { // NOLINT(*-no-recursion) lazy[get(l, r)] = false; if (l == r) { return; } int mid = (l + r) >> 1; build(l, mid); build(mid + 1, r); up(l, r); } inline void push(int l, int r) { int k = get(l, r); if (lazy[k]) { int mid = (l + r) >> 1; int left = get(l, mid), right = get(mid + 1, r); swap(cnt[0][left], cnt[1][left]); swap(cnt[0][right], cnt[1][right]); lazy[left] = !lazy[left]; lazy[right] = !lazy[right]; lazy[k] = false; } } void update(int l, int r, int x, int y) { // NOLINT(*-no-recursion) if (l == x && y == r) { swap(cnt[0][get(l, r)], cnt[1][get(l, r)]); lazy[get(l, r)] = !lazy[get(l, r)]; return; } push(l, r); int mid = (l + r) >> 1; if (y <= mid) { update(l, mid, x, y); } else if (x > mid) { update(mid + 1, r, x, y); } else { update(l, mid, x, mid); update(mid + 1, r, mid + 1, y); } up(l, r); } int query(int l, int r, int x, int y, int p) { // NOLINT(*-no-recursion) if (l == x && y == r) { return cnt[p][get(l, r)]; } push(l, r); int mid = (l + r) >> 1; if (y <= mid) { return query(l, mid, x, y, p); } else if (x > mid) { return query(mid + 1, r, x, y, p); } else { return query(l, mid, x, mid, p) ^ query(mid + 1, r, mid + 1, y, p); } } } seg; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; for (int i = 1; i <= n; ++i) { cin >> seg.cnt[0][SegTree::get(i, i)]; seg.cnt[1][SegTree::get(i, i)] = 0; } string str; str.reserve(n); cin >> str; for (int i = 0; i < n; ++i) if (str[i] == '1') swap(seg.cnt[0][SegTree::get(i + 1, i + 1)], seg.cnt[1][SegTree::get(i + 1, i + 1)]); seg.build(1, n); int q; cin >> q; for (int i = 0; i < q; ++i) { int o; cin >> o; if (o == 1) { int l, r; cin >> l >> r; seg.update(1, n, l, r); } else { int x; cin >> x; cout << seg.query(1, n, 1, n, x) << ' '; } } cout << endl; } } F. Selling a Menagerie 大致题意 你决定逐个出售动物园里的动物,每个动物都有其价格,出售可以获得对应价格。 每个动物都有其唯一害怕的动物,如果你出售的时候,其害怕的动物还没有被出售,那么你可以获得双倍的价格奖励 给出一个出售顺序,使得最终价值最高 思路 很明显是一个 dag 的拓扑排序问题。可惜的是,这个并不是 dag,是有环的。而每次解开一个环,就要消耗一定代价。 可以直接将原来拓扑用的入度改成代价,即所有指向(害怕)这个节点的价格之和,因为如果这个节点先被拓扑了,那么这些指向它的节点就拿不到双倍的价值了,即损失了他们价格之和的代价 然后搞个优先队列拓扑就好了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { struct node { int v, n; }; int n; cin >> n; vector<int> cost(n); vector<int> link(n); vector<int> deg(n, 0); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; link[i] = tmp - 1; } for (auto &item: cost) cin >> item; for (int i = 0; i < n; ++i) deg[link[i]] += cost[i]; vector<bool> visit(n, false); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> prq; for (int i = 0; i < n; ++i) prq.emplace(deg[i], i); while (!prq.empty()) { auto cur = prq.top(); prq.pop(); if (visit[cur.second]) continue; visit[cur.second] = true; cout << cur.second + 1 << ' '; int nxt = link[cur.second]; if (visit[nxt]) continue; deg[nxt] -= cost[cur.second]; prq.emplace(deg[nxt], nxt); } cout << endl; } } G. Replace With Product 大致题意 给你一个数组,允许你选择其中一段,将其换成这些值的乘积,问数组最大的和是多少。需要给出选择的那一段 思路 由于乘法的增长通常都会远大于求和,我们需要寻找一个临界点,使得 $\prod_{i=1}^n a_i \geq \sum_{i=1}^n a_i$,这样就可以无脑乘起来了 假定数组中只有两个值是 $> 1$ 的,为 $x, y$,那么可以得到 $$\begin{align*}xy & \geq n+x+y-2 \\ & \geq n+2\sqrt{xy}-2 \\let \space t & \rightarrow \sqrt{xy} \\t^2-2t+1 & \geq n - 1 \\t-1 & \geq \sqrt{n-1} \\t & \geq \sqrt{n-1}+1 \\xy & \geq n-1+2\sqrt{n-1}+1 \\ & \geq n+2\sqrt{n-1} \\ & \geq n+n-1+1 \\ & \geq 2n\end{align*}$$ 所以只要对于 $xy > 2n$ 的情况,则可以无脑选尽可能全部的即可,因为相加一定不如相乘,当然,是尽可能,不是一定全部 那对于那些不满足的,可以得到 $\prod_{i=1}^n a_i < 2n$,这是一个很难达到的,假定有 $x$ 个数值不为 $1$ 的值,那么必然平均值为 $\sqrt[x]{2n}$,当 $x = 100$ 的时候,平均值就一定 $< 2$ 了,就意味着有 $1$ 的存在,那更不可能乘起来达到目标值了,所以此时非 $1$ 的值数量极少,可以暴力求解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; int tot = 1; for (auto &item : data) { tot *= item; if (tot > 2 * n) break; } if (tot > 2 * n) { // make 1 less int l = 0, r = n - 1; while (data[l] == 1) l++; while (data[r] == 1) r--; cout << l + 1 << ' ' << r + 1 << endl; continue; } vector<int> not1; for (int i = 0; i < n; ++i) if (data[i] != 1) not1.push_back(i); if (not1.empty()) { cout << 1 << ' ' << 1 << endl; continue; } int mx = 0, l = 0, r = 0; for (int i = 0; i < not1.size() - 1; ++i) { int curP = data[not1[i]], curS = data[not1[i]] - 1; for (int j = i + 1; j < not1.size(); ++j) { curP *= data[not1[j]]; curS += data[not1[j]] - 1; int realS = curS + not1[j] - not1[i] + 1; if (mx < curP - realS) { mx = curP - realS; l = not1[i]; r = not1[j]; } } } cout << l + 1 << ' ' << r + 1 << endl; } }

2023/9/9
articleCard.readMore

左值-右值-将亡值

最初概念 如何确定一个值是左值还是右值? 通常有一个比较简单的判断方案:有地址的值被称为左值,没有地址的值称为右值 但是事实好像并非如此,特别是写了一些相关代码的时候,比如下面的这段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int f(int &a) { return 1; } int f(int &&a) { return 2; } void solve() { int a = 1; int &b = a; int &&c = 1; cout << f(1) << endl; // 2 cout << f(a) << endl; // 1 cout << f(b) << endl; // 1 cout << f(c) << endl; // 1 } 对应的输出结果也写在每一行的后面了,这似乎有一些意料之外的情况 第一行,一个单独的数字 1,很明显的确实是一个右值,符合预期 第二行,变量 a 明显也是一个合情合理的左值,那么也是符合预期的 接下来第三行,变量 b 作为 a 的一个引用,那毫无意义也是一个左值(b 只是引用了 a 的值,实际上仍然是 a 本身),符合预期 但是第四行,却让人摸不着头脑,明明 c 是一个明确的右值引用,为什么也是一个 1 这似乎表明了,c 是一个合法的左值,而非右值 尝试做一些看起来非法的操作 1 2 3 int &&c = 1; c += 10; cout << c << endl; // 11 看起来非常的合法合理,就像是一个活灵活现的左值,而并非它类型那样描述的右值。即然是左值,那么必然有地址,输出看看 1 2 3 cout << &a << endl; // 0x7fff1ba1f724 cout << &b << endl; // 0x7fff1ba1f724 cout << &c << endl; // 0x7fff1ba1f72c 从上面的数字可以看出来,c 确实是在栈上,即拥有一个合理合法的地址,这是发生了什么? 调查 如果把上述的代码改成汇编语言后,再看看结果 汇编前 1 2 3 4 5 6 int main() { int a = 1; int &b = a; int &&c = 1; c += 10; } 汇编结果(仅摘录核心段) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .cfi_startproc pushq%rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq%rsp, %rbp .cfi_def_cfa_register 6 subq$32, %rsp ; 以上均为函数定义需要的一些基本操作,例如记录栈位置等,忽略 movq%fs:40, %rax ; 设置 canary 值,用于检测 stack overflow 现象 movq%rax, -8(%rbp) ; 将 canary 值保存到栈的前 8 个字节中 xorl%eax, %eax ; 任何值 xor 自己必定为 0,此处相当于清理 eax 寄存器 movl$1, -32(%rbp) ; 将 1 存储到 28-32 这几个字节中(int 占用 4 个字节)【变量 a】 leaq-32(%rbp), %rax ; 将【a】的地址拷贝到 rax movq%rax, -24(%rbp) ; 将【a】的地址保存到 16-24 这几个字节中(64bit 上占用 8 个字节)【变量 b】 movl$1, %eax ; 将值 1 写入 eax movl%eax, -28(%rbp) ; 将 eax 的值写入 24-28 这几个字节中【未知变量】 leaq-28(%rbp), %rax ; 将【未知变量】的地址拷贝到 rax movq%rax, -16(%rbp) ; 将【未知变量】的地址写入到 8-16 这几个字节中【变量 c】 movq-16(%rbp), %rax ; 再读取【变量 c】的到 rax movl(%rax), %eax ; 将【变量 c】认为是一个地址,取出此地址中的值并写入到 eax 中 leal10(%rax), %edx ; edx = rax + 10 movq-16(%rbp), %rax ; 将【变量 c】的值拷贝到 rax 中 movl%edx, (%rax) ; 将 edx 的结果保存到 rax 对应的值的地址中(即写入【变量 c】作为地址所在的位置) movl$0, %eax ; 清空 eax movq-8(%rbp), %rax ; 取出 canary 值 xorq%fs:40, %rax je.L3 call__stack_chk_fail@PLT 可以注意到,对于引用而言,汇编仍然使用的是指针来解决,所以可以看到变量 b 记录下的是 a 的指针,而非真正的给 a 做了一个别名。而 c 也是一个指针,指向了一个未知的变量。这似乎就是我们寻找的答案 从内存本身而言,任何值都可以认为是左值,因为一个值存在,则必定存在具体的地址,即使它是作为常量的方式写在代码中,那起码它也应该存在于代码段,“存在即有地址” 但是对于这种在代码段“有地址”的值,又违背了代码段不可修改的原则,而具体操作的时候又未免会使用到这些值,这个时候,编译器会将代码段的这个值拷贝到栈空间,然后将其再赋给具体的对象,这个拷贝过来的值,像是一个右值,同时又具有着左值的特点,更确切的说,它属于“将亡值(xvalue)”。 将亡值 其中,lvalue 和 rvalue 就是我们一般认为上的左值和右值,而 glvalue 则是包含了将亡值的泛左值,而 prvalue 则是指那些纯右值,也就是那些在代码段里的值 将亡值则表示一种中间变量,例如使用了纯右值的时候,或者隐式类型转化,或者函数的返回值,这些都是将亡值充当的角色。实际上他们都有确切的栈上地址。 但是将亡值本身的含义是一个临时存在的变量,终是不可久留,这也就意味着编译器通常会限制对将亡值进行左值引用的方式。例如 1 double &x = (double)1; 此时编译器的报错是:Non-const lvalue reference to type 'double' cannot bind to a temporary of type 'double',即无法通过一个非常量的左值引用指向一个将亡值。而当你改成 const double &x = (double)1; 后,程序又可以通过编译了。这也说明了编译器实际上只是在做一些安全性的检查,并没有真正限制修改将亡值,甚至可以将将亡值变成长期存在的栈上的值(例如一开始的程序)

2023/9/3
articleCard.readMore

Educational Codeforces Round#154 (Div. 2)

A. Prime Deletion 大致题意 给一个 9 位数的数字,其中 $1-9$ 恰好各出现一次,允许删除一些位置,并保持原来的顺序不变,然后最终结果需要是一个素数。给出一个可能的素数,要求至少两位数 思路 看起来很难的问题,实际上很容易解决。因为至少两位数,且每个数字都有,那么我只要找到几个万能的解不就行了 我选择了 $13$ 和 $31$,只需要观察原数组中 $1, 3$ 的相对位置,选择其中一个输出即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str; str.reserve(9); cin >> str; int pos1 = 0, pos3 = 0; for (int i = 0; i < str.size(); ++i) { if (str[i] == '1') pos1 = i; if (str[i] == '3') pos3 = i; } if (pos1 < pos3) cout << "13" << endl; else cout << "31" << endl; } } B. Two Binary Strings 大致题意 给你两个 01 字符串 $s$,长度为 $n$,允许你对任何一个字符串进行无数次如下操作,问是否可能这两个字符串相同 选择 $l, r$ 满足,$1 \leq l < r \leq n$ 且 $s_l = s_r$ 使得 $\forall i \in [l, r], s_i \rightarrow s_l$ 思路 看似很头痛的问题,实际上很简单 如果一个字符串首尾是相同的,那么直接全选,就可以让这个字符串变成完全相同的字符串了,那就不用那么操心了。而反过来可以发现,字符串的首尾是一定不会变化的,因为它必定是被选择的 $l, r$,所以这两个字符串的首尾必须相互映射。如果同时顺便首尾相同了,那也不用思考更多了 接下来考虑首尾不同的情况,也就是一定同时存在 $0, 1$ 那么必定存在一个位置,出现 $01$ 或者 $10$ 相邻的情况,为了方便起见,也为了避免出现混乱,这里假定原字符串开头为 $0$,结束为 $1$,那么我们去找 $01$ 即可,因为必定存在。反之依然,证明同理 那么就有两种可能:1、两个字符串都在这个位置出现这个相邻,2、两个字符串不存在同时出现这个相邻情况 前者比较好办,直接从这个位置将字符串拆分成两半,每一半都是相同的即可 后者则可以证明无解决方案。方法也很简单:因为一旦有一个不相同,那么必然需要处理成相同的,而一旦需要处理,则需要外部的来把她们抹成相同(每次操作后 01 段数量一定减少,所以只能抹去)而外部本身也没有匹配上,故需要外部的外部来抹去,依此类推,可以得到无法解决 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string a, b; a.reserve(5000); b.reserve(5000); cin >> a >> b; if (a.front() != b.front() || a.back() != b.back()) { cout << "NO" << endl; continue; } if (a.front() == a.back()) { cout << "YES" << endl; continue; } bool flag = false; for (int i = 0; i < a.size() - 1; ++i) { if (a[i] == a.front() && a[i + 1] == a.back() && b[i] == b.front() && b[i + 1] == b.back()) { flag = true; break; } } cout << (flag ? "YES" : "NO") << endl; } } C. Queries for the Array 大致题意 一个数组,开始的时候是空的,有三种操作 在数组最后加一个值 移去数组最后的一个值 询问数组是否有序 现在告诉你操作的顺序,以及是否有序的结果,但是不告诉你具体加了什么值,问是否可能 思路 简单题,用栈模拟即可,只要记住两个原则即可 若当前有序,那么删除最后一个值仍然有序 若当前无序,那么加入一个值仍然无序 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void solve() { int _; cin >> _; string str; str.reserve(200010); for (int ts = 0; ts < _; ++ts) { cin >> str; stack<int> flag; bool res = true; for (char i : str) { switch (i) { case '+': if (!flag.empty() && flag.top() == 2) { flag.push(2); } else { flag.push(0); } break; case '-': if (flag.size() >= 2 && flag.top() == 1) { flag.pop(); flag.top() = 1; } else { flag.pop(); } break; case '1': if (!flag.empty() && flag.top() == 2) { res = false; break; } if (!flag.empty()) { flag.top() = 1; } break; case '0': if (flag.empty() || flag.size() == 1 || flag.top() == 1) { res = false; break; } if (!flag.empty()) { flag.top() = 2; } break; } } cout << (res ? "YES" : "NO") << endl; } } D. Sorting By Multiplication 大致题意 给你一个数组,开始的时候都是正整数,允许你进行如下操作,让整个数组变成严格递增,至少需要几次 选择数组上任意的一个区间 选择一个任意整数,可以是负数 将区间内的每一个值都乘上选择的素数 思路 第一感觉是可以用 dp 解决,也索性从这个方向考虑了。 每个数字其实可以是正数 or 负数,这很明显,也是这个题目要考虑的重点 定义 $dp[i][j]$,其中 $i$ 表示位置,$j$ 表示当前值是正数还是负数,$0$ 表示正数,$1$ 表示负数 任何一个值,如果它前面是负值,那么它自身不需要任何操作就能满足局部严格递增 如果当前值和前一个值在初始值上已经严格递增了,且前一个值不是负数的情况下,那么只需要跟着前一个值进行一样的乘 $x$ 运算即可,最终也保持严格递增 如果当前值和前一个值已经在初始值上递减了,那么要么让前一个值变成负数,要么就需要自身乘以一个更高的系数来放大 根据上述三条,可以得到状态转移方程 $$\left\{\begin{matrix} dp_{i,0} = &\left\{\begin{matrix}min(dp_{i-1,0}, dp_{i-1,1}), & a_{i-1} a_i\end{matrix}\right.\\dp_{i,1} = &\left\{\begin{matrix}dp_{i-1, 1} + 1, & a_{i-1} a_i\end{matrix}\right.\end{matrix}\right.$$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item: data) cin >> item; vector<int> dp[2]; for (auto &row: dp) row.resize(n); dp[0][0] = 0; dp[1][0] = 1; for (int i = 1; i < n; ++i) { if (data[i - 1] < data[i]) { dp[0][i] = min(dp[0][i - 1], dp[1][i - 1]); dp[1][i] = dp[1][i - 1] + 1; } else if (data[i - 1] == data[i]) { dp[0][i] = min(dp[0][i - 1] + 1, dp[1][i - 1]); dp[1][i] = dp[1][i - 1] + 1; } else { dp[0][i] = min(dp[0][i - 1] + 1, dp[1][i - 1]); dp[1][i] = dp[1][i - 1]; } } cout << min(dp[0][n - 1], dp[1][n - 1]) << endl; } }

2023/9/2
articleCard.readMore

Pinely Round 2 (Div. 1 + Div. 2)

A. Channel 大致题意 你是一个频道的频道主,然后发了一条消息,同时你能知道订阅者上下线的消息(不知道具体是谁),上线的人必定看你的消息,问有没有可能所有人都看过你的消息了 思路 简单题,统计最大同时在线,和最多多少在线,就可以了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, a, q; cin >> n >> a >> q; string str; str.reserve(q); cin >> str; int ma = a, tot = a, cur = a; for (auto &item: str) { if (item == '+') { cur++; tot++; ma = max(ma, cur); } else cur--; } cout << (ma >= n ? "YES" : tot >= n ? "MAYBE" : "NO") << endl; } } B. Split Sort 大致题意 有一个 $n$ 的排列,每次选择一个数组中的一个值,将小于它的值放左边,大于等于的放右边,顺序不变,问最多操作几次后数组有序 思路 第一反应就是归并排序,太像了,只需要算出归并排序需要几次就行了 但是这个值可以说是确定的,即几乎等于 $n$,这是归并的性质决定的。而实际上已经基本排序好的数组,压根不需要那么多次 再仔细思考操作带来的意义,实际上本身顺序并没有发生太大变动,假如移动的是 $x$,那么对于除开 $x$ 以外的所有值而言,在 $[1, x)$ 内都没有发生相对位置变化,同理对于 $[x, n]$ 也是,但是跨 $x$ 的相对位置都变成正确的了。即当对任意的 $x$ 进行操作后,实际上就解决掉了 $x$ 关联的全部的逆序对问题,以及跨越 $x$ 的逆序对。 综上可以得到,若不选择 $x$ 的情况下,那么若 $x - 1$ 在 $x$ 的右侧情况下,那么必然永远不能消除此逆序对。所以只需要找出这样的逆序对,然后进行操作即可。至于剩下的逆序对,在完成上述操作后,自然已经满足要求了,这里不再详细证明 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; set<int> flag; int ans = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (flag.count(tmp + 1)) ans++; flag.insert(tmp); } cout << ans << endl; } } C. MEX Repetition 大致题意 给一个长度为 $n$ 的数组,其值在 $[0, n]$ 内,且没有重复 每次操作,需要依次对每一个值进行 $MEX$ 计算,并代替当前的值,问进行 $x$ 次操作后,数组变成什么样 思路 在数组最后补上没有的那个值,然后你就会发现只是在旋转数组罢了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n + 1); set<int> flag; for (int i = 0; i <= n; ++i) flag.insert(i); for (int i = 0; i < n; ++i) cin >> data[i]; for (int i = 0; i < n; ++i) flag.erase(data[i]); data[n] = *flag.begin(); k %= (n + 1); for (int i = 0; i < n; ++i) cout << data[(i + n + 1 - k) % (n + 1)] << " \n"[i == n - 1]; } } D. Two-Colored Dominoes 大致题意 有一个 $n \times m$ 的矩阵,矩阵上放着很多小木板,一块木板一定恰好占用两个相邻的格子,木板之间不重叠。 现在要给木板上色,黑色和白色,一块木板占用的两个格子,必须不同颜色。矩阵内任意一行一列都必须黑白色相同数量,给出一种上色法 思路 要相同,必然占用的格子数量是偶数 结论很简单,竖着的木板在同行的数量(或者横着的木板在同列的数量)一定是偶数 可以简单画一下错开的情况,就会发现无解了 知道这个结论之后随便画一下就好了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<string> ans(n); for (auto &row: ans) { row.reserve(m); cin >> row; } bool flag = true; for (int i = 0; i < n; ++i) { int last = -1; for (int j = 0; j < m; ++j) { // for U if (ans[i][j] != 'U') continue; if (last == -1) last = j; else { ans[i][j] = 'W'; ans[i + 1][j] = 'B'; ans[i][last] = 'B'; ans[i + 1][last] = 'W'; last = -1; } } if (last != -1) { flag = false; break; } } for (int j = 0; j < m; ++j) { int last = -1; for (int i = 0; i < n; ++i) { // for L if (ans[i][j] != 'L') continue; if (last == -1) last = i; else { ans[i][j] = 'W'; ans[i][j + 1] = 'B'; ans[last][j] = 'B'; ans[last][j + 1] = 'W'; last = -1; } } if (last != -1) { flag = false; break; } } if (!flag) { cout << -1 << endl; continue; } for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) cout << ans[i][j]; cout << endl; } } } E. Speedrun 大致题意 有 $n$ 个任务,每个任务完成需要的时间可以忽略不计,但是每个任务都有指定的完成时间点。时间是循环的,即类似自然时间,23 点之后又是 0 点。任务之间也有依赖关系 问最短需要多少时间完成这些任务 思路 一开始写了一个拓扑排序找,然后三分了所有可能的开始时间点($[0, k]$),理论上时间应该来得及,但是不知道为什么 TLE 了 后面开始仔细思考其他解决方案 实际上每个节点的时间可以看成是 $h_i + x * k$,即需要求出最大和最小值之差。然后对于第一批没有依赖的任务,他们可以是第一天完成的,也可以是第二天完成的,但是不可能是第三天完成的。故可以考枚举无依赖的任务是否是第二天完成的,然后计算此时最大的完成时间。由于无依赖任务本身也是有时间的,根据时间排序后遍历的话,若当前是第二天完成的,那么前面的也都是第二天完成的了,就不需要每次单独计算了,可以让下一次的状态是从上一次变化过来的,这样计算成本非常小 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; struct node { int v, n; bool operator<(const node &rhs) const { return rhs.v < v; } }; vector<int> cost(n); vector<int> head(n, -1); vector<node> edge(m); vector<int> deg(n, 0); for (auto &item: cost) cin >> item; for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; edge[i] = {v - 1, head[u - 1]}; head[u - 1] = i; deg[v - 1]++; } queue<int> q; vector<int> begin; for (int i = 0; i < n; ++i) if (deg[i] == 0) { q.push(i); begin.push_back(i); } int mx = 0, ans = LONG_LONG_MAX; while (!q.empty()) { int cur = q.front(); q.pop(); mx = max(mx, cost[cur]); for (int i = head[cur]; i != -1; i = edge[i].n) { --deg[edge[i].v]; cost[edge[i].v] += ((max(0LL, cost[cur] - cost[edge[i].v]) + k - 1) / k) * k; if (deg[edge[i].v] == 0) q.push(edge[i].v); } } sort(begin.begin(), begin.end(), [&](const int &lhs, const int &rhs) { return cost[lhs] < cost[rhs]; }); ans = min(ans, mx - cost[begin.front()]); function<void(int)> add = [&](int x) { mx = max(mx, cost[x]); for (int i = head[x]; i != -1; i = edge[i].n) { if (cost[edge[i].v] - cost[x] >= 0) continue; cost[edge[i].v] += ((max(0LL, cost[x] - cost[edge[i].v]) + k - 1) / k) * k; add(edge[i].v); } }; for (auto &item : begin) { ans = min(ans, mx - cost[item]); cost[item] += k; add(item); } ans = min(ans, mx - cost[begin.front()]); cout << ans << endl; } }

2023/9/1
articleCard.readMore

Harbour.Space Scholarship Contest 2023-2024 (Div. 1 + Div. 2)

A. Increasing and Decreasing 大致题意 给出数列的第一个和最后一个值,构造一个数列,保证 数列严格递增 数列的递增速率严格递减 找出任意一个即可 思路 简单题,倒着构造即可,最后判断是否符合 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int x, y, n; cin >> x >> y >> n; vector<int> data(n); data[0] = x; data[n - 1] = y; for (int i = 1; i < n - 1; ++i) data[n - i - 1] = data[n - i] - i; if (data[0] >= data[1] || data[1] - data[0] <= data[2] - data[1]) { cout << -1 << endl; continue; } for (int i = 0; i < n; ++i) cout << data[i] << " \n"[i == n - 1]; } } B. Swap and Reverse 大致题意 有一个字符串,长度为 $n$,允许你进行如下操作 选择一个合理的 $i$,交换 $a_i$ 和 $a_{i + 2}$ 选择一段连续的字符串,其长度为 $k$(为给出固定值),翻转这段字符串 问操作无数次可以到达的最小字符串是什么 思路 大胆猜测,小心求证。大概模拟了几个 case,猜测如果 $k$ 为奇数,则奇偶位置单独排序后输出就行了,否则就全部排序一下输出就好了 为奇数的情况比较简单,说白了就是奇偶位不会交换,就不再证明了 为偶数的情况,其实只需要隔一个位置进行翻转即可,因为题目说明了 $k < n$。例如 $123456$,且 $k = 4$,则翻转一次后为 $432156$,再取 $1-5$ 翻转,得到 $451236$,可以观察到其实仅仅开头两位的奇偶得到了翻转,后面的奇偶性不变,故此可以实现任意连续两位的翻转,即全字符串都可以排序好 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; string str; str.reserve(n); cin >> str; if (k % 2) { string tmp1, tmp2; tmp1.resize((n + 1) / 2); tmp2.resize(n / 2); for (int i = 0; i < n; ++i) { if (i % 2) tmp2[i / 2] = str[i]; else tmp1[(i + 1) / 2] = str[i]; } sort(tmp1.begin(), tmp1.end()); sort(tmp2.begin(), tmp2.end()); for (int i = 0; i < n; ++i) { if (i % 2) cout << tmp2[i / 2]; else cout << tmp1[(i + 1) / 2]; } cout << endl; } else { sort(str.begin(), str.end()); cout << str << endl; } } } C. Divisor Chain 大致题意 给出一个初始值,每次可以减去它的整除数,直到其变成 $1$,给出一条合理的整除数路径,要求其中 $1$ 至多出现两次 思路 首先第一反应就是偶数的一半,因为这样一定可以快速减少。但是免不了出现大量的奇数,导致很容易撞上多个 $1$ 的情况 由于实际上是减法,所以理论上一个偶数不断的减去偶数,那么肯定还是偶数。所以可以让初始值根据情况先减去 $1$,变成偶数,然后不断的减去偶数,最后变成 $2$ 了之后,再减去 $1$ 变成 $1$ 之后就需要不断的找到可以作为整除数的偶数,当然首先 $2$ 肯定可以,毕竟 $2$ 一定是任何偶数的除数,但是只用 $2$ 的话又太多了,所以还得用大一点的。这就让人很容易想到 $2^x$。 从二进制的角度看,实际上任何一个值都可以被它的 lowbit 整除,不断的减去 lowbit,似乎就符合预期了。再只剩下最后一个 bit 位后,再持续减去 lowbit / 2 就可以实现不断减少 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { auto lb = [&](int x) { return x & -x; }; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> ans; ans.reserve(1000); ans.push_back(n); while (n != 1) { int tmp = lb(n); if (tmp != n) n -= tmp; else n -= tmp >> 1; ans.push_back(n); } cout << ans.size() << endl; for (int i = 0; i < ans.size(); ++i) cout << ans[i] << " \n"[i == ans.size() - 1]; } } D. Matrix Cascade 大致题意 有一个 $01$ 矩阵,每次可以选择一个位置,然后以这个位置作为三角形的顶点,向下所有三角形以内的的都进行翻转,问最少几次操作可以把这个矩阵都变成 $0$ 思路 想办法维护好下一行有哪些是被翻转了的,奇数次翻转才有意义,题目其实不难 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<string> data(n); for (auto &item: data) item.reserve(n); for (auto &item: data) cin >> item; vector<int> l(n), r(n); int ans = 0; for (int i = 0; i < n; ++i) { int flag = false; for (int j = 0; j < n; ++j) { flag = (l[j] + r[j]) % 2 == 0 ? flag : !flag; if (flag == (data[i][j] == '1')) continue; else { l[j]++; if (j + 1 < n) r[j + 1]++; flag = !flag; ans++; } } l[0] += l[1]; for (int j = 1; j < n - 1; ++j) l[j] = l[j + 1]; l[n - 1] = 0; for (int j = n - 1; j > 0; --j) r[j] = r[j - 1]; r[0] = 0; } cout << ans << endl; } } E. Guess Game 大致题意 Alice、Bob、Carol 玩游戏 首先有一个数组,仅 Carol 可见,然后 Carol 从中随机选出两个值 $x, y$,可以相同位置 Carol 将这两个值进行 OR 计算,计算后得到 $z$,然后将 $x, z$ 给 Alice 看,$y, z$ 给 Bob 看 然后 Alice 和 Bob 轮流猜测 $x > y$ 还是 $x < y$ 还是 $x = y$。若不确定,可以选择不知道,如果能够确定,那么一定需要答出来 问期望的猜测次数是多少 思路 首先要理清楚,为什么回答不知道反而可以确定。比较好的是,题目中已经给出了比较详细的说明。这里也给一个例子 例如 $x = 2, y = 3$,那么 Alice 可以拿到 $x = 2, z = 3$ 而 Bob 拿到的是 $y = 3, z = 3$。 那么此时,Alice 可以知道,Bob 只有可能是 $3, 1$ 其中一个,故判断不出来,Alice 只能回复不确定 轮到 Bob 的时候,Bob 知道 Alice 可以是 $3, 2, 1, 0$ 中任意一个。但是 Alice 回复不知道,对于 Bob 而言,如果 Alice 是 $1, 0$ 的话,当她看到 $z = 3$,说明 Bob 至少为 $2$,故 Alice 应该可以确定,故可以排除掉这两个,所以 Alice 可以是 $3, 2$ 其中一个,但是还是不确定是否是相同还是 Alice 的更小,故也回复不知道 再轮到 Alice 了,如果 Bob 无法判断的话,那么 Bob 一定不是 $1$,理由同上了。故此时 Alice 才可以确定 Bob 一定是 $3$,那么就可以确定了 综上,需要 3 轮 这道题因为涉及到 OR 运算,所以很自然从二进制角度考虑问题,二进制的比较,无非就是从最高的 bit 位开始,谁先不是 1 了,谁就小了。 从 OR 运算考虑,若“我”拿到的那个值某一个 bit 位是 0,但是 OR 的结果是 1,那么显然,对方这一位是 1。同理,若 OR 的结果为 0,那么对方和我一样都是 0。问题就在“我”和 OR 的结果都是 1 的那些 bit 位。这个时候我并不确定对方是否是 1,这也是需要多轮博弈的地方。 如果这两个选择的值,在某个 bit 位之前都是相同的,且通过一段博弈之后相互确认相同了,但是在这个 bit 位下是是不同的,即一个为 0,一个是 1。那么对于那个为 0 的而言,必然可以立刻回答出答案,而那个为 1 的则仍然不确定,所以此时需要再加上 $1, 2$ 次判定,这取决于谁先回答。 而对于前面相同的部分,若为 0 的,不用猜疑也能知道对方的情况,故可以跳过,不需要猜,而为 1 的部分,则需要进行一次不确定的回答,才可以确认对方也是 1。注意,这里只需要一次回答,就可以让双方都知道对方那一位是 1 了。如果不确定,可以将上面的 case 反过来,让 Bob 先猜,则可以理解原因了。 综上,给出两个数字,这两个数字要猜需要的轮次就是:(第一个不同的位置中,0 先回答 ? $0$ : $1$) + 不同的位置前有多少个 1 而因为要计算整个数列的情况,所以可以考虑建一棵 01字典树,然后在树上计算。树上每个节点表示如果当前节点就是那个不一样的位置,那么需要多少轮。结果就是:(当前节点上面为 1 的节点数量 2 + 1) 当前节点为 0 的数量 * 当前节点为 1 的数量 需要考虑一下如果数字相同的情况,这里就不再详细说明了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #define int long long struct node { int cnt, zero, one; }; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, mod = 998244353; cin >> n; vector<node> tree(n * 50); int mx = 0; auto newNode = [&]() { tree[mx].cnt = 0; tree[mx].zero = -1; tree[mx].one = -1; return mx++; }; int root = newNode(); auto add = [&](int x) { int cur = root; for (int i = 31; i >= 0; --i) { if (x & (1 << i)) cur = tree[cur].one == -1 ? tree[cur].one = newNode() : tree[cur].one; else cur = tree[cur].zero == -1 ? tree[cur].zero = newNode() : tree[cur].zero; tree[cur].cnt++; } }; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; add(tmp); } int ans = 0, total = n * n; function<void(int, int)> dfs = [&](int cur, int deep) { if (tree[cur].zero == -1 && tree[cur].one == -1) { ans += deep * tree[cur].cnt * tree[cur].cnt; ans %= mod; return; } if (tree[cur].zero == -1) dfs(tree[cur].one, deep + 1); else if (tree[cur].one == -1) dfs(tree[cur].zero, deep); else { ans += (2 * deep + 1) * tree[tree[cur].one].cnt * tree[tree[cur].zero].cnt; ans %= mod; dfs(tree[cur].one, deep + 1); dfs(tree[cur].zero, deep); } }; auto qp = [&](int a, int b) { if (b < 0) return 0LL; int ret = 1; a %= mod; while (b) { if (b & 1) ret = (ret * a) % mod; b >>= 1; a = (a * a) % mod; } return ret; }; auto inv = [&](int a) { return qp(a, mod - 2); }; dfs(0, 1); cout << (ans * inv(total) % mod) << endl; } }

2023/8/27
articleCard.readMore

Codeforces Round 894 (Div. 3)

A. Gift Carpet 大致题意 从字符串矩阵中依次找出四列,满足依次包含 “vika” 四个字符 思路 简单题,不过多赘述 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<string> data(n); for (auto &item : data) item.reserve(m); for (auto &item : data) cin >> item; string vika = "vika"; int cur = 0; for (int i = 0; i < m && cur < vika.size(); ++i) { for (int j = 0; j < n; ++j) { if (data[j][i] == vika[cur]) { cur++; break; } } } cout << (cur == vika.size() ? "YES" : "NO") << endl; } } B. Sequence Game 大致题意 有一个原始的序列,将其中的 $a_0$ 以及 $a_{i - 1} \leq a_i$ 的 $a_i$ 都提取出来给你,问可能的原始序列是什么 思路 简单题,如果提取后的某个值不满足上述条件的,在其前面加个 $1$ 就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; int add = 0; for (int i = 1; i < n; ++i) add += data[i] < data[i - 1]; cout << n + add << endl; cout << data[0]; for (int i = 1; i < n; ++i) { if (data[i] < data[i - 1]) cout << ' ' << 1; cout << ' ' << data[i]; } cout << endl; } } C. Flower City Fence 大致题意 判定将木板排序后,横着和竖着放是否完全相同 思路 简答题,第 $i$ 块木板的长度,是否恰好都等于 $\leq i$ 的模板数量 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; bool flag = true; int ptr = n - 1; for (int i = 0; i < n; ++i) { while (ptr >= 0 && data[ptr] <= i) ptr--; if (data[i] != ptr + 1) { flag = false; break; } } cout << (flag ? "YES" : "NO") << endl; } } D. Ice Cream Balls 大致题意 制作出恰好 $n$ 个不同的包含两个冰球的冰淇淋,需要多少个冰球(同时制作,两个冰淇淋之间不共用冰球) 思路 本题要求的恰好制作出,从最优方案上,肯定是不同的冰球更好,可以得到 $\frac{n \times (n - 1)}{2}$ 种冰淇淋,但是这样难以凑到恰好 通过上面的方案逼近答案后,再加一些重复的冰球,由于需要不同的冰淇淋,所以每种冰球的数量不能超过 $2$ 个,否则是溢出无意义的,不会带来更多方案 而每增加一个额外的重复冰球,仅能带来一种方案,即类似 ${1, 1}$ 这种重复冰球的方案。所以只需要一个简单的减法就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, mid; cin >> n; mid = (int)sqrt(n * 2); int ans = LONG_LONG_MAX; for (int i = max(2LL, mid - 10); i < mid + 10; ++i) if (i * (i - 1) / 2 <= n) ans = min(ans, i + n - (i * (i - 1) / 2)); cout << ans << endl; } } E. Kolya and Movie Theatre 大致题意 在 $n$ 天内选出 $m$ 天,其中每一天能够拿到一定的分数,还需要扣除任意两个选出的天之间的分数差(默认选出第 0 天),分数差仅取决于天数差,问最大能拿到多少分 思路 这道题第一眼以为是需要 dp 但是仔细读题,会发现其实扣除的分数差就是最后选出的那一天的 $index$,因为恰好把所有区间加上了 那么就变得很简单了,只需要计算到达每天的位置,最大的 $m$ 个分数的值是哪些,用个堆就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, d; cin >> n >> m >> d; int ans = 0, cur = 0; priority_queue<int, vector<int>, greater<>> prq; for (int i = 0; i < n; ++i) { cur -= d; int tmp; cin >> tmp; if (tmp < 0) continue; if (prq.size() < m) { prq.push(tmp); cur += tmp; } else if (tmp > prq.top()) { cur -= prq.top(); cur += tmp; prq.pop(); prq.push(tmp); } ans = max(ans, cur); } cout << ans << endl; } } F. Magic Will Save the World 大致题意 有两种魔法,火魔法和水魔法,每种魔法每秒钟都会积攒对应的法力值,使用 $x$ 点法力值可以打败体力低于等于 $x$ 的怪,怪必须一次打死,问最多需要多少时间才能打死所有的怪 思路 题意中很容易看出是一个背包问题,类似均分为两堆,但是这里不是均分,而是有比例分,所以可以分别计算一次,避免出错 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int f, w; cin >> f >> w; int n, sum = 0; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; for (int i = 0; i < n; ++i) sum += data[i]; if (f >= sum || w >= sum) { cout << 1 << endl; continue; } int p = (sum + f + w - 1) / (f + w), ans; { int target = f * p; vector<int> dp(target + 1); for (int i = 0; i < n; ++i) for (int j = target; j >= data[i]; --j) dp[j] = max(dp[j], dp[j - data[i]] + data[i]); int maxDp = 0; for (int i = 0; i <= target; ++i) maxDp = max(maxDp, dp[i]); if (sum - maxDp <= p * w) ans = p; else ans = (sum - maxDp + w - 1) / w; dp.clear(); } { int target = w * p; vector<int> dp(target + 1); for (int i = 0; i < n; ++i) for (int j = target; j >= data[i]; --j) dp[j] = max(dp[j], dp[j - data[i]] + data[i]); int maxDp = 0; for (int i = 0; i <= target; ++i) maxDp = max(maxDp, dp[i]); if (sum - maxDp <= p * f) ans = min(ans, p); else ans = min(ans, (sum - maxDp + f - 1) / f); dp.clear(); } cout << ans << endl; } } G. The Great Equalizer 大致题意 每次,将数组排序后,为一个数组中的每个值加上 $n, n - 1, n - 2 \dots, 1$,然后去重,重复,直到只剩下一个值,问最后这个值是什么。 不直接需要原数组的答案,是依次回答的,每次会修改数组中的值,然后询问,修改操作是继承的 思路 观察可以得到,最终结果实际上是 $max(a_i) - min(a_i) + max(a_i - a_{i-1}) + min(a_i)$,化简得到 $max(a_i) + max(a_i - a_{i-1})$。只需要维护好这两值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (auto &item : data) cin >> item; vector<int> copy = data; sort(copy.begin(), copy.end()); map<int, int> dif, cnt; for (int i = 0; i < n; ++i) cnt[data[i]]++; for (int i = 1; i < n; ++i) dif[copy[i] - copy[i - 1]]++; int total = 0; for (int i = 1; i < n; ++i) total += copy[i] - copy[i - 1]; int q; cin >> q; for (int i = 0; i < q; ++i) { int index, x; cin >> index >> x; if (n == 1) { data[0] = x; cout << x << ' '; continue; } int old = data[index - 1]; const auto oldIter = cnt.find(old); int al, ar, am, bl, br, bm; if (oldIter->second > 1) { oldIter->second--; bl = br = bm = 0; } else { int lv, rv; if (oldIter == cnt.begin()) lv = -1; else { auto left = oldIter; left--; lv = left->first; } auto right = oldIter; right++; if (right == cnt.end()) rv = -1; else rv = right->first; bl = lv == -1 ? 0 : old - lv; br = rv == -1 ? 0 : rv - old; bm = lv == -1 || rv == -1 ? 0 : bl + br; cnt.erase(oldIter); } data[index - 1] = x; const auto newIter = cnt.upper_bound(x); if (newIter == cnt.end()) { ar = am = 0; auto iter = newIter; iter--; al = x - iter->first; } else if (newIter == cnt.begin()) { al = am = 0; ar = newIter->first - x; } else { int lv, rv = newIter->first; auto tmp = newIter; --tmp; lv = tmp->first; al = x - lv; ar = rv - x; am = rv - lv; } cnt[x]++; auto del = [&](int t) { auto iter = dif.find(t); if (iter == dif.end()) return; if (iter->second == 1) dif.erase(iter); else iter->second--; }; dif[al]++; dif[ar]++; dif[bm]++; del(bl); del(br); del(am); cout << cnt.rbegin()->first + dif.rbegin()->first << ' '; } cout << endl; } }

2023/8/26
articleCard.readMore

Codeforces Round 888 (Div. 3)

A. Escalator Conversations 大致题意 有一个 $n$ 级台阶,每级 $h$ 高,有 $t$ 个人,问高为 $H$ 的一个人,通过在台阶上的方式,可以和哪些人等高 思路 简单题,差值 mod 一下恰好是台阶的倍数,且倍数恰好小于台阶数量,那么就可以了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k, h, ans = 0; cin >> n >> m >> k >> h; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; int dif = abs(tmp - h); if (dif == 0 || dif % k) continue; if (dif / k < m) ans++; } cout << ans << endl; } } B. Parity Sort 大致题意 有个数组,允许无限次交换两个位置,要求是交换的那两个数字奇偶性必须一致,问最终是否能有序 思路 简单题,每个位置最开始是奇数,那么无论怎么换都是奇数,且任何奇数都可以换到这个位置,偶数同理,故所以只需要排序后的每个位置的奇偶性保持即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data1(n), data2(n); for (int i = 0; i < n; ++i) cin >> data1[i]; for (int i = 0; i < n; ++i) data2[i] = data1[i]; sort(data2.begin(), data2.end()); bool check = false; for (int i = 0; i < n; ++i) if (data1[i] % 2 != data2[i] % 2) check = true; cout << (check ? "NO" : "YES") << endl; } } C. Tiles Comeback 大致题意 问能否在一个数列中找到一个子序列,满足 长度恰好是 $k$ 的倍数 将序列每 $k$ 个一段,分成 $x$ 段,每一段内的数字相同 第一个和最后一个必须在序列中 问是否存在即可 思路 简单题,分两种情况,第一种,如果开头和结尾的数字相同。那能找到 $k$ 个和首尾相同的数字,直接选出这些数字即可 如果不同,那么只需要和首相同找出 $k$ 个,然后在这 $k$ 个之后再找出 $k$ 个和尾相同的即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; if (data.front() == data.back()) { int cnt = 0; for (int i = 0; i < n; ++i) cnt += data[i] == data.front(); cout << (cnt >= k ? "YES" : "NO") << endl; } else { int cnt1 = 0, cnt2 = 0; for (int i = 0; i < n; ++i) if (cnt1 >= k) cnt2 += data[i] == data.back(); else cnt1 += data[i] == data.front(); cout << (cnt1 >= k && cnt2 >= k ? "YES" : "NO") << endl; } } } D. Prefix Permutation Sums 大致题意 给你一个丢了一个数字的前缀和,原数组为 $n$ 的排列,问是否存在可能的原始串 思路 前缀和相减就是原始数字了,直接算出所有的前缀差,找出重复的,和没有出现的,算一算加起来是否相同。或者只有一个没有出现的,恰好是最后那个值丢了的情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; set<int> st; for (int i = 0; i < n; ++i) st.insert(i + 1); int last = 0, out = -1; for (int i = 0; i < n - 1; ++i) { int tmp; cin >> tmp; auto iter = st.find(tmp - last); if (iter == st.end()) out = tmp - last; else st.erase(iter); last = tmp; } if (st.size() == 1 && out == -1) { cout << "YES" << endl; continue; } if (st.size() != 2) { cout << "NO" << endl; continue; } int a, b; a = *st.begin(); b = *(++st.begin()); cout << (a + b == out ? "YES" : "NO") << endl; } } E. Nastya and Potions 大致题意 有 $n$ 种药品,其中部分药品可以通过其他药品合成得到,每种药品的价格已知,且部分药品有库存,即免费,问得到每一种药品需要多少钱 思路 其实这是一个 Dag 图,拓扑一下,然后不断计算更小的值,替换掉原来的价格即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> cost(n + 1); for (int i = 1; i <= n; ++i) cin >> cost[i]; for (int i = 0; i < k; ++i) { int tmp; cin >> tmp; cost[tmp] = 0; } vector<vector<int>> from(n + 1); vector<vector<int>> to(n + 1); vector<int> in(n + 1, 0); queue<int> q; for (int i = 1; i <= n; ++i) { int cnt; cin >> cnt; if (cnt == 0) q.push(i); for (int j = 0; j < cnt; ++j) { int tmp; cin >> tmp; from[i].push_back(tmp); to[tmp].push_back(i); in[i]++; } } while (!q.empty()) { auto cur = q.front(); q.pop(); if (!from[cur].empty()) { int c = 0; for (auto &item: from[cur]) c += cost[item]; cost[cur] = min(cost[cur], c); } for (auto &item : to[cur]) if (--in[item] == 0) q.push(item); } for (int i = 1; i <= n; ++i) cout << cost[i] << " \n"[i == n]; } } F. Lisa and the Martians 大致题意 给定一个 $k$,然后给出 $n$ 个数字,保证每个数字都是 $[1, 2^k)$ 内,需要找到一个值 $x$,并取出这些数字中的两个 $a, b$,计算 $(a \oplus x) \& (b \oplus x)$ 的最大值 思路 根据公式,我们可以推导出:若 $a$ 和 $b$ 在二进制上重合度越大,则结果越大,相同即可以得到 $1$,否则这个 bit 只能是 $0$。而其中,高位的价值最高,故需要先满足高位相同 这时可以想到 01字典树,然后从根开始往下 dfs 找,找到重合度尽可能大的,然后再计算分歧部分的代价。 定义 01字典树的结构 1 2 3 4 struct node { int zero = -1, one = -1, cnt = 0; vector<int> index; }; 其中 zero 表示接下来为 0 的字典树节点,同样 one 为接下来为 1 的字典树节点。cnt 表示这个节点下有多少值,index 则是为了求解,需要保留下数值原始的下标 比如当前正在某个 01字典树的节点上,已知下面的 0 节点下至少有两个值,那么就可以考虑更具体的情况,1 也相同,此时并不需要考虑两个值分散在两边的可能,因为这样的话,下个 bit 就只能得到 0,而相同可以得到 1,故不需要考虑分散的情况 如果某个节点下面只有一个值,那就毫无意义,不需要考虑下去了,理论上也不应该走到这种分支 如果某个节点上恰好有两个值,且左边有一个右边有一个,这个时候是分歧点,故可以在此节点取出下面的那两个值,然后计算结果 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { struct node { int zero = -1, one = -1, cnt = 0; vector<int> index; }; int n, k; cin >> n >> k; vector<node> tree(n * 40); int rNode = 1, root = 0; auto newNode = [&]() { return rNode++; }; auto add = [&](int x, int index) { tree[root].cnt++; int cur = root; for (int i = k - 1; i >= 0; --i) { if (x & (1 << i)) { cur = tree[cur].one == -1 ? tree[cur].one = newNode() : tree[cur].one; tree[cur].cnt++; } else { cur = tree[cur].zero == -1 ? tree[cur].zero = newNode() : tree[cur].zero; tree[cur].cnt++; } } tree[cur].index.push_back(index); }; auto find = [&](int cur, int &x, int deep) { while (cur != -1) { if (tree[cur].zero == -1 && tree[cur].one == -1) return tree[cur].index[0]; if (tree[cur].zero == -1) { x |= 1 << deep; cur = tree[cur].one; } else cur = tree[cur].zero; deep--; } return -1; }; int res = INT_MIN, resX, l, r; function<void(int, int x, int)> dfs = [&](int cur, int x, int deep) { if (tree[cur].zero == -1 && tree[cur].one == -1) { assert(tree[cur].cnt >= 2 && tree[cur].index.size() >= 2); res = (1 << k) - 1; l = tree[cur].index[0]; r = tree[cur].index[1]; resX = x; return; } if (tree[cur].zero == -1) dfs(tree[cur].one, x, deep - 1); else if (tree[cur].one == -1) dfs(tree[cur].zero, x | (1 << deep), deep - 1); else { if (tree[tree[cur].zero].cnt >= 2) dfs(tree[cur].zero, x | (1 << deep), deep - 1); if (tree[tree[cur].one].cnt >= 2) dfs(tree[cur].one, x, deep - 1); if (tree[tree[cur].zero].cnt == 1 && tree[tree[cur].one].cnt == 1) { int lv = 0, rv = 1 << deep; int li = find(tree[cur].zero, lv, deep - 1); int ri = find(tree[cur].one, rv, deep - 1); int tmp = 0; for (int i = k - 1; i > deep; --i) tmp |= 1 << i; for (int i = deep; i >= 0; --i) if ((lv & (1 << i)) == (rv & (1 << i))) { tmp |= 1 << i; x |= (lv & (1 << i)) ? 0 : 1 << i; } if (tmp > res) { l = li; r = ri; resX = x; res = tmp; } } } }; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; add(tmp, i + 1); } dfs(0, 0, k - 1); cout << l << ' ' << r << ' ' << resX << endl; } } G. Vlad and the Mountains 大致题意 有 $n$ 座山,山之间有桥,从 $a$ 山到 $b$ 山的代价为 $h_b - h_a$,注意,可以为负数。代价超过上限则不能走 询问 $q$ 次,问能否从 $a$ 山到 $b$ 山,在能够消耗最大 $e$ 的代价情况下 思路 首先不能在线做,只能离线,毕竟在线就只剩下预处理求出任意两点的代价了 题意解析也很简单的,就是需要找到一条路径,满足最大值小于等于 $h_a + e$ 即可 询问是否可达,很容易想到了并查集去做,毕竟在一个集合里就是可达 接下来,我们根据海拔的高低进行排序,然后从低海拔开始,将山加入集合,并将可以使用的边加入并查集 因为每个询问能够到达最大高度是确定的,所以同时可以遍历所有询问,若最大高度已经到达了,下一个加入的山会超出最大高度了,此时这两个山还没有在一个集合中,那么必然不可达 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 void solve() { struct node { int v, n; }; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<pair<int, int>> h(n + 1); vector<node> edge(m); vector<int> head(n + 1, -1); for (int i = 1; i <= n; ++i) cin >> h[i].first; for (int i = 1; i <= n; ++i) h[i].second = i; for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; if (h[u].first > h[v].first) { edge[i] = {v, head[u]}; head[u] = i; } else { edge[i] = {u, head[v]}; head[v] = i; } } int q; cin >> q; struct query { int u, v, e, i; }; vector<query> ql(q); vector<bool> ans(q); for (int i = 0; i < q; ++i) cin >> ql[i].u >> ql[i].v >> ql[i].e; for (int i = 0; i < q; ++i) ql[i].e += h[ql[i].u].first; for (int i = 0; i < q; ++i) ql[i].i = i; sort(h.begin() + 1, h.end()); sort(ql.begin(), ql.end(), [&](const query &a, const query &b) { return a.e < b.e; }); vector<int> fa(n + 1); for (int i = 0; i < fa.size(); ++i) fa[i] = i; function<int(int)> find = [&](int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }; auto join = [&](int x, int y) { int rx = find(x), ry = find(y); if (rx == ry) return; fa[rx] = ry; }; int last = 0, qPtr = 0; for (int i = 1; i <= n; ++i) { if (last != h[i].first) { while (qPtr < q && ql[qPtr].e < h[i].first) { auto &que = ql[qPtr]; int ru = find(que.u), rv = find(que.v); ans[que.i] = ru == rv; qPtr++; } } last = h[i].first; for (int j = head[h[i].second]; j != -1; j = edge[j].n) join(h[i].second, edge[j].v); } while (qPtr < q) { auto &que = ql[qPtr]; int ru = find(que.u), rv = find(que.v); ans[que.i] = ru == rv && ql[qPtr].e >= last; qPtr++; } for (auto item: ans) cout << (item ? "YES" : "NO") << endl; cout << endl; } }

2023/8/20
articleCard.readMore

Educational Codeforces Round#153 (Div. 2)

A. Not a Substring 大致题意 需要构建一个只有小括号构成的字符串,既满足括号匹配,同时不存在一个子串等同于给出的一个字符串 思路 实际上很简单,只需要取 ()()()() 模式 和 (((()))) 这两种即可,因为这两种模式的唯一相同的子串就只有一对 (),而若需要一个满足括号匹配的字符串,那么必然存在 (),故这两种模式就可以应对所有情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str, ans; cin >> str; ans.resize(str.size() << 1); for (int i = 0; i < str.size(); ++i) ans[i] = '('; for (int i = 0; i < str.size(); ++i) ans[i + str.size()] = ')'; if (strstr(ans.c_str(), str.c_str()) == nullptr) { cout << "YES" << endl; cout << ans << endl; continue; } for (int i = 0; i < str.size(); ++i) ans[i * 2] = '('; for (int i = 0; i < str.size(); ++i) ans[i * 2 + 1] = ')'; if (strstr(ans.c_str(), str.c_str()) == nullptr) { cout << "YES" << endl; cout << ans << endl; continue; } cout << "NO" << endl; } } B. Fancy Coins 大致题意 有 $a1$ 个 $1$ 元,$a2$ 个 $k$ 元,同时你可以“借来”无限量的 $1$ 元和 $k$ 元,问组成 $m$ 元最多需要借多少硬币 思路 简单卡一下边界,多一个 $k$ 元和少一个 $k$ 元的两种情况考虑一下即可,比较简单 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int m, k, a1, a2; cin >> m >> k >> a2 >> a1; m -= min(m / k, a1) * k; if (m <= a2) { cout << 0 << endl; continue; } int ls = (m - a2) / k; int ans = ls + (m - a2 - ls * k); if (m - (ls + 1) * k >= 0) ans = min(ans, ls + 1); cout << ans << endl; } } C. Game on Permutation 大致题意 有一个数组,开始位置可以是任意的一个下标,每次可以移动到当前位置左边的任意一个值小于当前的位置。 两个人依次操作,谁最后无法进行操作了,谁胜利,问放在哪些位置,可以保证第二个开始操作的胜利 思路 假如说我摆放在一个位置,然后可以通过 $3$ 个依次操作达到最终无法移动(例如 $a \rightarrow b \rightarrow c \rightarrow d$),那么此时应该说第二个移动的人胜利 但是这个操作是可跳过的,因为你可以移动 $3$ 次,那么就必然可以一次移动到底,因为一定也符合题意,那么第一个移动的人为什么要遵循一个个移动呢,他完全可以直接 $a \rightarrow c$,然后第二个操作的人只能移动到 $d$,然后输了游戏 所以必须卡在一些只能移动一次的地方,否则就有可乘之机。 那么就必须保证选择的点满足 大于左边最小的值 小于左边之前确认的满足条件的点 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, ans = 0, curMin = INT_MAX, curMax = INT_MAX; cin >> n; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp < curMin) curMin = tmp; else if (tmp > curMin && tmp < curMax) { ans++; curMax = tmp; } } cout << ans << endl; } }

2023/8/20
articleCard.readMore

Codeforces Round 893 (Div. 2)

A. Buttons 大致题意 有 $1, 2, 3$ 三种按钮,其中 Anna 只能按 $1, 3$ 两种按钮,而 Katie 只能按 $2, 3$ 两种按钮。每个按钮只能按一次。 Anna 和 Katie 玩游戏,两人依次按按钮,Anna 先,直到谁没有按钮可以按,谁就输了,问谁会赢 思路 明显大家先抢着把 $3$ 按完就行了,因为 Anna 先开始按,所以为偶数则恰好对半分,为奇数则 Anna 多分到一个,然后计算谁按钮多就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int a, b, c; cin >> a >> b >> c; a += (c + 1) / 2; b += c / 2; cout << (a > b ? "First" : "Second") << endl; } } B. The Walkway 大致题意 有一条路,路上有一些饼干店,饼干店的初始位置都确定,有一个人带着无限数量的饼干,从路的某一端匀速走到另外一端,每隔 $k$ 分钟没有吃饼干的情况下,他会吃掉背包里的一片饼干,如果刚刚遇到了饼干店的情况下,他也会吃掉一片饼干,在同一个时间点不会吃掉超过一片饼干,且在刚刚进入路的时候需要吃一片饼干。 你可以移除掉一个,最多一个饼干店,问最少只需要吃到多少饼干 思路 通过饼干店,可以分割成 $n$ 段,每一段吃掉的饼干数量等于 $\left \lceil \frac{pos_{n} - pos_{n-1}}{t} \right \rceil$,那么对于每一个饼干店 $i$,若其被移除掉,可以带来减少的饼干数量为 $\left \lceil \frac{pos_{i + 1} - pos_{i - 1}}{t} \right \rceil - \left \lceil \frac{pos_{i} - pos_{i - 1}}{t} \right \rceil - \left \lceil \frac{pos_{i + 1} - pos_{i}}{t} \right \rceil$ 所以只需要枚举所有可能被干掉的饼干店,找到能减少的最大值就行了,注意一下,这里输出的最小饼干数量和能满足这个最小饼干数量的饼干店数量,而不是唯一那个位置 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, d; cin >> n >> m >> d; vector<int> data(m); for (int i = 0; i < m; ++i) cin >> data[i]; int tot = 0, ans = 0, cnt = 0; int l = 1, r = data[1]; for (int i = 0; i < m; ++i) { int tmp = (data[i] - l + d - 1) / d; tot += tmp; tmp += (r - data[i] + d - 1) / d; int del = (r - l + d - 1) / d; if (tmp - del > ans) { cnt = 1; ans = tmp - del; } else if (tmp - del == ans) cnt++; l = data[i]; r = i + 2 < m ? data[i + 2] : n + 1; } tot += (r - data[m - 1] + d - 1) / d; cout << tot - ans << ' ' << cnt << endl; } } C. Yet Another Permutation Problem 大致题意 给出一个 $n$ 的排列,使得所有相邻两个数的 $GCD$ 的值的种类足够多 思路 若相邻两个数字存在 $GCD$,那么必然这个 $GCD$ 小于等于其中的任意一个。而因为恰好是 $n$ 的排列,那么必然此 $GCD$ 的值本身一定存在与序列中。那么如果我直接取每个值以及其两倍的值放在一起,那么必然可以保证这个值可以存在。而且每个值的一半一定唯一,那么就可以得到唯一确认的绑定关系,然后排列即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<bool> vis(n + 1, false); vector<int> res; res.reserve(n); for (int i = 1; i <= n; ++i) { if (vis[i]) continue; res.push_back(i); vis[i] = true; int tmp = i * 2; while (tmp <= n) { vis[tmp] = true; res.push_back(tmp); tmp *= 2; } } for (int i = 0; i < n; ++i) cout << res[i] << " \n"[i == n - 1]; } } D. Trees and Segments 大致题意 有一个长度为 $n$ 的 $01$ 串,允许你翻转其中 $k$ 个,求对于每个 $a \in [1, n]$,求算 $a \times len_0 + len_1$ 的最大值。 $len_x$ 的指在这个字符串内,最长的一段连续的 $x$ 的长度 思路 这道题的难度跃升有点快,确实很难想清楚 首先考虑最暴力的情况,遍历每种 $a$,遍历所有可能的最长的 $0$ 的左右区间和 $1$ 的左右区间,然后计算是否满足并求解,那么总共需要 $n^6$ 首先校验合法可以通过前缀和的方式预处理,这样就可以少一个 $n$ 再考虑到不同的 $a$ 之和长度有关,而长度最多只有 $n$ 种可能(当 $len_0$ 为 $x$ 的时候,$max(len_1)$ 一定是唯一解),所以也不需要遍历所有 $a$,只需要计算出所有可能,然后再让 $a$ 和所有可能进行遍历即可,那么只需要一个单独的 $n^2$。 这样,我们只剩下了 $n^4$ 为了达到目标,我们还需要拆分这两个 $n^2$,让找 $max(len_1)$ 变成近乎 $O(1)$ 的查找。那么很显然我们需要预处理,因为当前通过 $n^2$ 的方式确定 $len_0$ 的情况下,其实 $len_1$ 仅会出现在这个区间的左边或者右边,故预处理从 $1 \rightarrow n$ 的每一个位置,进行 $1 \rightarrow k$ 次操作的情况下 $max(len_1)$ 是多少,同时还有 $n \rightarrow 1$ 的也需要 这里很显然应该通过 dp 去解决,设定 $dp[i][j]$ 表示从 $1 \rightarrow i$ 这段区间内在保证 $i$ 被选入作为 $len_1$ 的情况下(即无论如何当前位置得是 $1$)当前的连续的 $1$ 的长度是多少,这非常简单,可以得到递推公式 $$dp\_{i,j} = \left\{ \begin{matrix} dp\_{i-1,j} & s[i] = 1 \\\\dp\_{i-1,j-1} & s[i] = 0 \\\\0 & j = 0 \end{matrix} \right.$$ 然后再做一次取较大值 $dp_{i, j} = max(dp_{i, j}, dp_{i, j - 1}, dp_{i - 1, j})$,这样就可以 $O(1)$ 的方式快速得到在某个区间内,允许操作 $k$ 次的情况下,能够得到最长的 $1$ 串有多长了 然后暴力就好 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; string str; str.reserve(n); cin >> str; vector<vector<int>> left(n); for (auto &item : left) item.resize(k + 1); left[0][0] = str[0] == '0' ? 0 : 1; for (int i = 1; i <= k; ++i) left[0][i] = 1; for (int i = 1; i < n; ++i) { left[i][0] = str[i] == '0' ? 0 : (i == 0 ? 1 : left[i - 1][0] + 1); for (int j = 1; j <= k; ++j) left[i][j] = str[i] == '0' ? left[i - 1][j - 1] + 1 : left[i - 1][j] + 1; } for (int i = 1; i < n; ++i) left[i][0] = max(left[i][0], left[i - 1][0]); for (int i = 1; i < n; ++i) for (int j = 1; j <= k; ++j) left[i][j] = max(max(left[i - 1][j], left[i][j]), left[i][j - 1]); vector<vector<int>> right(n); for (auto &item : right) item.resize(k + 1); right[n - 1][0] = str[n - 1] == '0' ? 0 : 1; for (int i = 1; i <= k; ++i) right[n - 1][i] = 1; for (int i = n - 2; i >= 0; --i) { right[i][0] = str[i] == '0' ? 0 : (i == n - 1 ? 1 : right[i + 1][0] + 1); for (int j = 1; j <= k; ++j) right[i][j] = str[i] == '0' ? right[i + 1][j - 1] + 1 : right[i + 1][j] + 1; } for (int i = n - 2; i >= 0; --i) right[i][0] = max(right[i][0], right[i + 1][0]); for (int i = n - 2; i >= 0; --i) for (int j = 1; j <= k; ++j) right[i][j] = max(max(right[i + 1][j], right[i][j]), right[i][j - 1]); vector<int> preS(n + 1, 0); for (int i = 1; i <= n; ++i) preS[i] = preS[i - 1] + (str[i - 1] == '1'); vector<int> cnt(n + 1, -1); cnt[0] = max(left[n - 1][k], right[0][k]); for (int l = 0; l < n; ++l) { for (int r = l; r < n && preS[r + 1] - preS[l] <= k; ++r) { int cost = preS[r + 1] - preS[l]; int max1 = max(l == 0 ? 0 : left[l - 1][k - cost], r == n - 1 ? 0 : right[r + 1][k - cost]); cnt[r - l + 1] = max(cnt[r - l + 1], max1); } } vector<int> ans(n + 1, 0); for (int i = 1; i <= n; ++i) for (int j = 0; j <= n; ++j) ans[i] = max(ans[i], cnt[j] == -1 ? 0 : i * j + cnt[j]); for (int i = 1; i <= n; ++i) cout << ans[i] << " \n"[i == n]; } } 虽然写的挺丑,主要是不习惯写 dp,但是这段代码压根没有用到 if

2023/8/19
articleCard.readMore

OTPAUTH,两步验证中的通用协议

起因 昨天突然 GitHub 给我发了一封邮件,要求我必须添加 2FA 的验证 好吧好吧,那就创建吧。但是在创建的过程中,GitHub 问我是否有使用 1Password 之类的软件,如果有的话,可以扫码添加 2FA 嗯???因为我基本上是躺在 Apple 生态里的,所以选择让 Apple 自带的密码管理系统试试看,于是直接扫码了 GitHub 提供的二维码 结果扫码之后果真添加了 2FA 的能力 这个验证码很像 steam 使用的那种自生成的两步验证码,这让我觉得似乎有一种通用协议,来实现这样一套东西。立马开整 第一步,再搞到一份这样的二维码。我翻阅了 GitHub 的文档,最终找到了一份 GitHub 提供的示例二维码 接着直接读取二维码内的信息得到了这样一个地址: otpauth://totp/GitHub:octocat-testing?secret=F76W4SX573PDRIDA&issuer=GitHub 嗯,看起来是有一个用 OTPAUTH 的验证协议,其中 OTP 大概率就是 One Time Password 了。 OTP 协议 一般 OTP 有两种策略: 计次使用(HOTP)和计时使用(TOTP)。计次使用的密码使用过一次就失效;计时使用的密码过一段时间就失效。 HOTP 的全称是 HMAC-based One Time Password,它是基于 HMAC 的一次性密码生成算法。HMAC 的全称是 Hash-based Message Authentication Code,是指密钥相关的哈希运算消息认证码。HMAC 利用 MD5、SHA-1 等哈希算法,针对输入的密钥和消息,输出消息摘要。HOTP 算法中,传入密钥 K 和计数器 C,得到数字校验码。 实际使用 HOTP 中,服务端会给用户生成密钥 K,并约定起始计数器 C。客户端根据 K 和 C 生成校验码,并在用户点击刷新按钮后将计数器加 1,同时更新校验码;而服务端会在每次校验成功后将计数器加 1,这就保证了校验码只能使用一次。但客户端刷新并不通知服务端,很可能出现客户端计数器大于服务端的情况。所以一般的实现里,服务端如果用 PASSWORD = HOTP(K, C) 验证失败,还会尝试 C+1、C+2…,如果匹配上了,就更新服务端的计数器,保证跟客户端步调一致。出于安全考虑,服务端会设置一个最大值,并不会无限制地尝试下去。 HOTP 的优点是可以事先算好一批校验码,用户可以把他们打印出来随身携带逐个使用,用一个划掉一个,达到客户端计数器累加的效果,这样可以完全不依赖于电子设备。HOTP 的缺点是计数器很容易不一致,服务端经常需要通过不断尝试来同步计数器,从而降低了安全性。 TOTP 的全称是 Time-based One-time Password,它是基于时间的一次性密码生成算法。TOTP 算法需要约定一个起始时间戳 T0,以及间隔时间 TS。把当前时间戳 now 减去 T0,用得到的时间差除以 TS 并取整,可以得到整数 TC。根据 PASSWORD = HOTP(K, TC) 就可以得到数字校验码。 TOTP 实际上只是把 HOTP 的递增计数器换成了与当前时间有关的 TS,从而在服务端 / 客户端时间一致的前提下,解决了 HOTP 需要同步计数器的问题。同时,TOTP 算法需要用到当前时间,需要现场计算,无法提前算好打印出来。默认情况下,TOTP 在间隔时间 TS 内都能通过校验,并不是一次有效。这个问题可以通过在服务端记录最后一次 TC 来解决,由于 TS 一般很短,通常也可以忽略。 翻阅 google-authenticator 的 wiki 可以看到,这里有非常详细关于 URL 的参数的描述 Secret REQUIRED: The secret parameter is an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted. Issuer STRONGLY RECOMMENDED: The issuer parameter is a string value indicating the provider or service this account is associated with, URL-encoded according to RFC 3986. If the issuer parameter is absent, issuer information may be taken from the issuer prefix of the label. If both issuer parameter and issuer label prefix are present, they should be equal. Valid values corresponding to the label prefix examples above would be: issuer=Example, issuer=Provider1, and issuer=Big%20Corporation. Older Google Authenticator implementations ignore the issuer parameter and rely upon the issuer label prefix to disambiguate accounts. Newer implementations will use the issuer parameter for internal disambiguation, it will not be displayed to the user. We recommend using both issuer label prefix and issuer parameter together to safely support both old and new Google Authenticator versions. Algorithm OPTIONAL: The algorithm may have the values: SHA1 (Default) SHA256 SHA512 Currently, the algorithm parameter is ignored by the Google Authenticator implementations. Digits OPTIONAL: The digits parameter may have the values 6 or 8, and determines how long of a one-time passcode to display to the user. The default is 6. Currently, on Android and Blackberry the digits parameter is ignored by the Google Authenticator implementation. Counter REQUIRED if type is hotp: The counter parameter is required when provisioning a key for use with HOTP. It will set the initial counter value. Period OPTIONAL only if type is totp: The period parameter defines a period that a TOTP code will be valid for, in seconds. The default value is 30. Currently, the period parameter is ignored by the Google Authenticator implementations.

2023/8/16
articleCard.readMore

Codeforces Round 892 (Div. 2)

A. United We Stand 大致题意 有一个数组,你需要把里面的值分成两个数组 $a, b$,保证 $a$ 数组中不存在任何一个值,除 $b$ 数组中的任何一个值后,余数为 0 思路 这里强制要求 $a$ 除以 $b$,而且也没有要求数量均分之类的,只要得到任意解就行。那把最大的那个值放到 $b$,剩下的都放到 $a$ 就行了,因为除以一个比你大的值,那么一定没办法除尽的 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; int mx = 0, cnt = 0; for (int i = 0; i < n; ++i) { if (data[i] == mx) cnt++; else if (data[i] > mx) { cnt = 1; mx = data[i]; } } if (n == cnt) { cout << -1 << endl; continue; } cout << n - cnt << ' ' << cnt << endl; for (int i = 0; i < n; ++i) if (data[i] != mx) cout << data[i] << ' '; cout << endl; for (int i = 0; i < n; ++i) if (data[i] == mx) cout << data[i] << ' '; cout << endl; } } B. Olya and Game with Arrays 大致题意 有 $n$ 个数组,允许你将每一个数组的其中一个值移动到另外一个数组,操作必须同时进行,即每个数字仅可以被移动一次,然后再将每个数组的最小值相加,问最大可以是多少 思路 首先,每次移动一个值,要使这个数组的最小值提升,那么必定是移动最小的那个值,因为只能移动一次,所以必定是最小的那个值被移动,此时整个数组的最小值就是原来的次小值了 最终所有最小值都会被移动到其他的数组,而为了避免影响到整体最终结果值,那么肯定需要移动到一个数组本来的最小值就比这个被移动过来的小的数组上,而恰好的是,那个全局最小值所在的数组也需要移动最小值,那么必然这个全局最小值都会被移动到某个特定的数组,而其他数组的最小值如果也移动到那个数组,那么一定不会产生影响 所以需要计算最小值移动到哪个数组,那么应该选择移动后影响最小的,影响就是每个数组因为移动会减少的最小值差异,和不被移动所能够变成倒数第二小值带来的增量 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, mi = LONG_LONG_MAX; cin >> n; vector<pair<int, int>> data(n); for (int i = 0; i < n; ++i) { int m, tmp; cin >> m; int u = LONG_LONG_MAX, v = LONG_LONG_MAX; for (int j = 0; j < m; ++j) { cin >> tmp; mi = min(mi, tmp); if (tmp < u) { v = u; u = tmp; } else if (tmp < v) v = tmp; } data[i] = {u, v}; } int t = 0; for (int i = 0; i < n; ++i) { int cur = (data[i].second - data[i].first) + (data[i].first - mi); int lst = (data[t].second - data[t].first) + (data[t].first - mi); if (cur < lst) t = i; } int ans = 0; for (int i = 0; i < n; ++i) { if (i == t) ans += mi; else ans += data[i].second; } cout << ans << endl; } } C. Another Permutation Problem 大致题意 一个 $n$ 的排列数组,将每一个值和它的下标(从 $1$ 开始)相乘,然后去掉最大的值之后求和,问在哪种排列下,这个求和的值最大可以是多少,不需要计算出具体的数组,只需要给出结果就行了 思路 因为数据量级很小,所以可以暴力扫 假定当前计算乘积完成后,所有结果数中最大值为 $x$,然后从大到小遍历每一个值,计算出此时每个值下标可以是多少,尽可能取较大的,就可以算出当前情况下的结果是多少 然后暴力扫所有可能的最大值就行了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int ans = 0; auto cal = [&](int mx) { int total = 0, big = 0; set<int> s; for (int i = 1; i <= n; ++i) s.insert(i); for (int i = n; i > 0; --i) { int mi = mx / i; auto iter = s.upper_bound(mi); if (iter == s.begin()) return; iter--; total += *iter * i; big = max(big, *iter * i); s.erase(iter); } ans = max(ans, total - big); }; int mi = ((n + 1) / 2) * ((n + 2) / 2); for (int i = 1; i <= n; ++i) { for (int j = (mi + i - 1) / i; j <= n; ++j) { int maxValue = i * j; cal(maxValue); } } cout << ans << endl; } } D. Andrey and Escape from Capygrad 大致题意 有一条射线,起点是 $0$ 射线上有一些传送门,对于每一个传送门,可以用四个数字表示 $l, r, a, b$,表示如果你在 $[l, r]$ 这个区间内的话,就可以传送到 $[a, b]$ 这个区间的任意一个位置。问从某个点开始,可以使用无限次数传送门,最远可以到哪里 强调,对于任意一个传送门,都满足 $l \leq a \leq b \leq r$ 思路 很明显的是,实际上只有 $[l ,b]$ 这段传送门是有意义的,因为对于 $[b, r]$ 这段,实际上进入这个传送门之后,只能回到更小的值。当然有人可能会担心,是不是存在可以通过回到更小的值,导致可以进入实际上能够传送到更远的传送门了。假定当前传送门为 $l_0, r_0, a_0, b_0$ 另外一个可以进入且更好的传送门为 $l_1, r_1, a_1, b_1$,当前位置为 $x$,根据给出的现象可以得到 $b_0 < b_1$,且 $l_1 \leq b_0$,且 $b_0 < x \leq r_0$,这样才能满足进入之后能够进入到另外一个传送门,且更远。但是根据题意又可以得到 $b_0 \leq r_0$,所以组合上述的表达式就可以得到 $l_1 \leq b_0 \leq x \leq r_0$ 此时 $x$ 明明可以直接进入第二个传送门,压根不需要回来绕一下 明确这一点后,只需要维护所有传送门的 $l, b$ 这两个节点即可 $l$ 表示进入的节点,$b$ 表示出去的节点,做 hash 后按照从大到小的下标遍历,维护当前下标涉及到的传送门,若当前时间内有任何传送门,则进入,否则留在原地,若当前所有传送门都已经结束了,那么还在传送门中的都需要出来 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, q; cin >> n; vector<pair<int, pair<int, int>>> event; event.reserve(4 * n); for (int i = 0; i < n; ++i) { int l, r, a, b; cin >> l >> r >> a >> b; event.push_back({l, {1, i}}); event.push_back({b, {2, i}}); } cin >> q; event.reserve(4 * n + q); for (int i = 0; i < q; ++i) { int tmp; cin >> tmp; event.push_back({tmp, {3, i}}); } sort(event.begin(), event.end()); vector<int> ans(q); stack<int> query; set<int> len; for (auto &item: event) { switch (item.second.first) { case 1: len.insert(item.second.second); break; case 2: len.erase(item.second.second); if (len.empty()) { while (!query.empty()) { ans[query.top()] = item.first; query.pop(); } } break; case 3: if (len.empty()) ans[item.second.second] = item.first; else query.push(item.second.second); break; } } for (auto &item: ans) cout << item << ' '; cout << endl; } }

2023/8/13
articleCard.readMore

Codeforces Round 891 (Div. 3)

A. Array Coloring 大致题意 把一个数组里的值分成两组,让这两组的所有元素求和后,奇偶性一致 思路 只要判定原数组中奇数的个数就行了,奇数个数的奇数就肯定不行 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, ans = 0; cin >> n; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ans += tmp % 2; } cout << (ans % 2 ? "NO" : "YES") << endl; } } B. Maximum Rounding 大致题意 可以对一个数字进行无数次任意位置的四舍五入,问最大值可以是多少 思路 也是比较简单的,只需要从左往右找到第一个 $\geq 5$ 的值,并从此值开始往前一致进位,然后再判断进位后的是否 $\geq 5$,然后无限制的进位即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str; cin >> str; for (int i = 0; i < str.size(); ++i) { str[i] -= '0'; if (str[i] >= 5) { for (int j = i - 1; j >= 0; --j) { if (str[j + 1] >= 5) { str[j]++; str[j + 1] = 0; } else break; } for (int j = i; j < str.size(); ++j) str[j] = 0; break; } } if (str[0] == 5) str[0] = 0; if (str[0] == 0) cout << '1'; for (char i : str) cout << char(i + '0'); cout << endl; } } C. Assembly via Minimums 大致题意 有一个数组长度为 $n$,暂时不知道具体的内容,通过这个数组得到一个新数组,其中的每一项为 $\forall i \in [1, n], \forall j \in [1, n], min(a_i, a_j)$,求出一个可能的原数组 思路 反过来思考,假如一个原数组已经从小到大排序好了,那么通过这个方法会得到 $(n - 1)$ 个 $a_0$,$(n - 2)$ 个 $a_1$,$0$ 个 $a_n$……以此类推,所以按照此规律反推即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, n2; cin >> n; n2 = n * (n - 1) / 2; map<int, int> cnt; for (int i = 0; i < n2; ++i) { int tmp; cin >> tmp; cnt[tmp]++; } int des = n - 1; vector<int> res; for (auto & iter : cnt) { while (iter.second > 0) { iter.second -= des; res.push_back(iter.first); des--; } } for (int re : res) cout << re << ' '; cout << res.back() << endl; } } D. Strong Vertices 大致题意 给出两个数组 $a, b$,对于 $i \in [1, n], j \in [1, n], a_i - a_j \geq b_i - b_j$ 则在一个图中绘制边 $i \rightarrow j$ 的有向边,求问图中存在多少个点,满足这些点可以通过一条或者几条路径到达所有其他节点 思路 这道题迷惑性很强 首先需要变形一下公式,得到 $a_i - b_i \geq a_j - b_j$,这样是否存在边的情况,就直接和当前下标相关了。根据公式容易可以得到,若存在一个节点的 $a_i - b_i \geq max_{j=1}^n(a_j - b_j)$ 的时候,那么就等于直接和所有其他点有边了 另外,通过上述公式还可以明显直到,压根不可能存在需要走两条路径的情况,因为所有点的可达点都必定满足 $a_i - b_i \leq a_j - b_j$($i$ 为当前节点,$j$ 为可以达到的点),故只需要考虑最大的差值项即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data1(n), data2(n); for (int i = 0; i < n; ++i) cin >> data1[i]; for (int i = 0; i < n; ++i) cin >> data2[i]; vector<int> ans; int tmp = LONG_LONG_MIN; for (int i = 0; i < n; ++i) { if (data1[i] - data2[i] > tmp) { tmp = data1[i] - data2[i]; ans.clear(); ans.push_back(i); } else if (data1[i] - data2[i] == tmp) ans.push_back(i); } cout << ans.size() << endl; for (auto i : ans) cout << i + 1 << ' '; cout << endl; } } E. Power of Points 大致题意 有一个数组 $a$ 接下来需要计算一个值 f(x),这个值的是这样计算的: 对于数组中的每一项,求算一个区间 $[x, a_i]$,可以得到 $n$ 个区间 求出所有可能的正整数所命中的区间数量的和 需要求算 $\sum_{i=0}^n f(a_i)$ 思路 排序一下 然后遍历数组,维护从这个值到下一个值,左右区间带来的贡献即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<pair<int, int>> data(n); for (int i = 0; i < n; ++i) cin >> data[i].first; for (int i = 0; i < n; ++i) data[i].second = i; sort(data.begin(), data.end()); int left = 0; int right = 0; for (int i = 0; i < n; ++i) right += abs(data[i].first - data[0].first) + 1; vector<pair<int, int>> res; res.emplace_back(data[0].second, left + right); for (int i = 1; i < n; ++i) { left += 1; right -= 1; int cap = abs(data[i].first - data[i - 1].first); left += cap * i; right -= cap * (n - i); res.emplace_back(data[i].second, left + right); } sort(res.begin(), res.end()); for (auto i : res) cout << i.second << ' '; cout << endl; } } F. Sum and Product 大致题意 有一个数组 $a$,给出 $q$ 次询问,每次询问 $x, y$ 两个值,问有多少对不同的 $i, j$ 满足 $i < j, a_i + a_j = x, a_i \times a_j = y$ 思路 把公式化简了,其实就是二元一次方程求解,实际上很简单 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #define int long long int bf(int x) { int l = 0, r = 1e10 + 10; while (l + 1 < r) { int mid = (l + r) / 2; if (mid * mid == x) return mid; if (mid * mid < x) l = mid; else r = mid; } return l; } void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; map<int, int> mp; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mp[tmp]++; } int q; cin >> q; for (int i = 0; i < q; ++i) { int x, y; cin >> x >> y; int inner = x * x - 4 * y; int outer = bf(inner); if (outer * outer != inner) { cout << 0 << ' '; continue; } if ((x + outer) % 2 != 0) { cout << 0 << ' '; continue; } int l = (x + outer) / 2; int r = (x - outer) / 2; auto lIter = mp.find(l); auto rIter = mp.find(r); if (l == r) { int cnt = lIter == mp.end() ? 0 : lIter->second; cout << cnt * (cnt - 1) / 2 << ' '; } else cout << (lIter == mp.end() ? 0 : lIter->second) * (rIter == mp.end() ? 0 : rIter->second) << ' '; } cout << endl; } } G. Counting Graphs 大致题意 这道题还是很不错的~ 给出一颗树,树的边有权重 问在保证任意边权重不超过 $S$ 的情况下,有多少种不同的图,满足它的最小生成树一定是给出的树 思路 最小生成树很容易让人想到是否和边排序后 + 并查集操作有关 我们需要往图里加入一些无意义的边,比如权值大于树上最大权值的情况下,无论怎么加都是合理的 而核心需要考虑的就是,当使用的边权值小于等于树上的最大权值的情况下,还能加到哪里 回到这里提到的第二段:无意义的边。如果我们提取出这个生成树的子树,对于每一个子树,是否都可以使用这一条,即使这个权值小于树上的最大权值,但是只要它大于子树本身的最大权值即可 如此“分治”,只需要根据权值从小到大排序边,然后一条条加入图中,然后通过并查集来确认新多了多少条可以加的边,然后再考虑上可以加的权值种类,得到解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #define int long long void solve() { int mod = 998244353; struct node { int u, v, c; }; int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, s; cin >> n >> s; vector<node> data(n - 1); for (int i = 0; i < n - 1; ++i) cin >> data[i].u >> data[i].v >> data[i].c; sort(data.begin(), data.end(), [&](node a, node b) { return a.c < b.c; }); vector<int> fa(n + 1); vector<int> cnt(n + 1); for (int i = 0; i < fa.size(); ++i) { fa[i] = i; cnt[i] = 1; } function<int(int)> find = [&](int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }; function<int(int, int)> join = [&](int x, int y) { int fx = find(x), fy = find(y); if (fx == fy) return 0ll; fa[fx] = fy; int res = (cnt[fx] * cnt[fy] - 1); cnt[fy] += cnt[fx]; return res; }; function<int(int, int)> pow = [&](int n, int p) { int ans = 1, buff = n; while (p) { if (p & 1) ans = (ans * buff) % mod; buff = (buff * buff) % mod; p >>= 1; } return ans; }; int ans = 1; for (int i = 0; i < n - 1; ++i) { int cCap = s - data[i].c, nC = join(data[i].u, data[i].v); ans *= pow(cCap + 1, nC); ans %= mod; } cout << ans << endl; } }

2023/8/12
articleCard.readMore

Codeforces Round 890 (Div. 2)

A. Tales of a Sort 大致题意 有个数列,每次操作可以将所有值减少 $1$,除非它已经是 $0$ 了,问需要多少次操作,才能将整个数列变成非递减数列 思路 很明显的一点,如果发现一对不满足条件的相邻对,即 $a_i > a_{i + 1}$,如果不把他们都减少到 $0$ 的情况下,永远无法满足题目条件,故只需要找到不满足的对,然后取最大的那个值即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, last = 0, mx = 0, tmp; bool ans = false; cin >> n; for (int i = 0; i < n; ++i) { cin >> tmp; if (tmp < last) mx = max(mx, last); else last = tmp; } cout << mx << endl; } } B. Good Arrays 大致题意 题目给出一个数组 $a$,要你判定是否存在另外一个数组 $b$,满足 $\sum_{i=1}^n a_i = \sum_{i=1}^n b_i, \forall i \in [1, n], a_i \neq b_i, a_i > 0, b_i > 0$ 思路 读题易得:若原来的值是 $1$,那么必须找别的值借 $1$ 才能保证 $a_i \neq b_i$,而其他值则都可以简单变成 $1$ 解决。故只需要计算有多少可以冗余调配的值即可 需要特判一下只有一个值的情况 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, cnt = 0, sum = 0; cin >> n; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; cnt += tmp == 1; sum += tmp; } cout << (cnt + n > sum || n == 1 ? "NO" : "YES") << endl; } } C. To Become Max 大致题意 有一个初始数组 $a$,每次可以选择一个 $i \in [1, n - 1]$,若 $a[i] \leq a[i + 1]$ 则使得 $a[i] = a[i] + 1$,问在最多操作 $k$ 次的情况下,数组的最大值可以为多少 思路 注意到数据量,$n$ 仅有 $1000$,意味着复杂度可以达到 $n^2 log(1e9)$ 的级别,然后再做思考 按照公式,那么最终得到的数组,必定存在一个恰好递减的阶梯。另外很明显的是,数组的最后一个值必定不可动,那就意味着实际上最大值的可行性是被最后一个值限定的,最大值为 $a_n + n - 1$ 由于复杂度有非常大的冗余,故可以作出如下的暴力搜索 遍历所有可能为最大值的下标 $i$ 二分查找最终的最大值 尝试构建最大值的可能性,是否能够在 $k$ 消耗内,完成构建一个阶梯 这样下来恰好复杂度满足预期 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; int ans = 0; for (int i = 0; i < n; ++i) ans = max(ans, data[i]); for (int l = 0; l < n; ++l) { int b = ans, e = ans + k + 1; while (b + 1 < e) { int mid = (b + e) / 2; int cost = 0; bool keyPoint = false; for (int i = l; i < n && !keyPoint && cost <= k; ++i) { if (data[i] >= mid - (i - l)) keyPoint = true; else cost += mid - data[i] - (i - l); } if (keyPoint && cost <= k) b = mid; else e = mid; } ans = max(ans, b); } cout << ans << endl; } } D. More Wrong 大致题意 交互题 有一个 $n$ 的排列 $a$,只知道长度 $n$,每次可以询问 $[l, r]$ 区间下,逆序对数量,每次询问的代价是 $(r - l)^2$,问如何在 $5 \times n^2$ 的代价下,找到最大值的下标 思路 区间逆序对,很容易想到归并排序 先得到两个简单的结论: 若已知一个区间的逆序对数量,再最后加入一个元素,且没有改变逆序对数量,那么这个加入的元素必定是当前区间的最大值 若已知一个区间的逆序对数量,再最前面加入一个元素,且增加了恰好等于原来元素个数的逆序对,则新加入的元素必定是当前区间的最大值 这两个结论显而易见,就不再解释 从归并排序的视角看,我们假定找到了一个区间前半部分的最大值的下标,又找到了后半部分最大值的下标,那么需要判断这两个值谁更大的时候,就可以通过上面的定律来判定,只需要两次查询即可 如此递归下去,可以得到最终的查询费用为 $$\begin{cases}& 2(n - 1)^2 + 2 \times 2(0.5n - 1)^2 + 2 \times 4(0.25n - 1)^2 + \dots \\\leq & 2n^2 + 4(0.5n)^2 + 8(0.25n)^2 + \dots \\= & 2(n^2 + 0.5n^2 + 0.25n^2 + \dots) \leq 4n^2 \leq 5n^2\end{cases}$$ 可以证得这个方法的消耗低于要求 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 map<pair<int, int>, int> m; int interactive(int l, int r) { auto iter = m.find({l, r}); if (iter != m.end()) { return iter->second; } int temp; cout << "? " << l << ' ' << r << endl; cout.flush(); cin >> temp; m.insert({{l, r}, temp}); return temp; } int dfs(int l, int r) { if (l == r) { return l; } if (l + 1 == r) { return interactive(l, r) == 1 ? l : r; } if (l + 2 == r) { int lm = dfs(l, l + 1); if (lm == l) { return interactive(l, r) == 1 ? r : l; } else return dfs(lm, r); } int mid = (l + r) >> 1; int lm = dfs(l, mid); int rm = dfs(mid + 1, r); if (lm + 2 >= rm) return dfs(lm, rm); int t1 = interactive(lm + 1, rm - 1); int t2 = interactive(lm, rm); return t2 >= t1 + rm - lm ? lm : rm; } void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { m.clear(); int n; cin >> n; int ans = dfs(1, n); cout << "! " << ans << endl; } } E1. PermuTree (easy version) 大致题意 有一个树和一个 $n$ 的排列 $a$,求出使得满足 $a_u < a_{lca(u, v)} < a_v$ 这个等式的最多的排列下,满足多少次 思路 在树上并没有 $u, v$ 之分,实际上可以相互对掉,所以这棵树实际上需要尽可能满足二叉搜索树的结构才行,即每个节点下,左边的值都小于当前节点,右边的值都大于当前节点。 但是这不一定是一颗二叉树,而是多叉树,而在满足上述等式的情况下,则需要人为的将所有子节点划分为两份,一份大于一份小于。即,假如一个节点有 $3$ 个直接子节点,那么必定存在有两个直接的子节点的下的所有值都小于当前节点,同时另外一个直接子节点下的所有值都要大于此节点,那么最终的满足等式的量级为 $(cnt_1 + cnt_2) \times cnt_3$ 而又因为划分的时候,总共的子节点数量之和是确定的,故需要尽可能对半分,那么就需要背包运算 而这又是树结构,所以只需要在树上 dp 上做背包 dp 即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 struct node { int v, nxt; }; vector<node> edge(5010); vector<int> head(5010); int dp(vector<int> &pack) { if (pack.size() == 0) { return 0; } if (pack.size() == 1) { return 0; } if (pack.size() == 2) { return pack[0] * pack[1]; } int sum = 0; for (int i : pack) sum += i; vector<int> dp(sum / 2 + 1, 0); for (int i : pack) for (int w = sum / 2; w >= i; --w) dp[w] = max(dp[w], dp[w - i] + i); int left = 0; for (auto i : dp) left = max(left, i); return left * (sum - left); } int tree(int index, int &cal) { int res = 1; vector<int> temp; for (int i = head[index]; i != -1; i = edge[i].nxt) { temp.push_back(tree(edge[i].v, cal)); } cal += dp(temp); for (int i : temp) res += i; return res; } void solve() { int n; cin >> n; for (int i = 0; i <= n; ++i) head[i] = -1; for (int i = 1; i < n; ++i) { int tmp; cin >> tmp; edge[i] = {i + 1, head[tmp]}; head[tmp] = i; } int ans = 0; tree(1, ans); cout << ans << endl; }

2023/8/12
articleCard.readMore

Educational Codeforces Round#152 (Div. 2)

A. Morning Sandwich 大致题意 有 $a$ 个面包片,$b$ 个奶酪片,$c$ 个火腿片,要保证奶酪片和火腿片必须被两块面包片直接夹,问最多可以造多大的汉堡 思路 很简单,就不讲了 AC code 1 2 3 4 5 6 7 8 9 10 void solve() { int _; cin >> _; for (int tss = 0; tss < _; ++tss) { int a, b, c; cin >> a >> b >> c; int m = min(a - 1, b + c); cout << m * 2 + 1 << endl; } } B. Monsters 大致题意 有 $n$ 个怪物,每个怪物有一定生命值 $a[i]$,每次攻击一定是对着生命值最高的怪物,并减少其生命值 $k$,若有多个则选择最左边的,问怪物的死亡顺序 思路 当有任何怪物的生命值大于 $k$ 的时候,那就意味着不会有怪物死亡,所以可以先对所有怪物进行 mod k 操作 此时,就会有怪物出现死亡,因为会恰好降低到 $0$,需要先输出为 $0$ 的怪物,从左往右输出即可 后面,再根据剩余生命值从大到小,从左往右输出即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void solve() { int _; cin >> _; for (int tss = 0; tss < _; ++tss) { int n, k; cin >> n >> k; map<int, vector<int>> res; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; tmp %= k; res[tmp].push_back(i); } auto zero = res[0]; for (auto i : zero) cout << i + 1 << ' '; for (auto iter = res.rbegin(); iter != res.rend(); ++iter) { if (iter->first == 0) continue; for (auto i : iter->second) cout << i + 1 << ' '; } cout << endl; } } C. Binary String Copying 这题老坑人了QAQ 大致题意 有一个初始字符串 $s$,仅有 0 和 1 组成。 有 $k$ 次操作,每次操作都是在这个初始字符串上进行的。每次操作选择一个区间 $[l, r]$,并使得这个区间内的值进行排序,即 $0101010 \rightarrow 0000111$ 问,得到的这 $k$ 个字符串,有多少种不同的 思路 有点蒙,但还是得找找思路 考虑到字符串种类,第一反应是做字符串 hash,这非常简单,因为字符串仅有 0 和 1,为了方便,如果当前位为 1 则取这一位的权制,否则不取,这样就有点类似 $mod$ 进制数了 接下来是如何计算 $k$ 个字符串的 hash 值,根据题目给出的数据量,看起来必须在 O(sqrt(n)) 时间之内完成。我们可以发现,每次操作后实际上有三个区间 $[1, l)$ 和原来的 hash 值相同 $[l, r]$ 不知道! $(r, n]$ 和原来的 hash 值相同 即然某段区间存在和之前 hash 值相同的情况,那么可以通过对 hash 值前缀和的方式,使得能够快速求出任意区间的 hash 值,那么就只剩下中间的区间了 有意思的是,排序后的值恰好是一个前面为 0 后面为 1 的字符串,所以计算单独计算这段区间内的 hash 结果非常简单,只需要有一个每一位的 hash 值的前缀和,并且知道 1 从哪一位开始即可,因为最后一个一定也是 1(当然需要排除一下压根没有 1 的情况) 而需要计算某个区间内的 1 的数量,好像又可以做一次前缀和 最终,只需要三个前缀和就可以简单解决问题了 但是试了两个 hash 值都不通过,一气之下直接同时计算两个 hash 值,然后就过了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 void solve() { int _; cin >> _; vector<int> pw1(2e5 + 10); vector<int> pw2(2e5 + 10); int tmp1 = 1; int tmp2 = 1; int mod = 998244353; for (int i = 1; i < pw1.size(); ++i) { pw1[i] = (pw1[i - 1] + tmp1) % mod; tmp1 *= 131; tmp1 %= mod; pw2[i] = (pw2[i - 1] + tmp2) % mod; tmp2 *= 1331; tmp2 %= mod; } for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> a(n + 1), b1(n + 1), b2(n + 1); string str(n, '0'); cin >> str; tmp1 = 1; tmp2 = 1; for (int i = 0; i < n; ++i) { a[i + 1] = a[i] + str[i] - '0'; b1[i + 1] = (b1[i] + (str[i] == '1' ? tmp1 : 0)) % mod; tmp1 *= 131; tmp1 %= mod; a[i + 1] = a[i] + str[i] - '0'; b2[i + 1] = (b2[i] + (str[i] == '1' ? tmp2 : 0)) % mod; tmp2 *= 1331; tmp2 %= mod; } set<pair<int, int>> res; for (int i = 0; i < k; ++i) { int l, r; cin >> l >> r; int ls, rs; { int left = b1[l - 1] % mod; int right = (b1[n] - b1[r] + mod) % mod; int one = a[r] - a[l - 1]; int zero = r - l + 1 - one; int from = l + zero, to = r; int mid = (pw1[to] - pw1[from - 1] + mod) % mod; ls = (left + right + mid) % mod; } { int left = b2[l - 1] % mod; int right = (b2[n] - b2[r] + mod) % mod; int one = a[r] - a[l - 1]; int zero = r - l + 1 - one; int from = l + zero, to = r; int mid = (pw2[to] - pw2[from - 1] + mod) % mod; rs = (left + right + mid) % mod; } res.insert({ls, rs}); } cout << res.size() << endl; } } D. Array Painting 大致题意 有一段数组 $a$,每个值必定是 $0, 1, 2$ 其中一个,开始的时候所有值都是蓝色的,你可以进行如下操作 选择一个蓝色的值,把它变成红色,同时支付一块钱 选择一个红色的值,若其大于 $0$,则将其减少 $1$,并将一个相邻它的值改为红色 问,你最少需要支付多少钱才能把所有值变成红色 大致思路 容易明白的是,无论如何先随便找个 $2$ 染色的一定不亏,然后这个 $2$ 就会向两侧染色,直到遇到 $0$。那么这样处理完成后,再找下一个没有被染色的 $2$,直到 $2$ 被全部耗尽 然后是 $0, 1$ 的问题了,很明显的是,在一段连续没有被染色的区间下,可以分类讨论 如果只有 $1$,那么恭喜,染色一个端点,就可以染色全部 如果只有 $0$,那么就得一个个染色 如果同时有 $1$ 和 $0$ 因为每一段连续的 $1$ 可以附带染色一个相邻的 $0$,实际上操作几个 $1$ 等于也染色了几个 $0$,所以最终结果一般还是得跟 $0$ 数量相同 但是从第一条种可以得到,因为一段 $1$ 就得花费一次染色,不能只看 $0$ 的数量,例如 $101$ 这种情况下,虽然只有一个 $0$,但是 $1$ 有两端,则至少需要染色 $2$ 次(实际上就是因为第二段 $1$ 不会染色任何其他 $0$) 所以结论为,每一段没有染色的区间,需要 $max(cnt_0, cnt_part1)$ 的花费 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void solve() { int n; cin >> n; vector<int> data(n); vector<bool> vis(n, false); int res = 0; for (int i = 0; i < n; ++i) cin >> data[i]; // 先处理 2 的情况 for (int i = 0; i < n; ++i) { if (vis[i] || data[i] != 2) continue; res++; vis[i] = true; for (int j = i - 1; j >= 0; --j) { if (!vis[j]) { vis[j] = true; if (data[j] == 0) break; } else break; } for (; i < n; ++i) { vis[i] = true; if (data[i] == 0) break; } } // 再处理 1 的情况 int last = -1; int zero = 0; int oneP = 0; for (int i = 0; i < n; ++i) { if (vis[i]) { res += max(oneP, zero); zero = 0; oneP = 0; last = -1; } else if (data[i] == 0) { zero++; last = 0; } else if (last != 1) { last = 1; oneP++; } } res += max(oneP, zero); cout << res << endl; }

2023/8/6
articleCard.readMore

Codeforces Round#889 (Div. 2)

退役入职后的首次刷题,确实感觉对题目的敏感度下降了很多 A. Dalton the Teacher 题面概要 有一个长度为 $n$ 的打乱序列 $a$,每次操作可以将 $a[i]$ 和 $a[j]$ 进行交换位置,问至少多少次操作才能让满足 $\forall i \in [1, n], i \ne a[i]$ 思考 因为每次操作会影响到两个位置的值,而本身已经满足条件的可以不用操作,故结果就是 $\left \lceil x / 2 \right \rceil $ 其中 $x$ 为仍然不满足条件的位置数量 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int cnt = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp == i + 1) cnt++; } cout << (cnt + 1) / 2 << endl; } } B. Longest Divisors Interval 题面概要 一个大的整数 $n$,找到一个区间 $[l, r]$,满足 $\forall x \in [l, r], n \space mod \space x = 0$,求出最大的 $r - l + 1$ 思考 因为一连串的数字,很容易产生很多大量的互质数,导致如果要满足条件的话,则需要非常非常大的值。因此,考虑互质数出现频率最低的区间,即从 $1$ 开始的区间,可以用测试代码得到,$[1, 100]$ 的情况下,目标值最小为 $9223372036854775807$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void solve() { map<int, int> cnt; for (int i = 1; i < 100; ++i) { for (int j = 1; j <= i / 2; ++j) { if (i % j == 0) { cnt[j]++; } } } int res = 1; for (auto item : cnt) { res *= pow(item.first, item.second); } cout << res << endl; } 所以最大区间长度不可能超过 100 我们假定此区间为 $[l, r]$,满足 $r - l + 1 = n$,那么我们容易得到,$\forall i \in [1, n], \exists j \in [l, r], j % i == 0$ 而这个公式说明了,假定存在 $[l, r], l \ne 1$ 满足 $\forall x \in [l, r], n mod x = 0$上述条件,那么同时必然满足 $\forall x \in [1, r - l + 1], n mod x = 0$ 所以我们只需要考虑从 $1$ 开始的区间内,最大能够满足到哪个值即可,此时得到的一定是最优解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int cnt = 0; for (int i = 1; i <= 100; ++i) { if (n % i == 0) { cnt++; } else { break; } } cout << cnt << endl; } } C2. Dual (Hard Version) 题意完全一致,就直接做 Hard Version 了 题面概要 有 $n (n \leq 20)$ 个值的序列 $a$,每个值都在 $[-20, 20]$ 内,每次操作可以选择任意两个值,进行 $a[i] += a[j], i \in [1, n], j \in [1, n]$。求出一种可行的解,最多进行 31 次操作,使得整个序列不存在非递减对,即满足 $\forall i \in [1, n - 1], a[i] \leq a[i + 1]$ 思路 最简单的方式就是将所有值变成非负数,做一次前缀和,或者所有值变成非正数,然后做一次后缀和。两种方案思路是完全相同的,最差情况下都需要 $n - 1$ 步操作 接下来就是需要考虑如何将所有值变成非负数(非正数,下同,后续只强调非负数) 如果需要将所有值变成非负数,前提是必须要有一个正数(不考虑都是 0 的情况,可以特判解决),需要在这个前提下考虑才有意义 那么最简单的方式就是将最大的正数通过累加的方式实现幂次放大,直到和最小值相加也能满足大于等于 $0$ 即可。此时可以得到最终需要的操作数:$n - 1 + \left \lceil log_2 \frac{abs(maxValue)}{abs(minValue)} \right \rceil + cnt_-$(其中的 $cnt_-$ 表示负数个数) 同样,可以得到非正数需要的操作数 $n-1+\left \lceil log_2 \frac{abs(minValue)}{abs(maxValue)} \right \rceil + cnt_+$(其中的 $cnt_+$ 表示负数个数) 最终,我们可以取其中的较小值,得到公式 $$\begin{align}= & min(n - 1 + \left \lceil log_2 \frac{abs(maxValue)}{abs(minValue)} \right \rceil + cnt_-, n - 1 + \left \lceil log_2 \frac{abs(minValue)}{abs(maxValue)} \right \rceil + cnt_+) \\\leq & n - 1 + min(\left \lceil log_2 \frac{abs(maxValue)}{abs(minValue)} \right \rceil + cnt_-, \left \lceil log_2 \frac{abs(minValue)}{abs(maxValue)} \right \rceil + n - cnt_-) \\= & n - 1 + min(\left \lceil log_2 abs(maxValue) - log_2 abs(minValue) \right \rceil + cnt_-, \left \lceil log_2 abs(minValue) - log_2 abs(maxValue) \right \rceil + n - cnt_-) \\= & \begin{cases}n - 1 + min(\left \lceil log_2 abs(maxValue) - log_2 abs(minValue) \right \rceil + cnt_-, 0 + n - cnt_-), abs(maxValue) >= abs(minValue) \\n - 1 + min(0 + cnt_-, \left \lceil log_2 abs(minValue) - log_2 abs(maxValue) \right \rceil + n - cnt_-), abs(maxValue) 20) \\\leq & \begin{cases}20 - 1 + min(5 + cnt_-, 20 - cnt_-) \\20 - 1 + min(cnt_-, 5 + 20 - cnt_-)\end{cases} \\= & \begin{cases}19 + min(5 + cnt_-, 20 - cnt_-) \\19 + min(cnt_-, 25 - cnt_-)\end{cases} \\\leq & \begin{cases}19 + 12 \\19 + 12\end{cases} \because \left \lceil cnt_- \right \rceil = cnt_- \\= & 31\end{align}$$ 求的上述方法最大操作次数一定满足要求 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; int maxIndex = 0; int minIndex = 0; int maxValue = data[0]; int minValue = data[0]; for (int i = 0; i < n; ++i) { if (maxValue <= data[i]) { maxValue = data[i]; maxIndex = i; } if (minValue > data[i]) { minValue = data[i]; minIndex = i; } } if (maxValue == minValue) { cout << 0 << endl; continue; } if (maxValue > 0) { vector<int> copy = data; vector<pair<signed, signed>> op; while (maxValue + minValue < 0) { copy[maxIndex] += maxValue; maxValue += maxValue; op.emplace_back(maxIndex, maxIndex); } for (int i = 0; i < copy.size(); ++i) { if (copy[i] < 0) { copy[i] += maxValue; op.emplace_back(i, maxIndex); } } for (int i = 1; i < copy.size(); ++i) { if (copy[i] < copy[i - 1]) { copy[i] += copy[i - 1]; op.emplace_back(i, i - 1); } } if (op.size() <= 31) { cout << op.size() << endl; for (auto o : op) { cout << o.first + 1 << ' ' << o.second + 1 << endl; } continue; } } if (minValue < 0) { vector<int> copy = data; vector<pair<signed, signed>> op; while (maxValue + minValue > 0) { copy[minIndex] += minValue; minValue += minValue; op.emplace_back(minIndex, minIndex); } for (int i = 0; i < copy.size(); ++i) { if (copy[i] > 0) { copy[i] += minValue; op.emplace_back(i, minIndex); } } for (int i = (int)copy.size() - 2; i >= 0; --i) { if (copy[i] > copy[i + 1]) { copy[i] += copy[i + 1]; op.emplace_back(i, i + 1); } } if (op.size() <= 31) { cout << op.size() << endl; for (auto o : op) { cout << o.first + 1 << ' ' << o.second + 1 << endl; } continue; } } } } D. Earn or Unlock 大致题意 有一个数组 $a$,长度为 $n$,其中所有数字都是锁定状态,仅第一个(最左边的)是解锁状态。 你必须从左往右遍历这个数组,然后对于当前的值,如果是锁定状态,则结束,否则你可以选择 获得等同于当前值的分数 从左往右依次解锁 m 张锁定状态的值,m 为当前值,直到所有值都被解锁 思路 假如最终解锁了 x 个值(数组长度无限的情况下考虑)那么最终的分数为 $prefixSum(x) - x + 1$,因为有 $x - 1$ 的分数被用来解锁了 可以发现,如果能够明确最终解锁的位置,那么就可以确定分数,而并不需要去关注到底哪些值用来解锁了,哪些值用来记分了 考虑 dp 的方式解决,定义 dp[i] 表示从第一位到第 $i$ 位时,能够解锁 dp[i] 内的所有位置的集合,那么可以得到状态转移公式 $dp[i + 1] = dp[i] \cup (dp[i] + a[i]) - i$,即下一个位置能够解锁到的位置就是当前能够解锁到的位置并如果当前值作为解锁使用能够到达的位置,再去除掉当前位置,毕竟不能回头走 由于可以解锁过头,为了方便计算,故需要 dp 的范围是 $max(a[i]) + n$ 用 bitset 维护最终可以解锁到的位置即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void solve() { const int N = 2e5 + 10; int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; bitset<N> bit; bit.set(0, true); int res = 0; int prefix = 0; for (int i = 0; i < n * 2; ++i) { if (i < n) { prefix += data[i]; } if (bit.test(i)) { res = max(res, prefix - i); } if (i < n) { bit |= bit << data[i]; bit.set(i, false); } } cout << res << endl; }

2023/8/5
articleCard.readMore

Java Script 的 null 和 undefined 随想

有些时候感觉一些语言里看起来很蠢的设计,实际上却能解决一些很有意思的场景。比如 JavaScript 的 null 和 undefined,虽然看起来都是表示空的意思,但是实际上却解决了“没有这个值”,“这个值为空”这样两种语义。在缓存穿透的问题上,如果 redis、memcached 等数据库也有这样一层设计等话,是不是就能解决 null 穿透问题了呢

2022/8/30
articleCard.readMore

记一次 SQL LEFT JOIN 没有得到预期结果的错误

最近在业务中做数据开发的时候,写了一个 SQL 但是没有得到预期的结果,大致如下 1 2 3 4 5 6 7 8 9 10 表 a +----+------+-----+ | id | name | tid | +----+------+-----+ | 1 | aaa | 101 | +----+------+-----+ | 2 | bbb | 102 | +----+------+-----+ | 3 | ccc | 103 | +----+------+-----+ 1 2 3 4 5 6 7 8 9 10 表 b +------+------+-------+ | id | nick | type | +------+------+-------+ | 1001 | abc | false | +------+------+-------+ | 1002 | edf | true | +------+------+-------+ | 1003 | xyz | true | +------+------+-------+ 然后圈选的 SQL 的为 1 2 3 4 5 6 7 8 9 10 11 12 SELECT a.name b.nick FROM a LEFT JOIN b ON a.tid = b.id WHERE b.type = "true" ; 本意上,通过 LEFT JOIN ,即使没有找到,也应该正常返回数据,但是实际上没有返回任何数据 因为 WHERE 条件是在 JOIN 之后发生的,所以实际上,因为 LEFT JOIN 拿不到数据,所以所有列的 b.type 都是 NULL,当然就不是 true 此时可以拆分这两个条件,例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @true_b := SELECT id, nick FROM b WHERE type = "true" ; SELCT a.name c.nick FROM a LEFT JOIN @true_b c ON a.tid = c.id

2022/5/29
articleCard.readMore

Codeforces Round#789(Div. 2)

B2. Tokitsukaze and Good 01-String (hard version) 大致题意 有一段 01 组成的字符串,保证长度为偶数 你可以选择一个 0 或者 1,将其变为 1 或者 0 问至少需要操作几次,可以使得所有的 0 或者 1 段都为偶数长度。同时,此时,最少有多少段单独段 0 或 1 段 分析 首先,因为总长度为偶数,所以奇数段一定是成对出现的,可以简单讨论五种情况 改变一个奇数段内部,可以生成两个偶数段一个奇数段 改变一个偶数段内部,可以生成两个奇数段和一个偶数段 改变两个偶数段边缘,可以生成两个奇数段 改变两个奇数段边缘,可以生成两个偶数段 改变奇偶段边缘,可以交换奇偶关系 这几种方法中,只有改变两个奇数段边缘是有意义的,但是并不一定每次都那么好运。所以必须选择一种方法去将两个离得很远的奇数段靠近 明显只有第一个和最后一个可选,在不产生新的奇数段的前提下改变位置。但是第一个明显有点蠢……因为生成的奇数段在原奇数段内部(仅一个 0 或者 1),所以只能选最后一种方案 所以我们需要选择两个奇数段,然后通过方法五将它们贴近到相邻,然后再用方法四消灭它们,所需要的数量也就是奇数段之间的偶数段个数 + 1 数量解决了,接下来就是分配如何变化使得数量最少了。因为对于每一个奇数段而言,只会改变一个,而对于偶数段而言,两侧边缘都需要发生变化,所以 当奇数段的长度为 1 的时候,变化此奇数段,当偶数段长度为 2 的时候,左右两侧都变化此偶数段。然后再统计不同的奇偶段数量即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include "bits/stdc++.h" using namespace std; #define int long long void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; string str; str.resize(n); cin >> str; vector<int> st; char last = -1; for (int i = 0; i < n; ++i) { if (str[i] == last) { st.back()++; } else { st.push_back(1); last = str[i]; } } int isOdd = 0, ans = 0; for (int i = 0; i < st.size(); ++i) { if (st[i] % 2) { isOdd = !isOdd; if (st[i] == 1) st[i] = 0; } else if (isOdd) { if (st[i] == 2) st[i] = 0; } ans += isOdd; } int ls = -1, cnt = 0; for (int i = 0; i < st.size(); ++i) { if (st[i] == 0) continue; if (ls != (i % 2)) { ls = i % 2; cnt++; } } cout << ans << ' ' << max(1LL, cnt) << endl; } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); signed localTestCount = 1, localReadPos = (signed) cin.tellg(); char localTryReadChar; do { if (localTestCount > 20) throw runtime_error("Check the std input!!!"); auto startClockForDebug = clock(); solve(); auto endClockForDebug = clock(); cerr << "Test " << localTestCount++ << " Run Time: " << double(endClockForDebug - startClockForDebug) / CLOCKS_PER_SEC << "s" << endl; // cout << "Test " << localTestCount << " successful" << endl; // cout << "--------------------------------------------------" << endl; } while (localReadPos != cin.tellg() && cin >> localTryReadChar && localTryReadChar != '$' && cin.putback(localTryReadChar)); #else solve(); #endif return 0; }

2022/5/28
articleCard.readMore

GCC/G++ 预编译头性能优化

最近一直在搞 OJ,为了一个高效的、安全的沙盒编译环境操碎了心,终于实现了一个安全的且对运行影响非常低的沙盒,但是发现程序的编译效率非常的低。 最后通过查阅各种的博客,终于发现了一个非常高效的解决方案 万能头文件问题 在 OJ 的任务中,很多 ACMer 为了方便起见,经常使用万能头文件 bits/stdc++.h。 当然这个头文件之前也惹过一次麻烦,就是著名的银川线上赛的 5 元学生机 OJ 事件,使用了 bits/stdc++.h 的编译效率非常低,因为这个头文件本身包含了太多的东西,罗列如下 (推荐直接点目录跳转到下一章) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 . /usr/include/x86_64-linux-gnu/c++/7/bits/stdc++.h .. /usr/include/c++/7/cassert ... /usr/include/x86_64-linux-gnu/c++/7/bits/c++config.h .... /usr/include/x86_64-linux-gnu/c++/7/bits/os_defines.h ..... /usr/include/features.h ...... /usr/include/x86_64-linux-gnu/sys/cdefs.h ....... /usr/include/x86_64-linux-gnu/bits/wordsize.h ....... /usr/include/x86_64-linux-gnu/bits/long-double.h ...... /usr/include/x86_64-linux-gnu/gnu/stubs.h ....... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h .... /usr/include/x86_64-linux-gnu/c++/7/bits/cpu_defines.h ... /usr/include/assert.h .. /usr/include/c++/7/cctype ... /usr/include/ctype.h .... /usr/include/x86_64-linux-gnu/bits/types.h ..... /usr/include/x86_64-linux-gnu/bits/wordsize.h ..... /usr/include/x86_64-linux-gnu/bits/typesizes.h .... /usr/include/endian.h ..... /usr/include/x86_64-linux-gnu/bits/endian.h ..... /usr/include/x86_64-linux-gnu/bits/byteswap.h ...... /usr/include/x86_64-linux-gnu/bits/wordsize.h ...... /usr/include/x86_64-linux-gnu/bits/byteswap-16.h ..... /usr/include/x86_64-linux-gnu/bits/uintn-identity.h .... /usr/include/x86_64-linux-gnu/bits/types/locale_t.h ..... /usr/include/x86_64-linux-gnu/bits/types/__locale_t.h .. /usr/include/c++/7/cerrno ... /usr/include/errno.h .... /usr/include/x86_64-linux-gnu/bits/errno.h ..... /usr/include/linux/errno.h ...... /usr/include/x86_64-linux-gnu/asm/errno.h ....... /usr/include/asm-generic/errno.h ........ /usr/include/asm-generic/errno-base.h .. /usr/include/c++/7/cfloat ... /usr/lib/gcc/x86_64-linux-gnu/7/include/float.h .. /usr/include/c++/7/ciso646 .. /usr/include/c++/7/climits ... /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/limits.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/syslimits.h ..... /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/limits.h ...... /usr/include/limits.h ....... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h ....... /usr/include/x86_64-linux-gnu/bits/posix1_lim.h ........ /usr/include/x86_64-linux-gnu/bits/local_lim.h ......... /usr/include/linux/limits.h ....... /usr/include/x86_64-linux-gnu/bits/posix2_lim.h ....... /usr/include/x86_64-linux-gnu/bits/xopen_lim.h ........ /usr/include/x86_64-linux-gnu/bits/uio_lim.h .. /usr/include/c++/7/clocale ... /usr/include/locale.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/x86_64-linux-gnu/bits/locale.h .. /usr/include/c++/7/cmath ... /usr/include/c++/7/bits/cpp_type_traits.h ... /usr/include/c++/7/ext/type_traits.h ... /usr/include/math.h .... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h .... /usr/include/x86_64-linux-gnu/bits/math-vector.h ..... /usr/include/x86_64-linux-gnu/bits/libm-simd-decl-stubs.h .... /usr/include/x86_64-linux-gnu/bits/floatn.h ..... /usr/include/x86_64-linux-gnu/bits/floatn-common.h ...... /usr/include/x86_64-linux-gnu/bits/long-double.h .... /usr/include/x86_64-linux-gnu/bits/flt-eval-method.h .... /usr/include/x86_64-linux-gnu/bits/fp-logb.h .... /usr/include/x86_64-linux-gnu/bits/fp-fast.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls-helper-functions.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls-helper-functions.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls-helper-functions.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls-helper-functions.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/mathcalls.h .... /usr/include/x86_64-linux-gnu/bits/iscanonical.h ... /usr/include/c++/7/bits/std_abs.h .... /usr/include/stdlib.h ..... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h ..... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ..... /usr/include/x86_64-linux-gnu/bits/waitflags.h ..... /usr/include/x86_64-linux-gnu/bits/waitstatus.h ..... /usr/include/x86_64-linux-gnu/sys/types.h ...... /usr/include/x86_64-linux-gnu/bits/types/clock_t.h ...... /usr/include/x86_64-linux-gnu/bits/types/clockid_t.h ...... /usr/include/x86_64-linux-gnu/bits/types/time_t.h ...... /usr/include/x86_64-linux-gnu/bits/types/timer_t.h ...... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ...... /usr/include/x86_64-linux-gnu/bits/stdint-intn.h ...... /usr/include/x86_64-linux-gnu/sys/select.h ....... /usr/include/x86_64-linux-gnu/bits/select.h ........ /usr/include/x86_64-linux-gnu/bits/wordsize.h ....... /usr/include/x86_64-linux-gnu/bits/types/sigset_t.h ........ /usr/include/x86_64-linux-gnu/bits/types/__sigset_t.h ....... /usr/include/x86_64-linux-gnu/bits/types/struct_timeval.h ....... /usr/include/x86_64-linux-gnu/bits/types/struct_timespec.h ...... /usr/include/x86_64-linux-gnu/sys/sysmacros.h ....... /usr/include/x86_64-linux-gnu/bits/sysmacros.h ...... /usr/include/x86_64-linux-gnu/bits/pthreadtypes.h ....... /usr/include/x86_64-linux-gnu/bits/thread-shared-types.h ........ /usr/include/x86_64-linux-gnu/bits/pthreadtypes-arch.h ......... /usr/include/x86_64-linux-gnu/bits/wordsize.h ..... /usr/include/alloca.h ...... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ..... /usr/include/x86_64-linux-gnu/bits/stdlib-float.h .. /usr/include/c++/7/csetjmp ... /usr/include/setjmp.h .... /usr/include/x86_64-linux-gnu/bits/setjmp.h ..... /usr/include/x86_64-linux-gnu/bits/wordsize.h .. /usr/include/c++/7/csignal ... /usr/include/signal.h .... /usr/include/x86_64-linux-gnu/bits/signum.h ..... /usr/include/x86_64-linux-gnu/bits/signum-generic.h .... /usr/include/x86_64-linux-gnu/bits/types/sig_atomic_t.h .... /usr/include/x86_64-linux-gnu/bits/types/siginfo_t.h ..... /usr/include/x86_64-linux-gnu/bits/wordsize.h ..... /usr/include/x86_64-linux-gnu/bits/types/__sigval_t.h ..... /usr/include/x86_64-linux-gnu/bits/siginfo-arch.h .... /usr/include/x86_64-linux-gnu/bits/siginfo-consts.h ..... /usr/include/x86_64-linux-gnu/bits/siginfo-consts-arch.h .... /usr/include/x86_64-linux-gnu/bits/types/sigval_t.h .... /usr/include/x86_64-linux-gnu/bits/types/sigevent_t.h ..... /usr/include/x86_64-linux-gnu/bits/wordsize.h .... /usr/include/x86_64-linux-gnu/bits/sigevent-consts.h .... /usr/include/x86_64-linux-gnu/bits/sigaction.h .... /usr/include/x86_64-linux-gnu/bits/sigcontext.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/x86_64-linux-gnu/bits/types/stack_t.h ..... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/x86_64-linux-gnu/sys/ucontext.h .... /usr/include/x86_64-linux-gnu/bits/sigstack.h .... /usr/include/x86_64-linux-gnu/bits/ss_flags.h .... /usr/include/x86_64-linux-gnu/bits/types/struct_sigstack.h .... /usr/include/x86_64-linux-gnu/bits/sigthread.h .. /usr/include/c++/7/cstdarg ... /usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h .. /usr/include/c++/7/cstddef ... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .. /usr/include/c++/7/cstdio ... /usr/include/stdio.h .... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/x86_64-linux-gnu/bits/types/__FILE.h .... /usr/include/x86_64-linux-gnu/bits/types/FILE.h .... /usr/include/x86_64-linux-gnu/bits/libio.h ..... /usr/include/x86_64-linux-gnu/bits/_G_config.h ...... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ...... /usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h .... /usr/include/x86_64-linux-gnu/bits/stdio_lim.h .... /usr/include/x86_64-linux-gnu/bits/sys_errlist.h .. /usr/include/c++/7/cstdlib .. /usr/include/c++/7/cstring ... /usr/include/string.h .... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/strings.h ..... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .. /usr/include/c++/7/ctime ... /usr/include/time.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .... /usr/include/x86_64-linux-gnu/bits/time.h ..... /usr/include/x86_64-linux-gnu/bits/timex.h .... /usr/include/x86_64-linux-gnu/bits/types/struct_tm.h .... /usr/include/x86_64-linux-gnu/bits/types/struct_itimerspec.h .. /usr/include/c++/7/ccomplex ... /usr/include/c++/7/complex .... /usr/include/c++/7/cmath .... /usr/include/c++/7/sstream ..... /usr/include/c++/7/istream ...... /usr/include/c++/7/ios ....... /usr/include/c++/7/iosfwd ........ /usr/include/c++/7/bits/stringfwd.h ......... /usr/include/c++/7/bits/memoryfwd.h ........ /usr/include/c++/7/bits/postypes.h ......... /usr/include/c++/7/cwchar .......... /usr/include/wchar.h ........... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h ........... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ........... /usr/include/x86_64-linux-gnu/bits/wchar.h ........... /usr/include/x86_64-linux-gnu/bits/types/wint_t.h ........... /usr/include/x86_64-linux-gnu/bits/types/mbstate_t.h ....... /usr/include/c++/7/exception ........ /usr/include/c++/7/bits/exception.h ........ /usr/include/c++/7/bits/exception_ptr.h ......... /usr/include/c++/7/bits/exception_defines.h ......... /usr/include/c++/7/bits/cxxabi_init_exception.h .......... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ......... /usr/include/c++/7/typeinfo .......... /usr/include/c++/7/bits/hash_bytes.h ......... /usr/include/c++/7/new .......... /usr/include/c++/7/exception ........ /usr/include/c++/7/bits/nested_exception.h ......... /usr/include/c++/7/bits/move.h .......... /usr/include/c++/7/bits/concept_check.h .......... /usr/include/c++/7/type_traits ....... /usr/include/c++/7/bits/char_traits.h ........ /usr/include/c++/7/bits/stl_algobase.h ......... /usr/include/c++/7/bits/functexcept.h ......... /usr/include/c++/7/ext/numeric_traits.h ......... /usr/include/c++/7/bits/stl_pair.h ......... /usr/include/c++/7/bits/stl_iterator_base_types.h ......... /usr/include/c++/7/bits/stl_iterator_base_funcs.h .......... /usr/include/c++/7/debug/assertions.h ......... /usr/include/c++/7/bits/stl_iterator.h .......... /usr/include/c++/7/bits/ptr_traits.h ......... /usr/include/c++/7/debug/debug.h ......... /usr/include/c++/7/bits/predefined_ops.h ........ /usr/include/c++/7/cwchar ........ /usr/include/c++/7/cstdint ......... /usr/lib/gcc/x86_64-linux-gnu/7/include/stdint.h .......... /usr/include/stdint.h ........... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h ........... /usr/include/x86_64-linux-gnu/bits/wordsize.h ........... /usr/include/x86_64-linux-gnu/bits/stdint-uintn.h ....... /usr/include/c++/7/bits/localefwd.h ........ /usr/include/x86_64-linux-gnu/c++/7/bits/c++locale.h ......... /usr/include/c++/7/clocale ........ /usr/include/c++/7/cctype ....... /usr/include/c++/7/bits/ios_base.h ........ /usr/include/c++/7/ext/atomicity.h ......... /usr/include/x86_64-linux-gnu/c++/7/bits/gthr.h .......... /usr/include/x86_64-linux-gnu/c++/7/bits/gthr-default.h ........... /usr/include/pthread.h ............ /usr/include/sched.h ............. /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h ............. /usr/include/x86_64-linux-gnu/bits/sched.h ............. /usr/include/x86_64-linux-gnu/bits/cpu-set.h ............ /usr/include/x86_64-linux-gnu/bits/wordsize.h ......... /usr/include/x86_64-linux-gnu/c++/7/bits/atomic_word.h ........ /usr/include/c++/7/bits/locale_classes.h ......... /usr/include/c++/7/string .......... /usr/include/c++/7/bits/allocator.h ........... /usr/include/x86_64-linux-gnu/c++/7/bits/c++allocator.h ............ /usr/include/c++/7/ext/new_allocator.h .......... /usr/include/c++/7/bits/ostream_insert.h ........... /usr/include/c++/7/bits/cxxabi_forced.h .......... /usr/include/c++/7/bits/stl_function.h ........... /usr/include/c++/7/backward/binders.h .......... /usr/include/c++/7/bits/range_access.h ........... /usr/include/c++/7/initializer_list .......... /usr/include/c++/7/bits/basic_string.h ........... /usr/include/c++/7/ext/alloc_traits.h ............ /usr/include/c++/7/bits/alloc_traits.h ........... /usr/include/c++/7/ext/string_conversions.h ............ /usr/include/c++/7/cstdlib ............ /usr/include/c++/7/cwchar ............ /usr/include/c++/7/cstdio ............ /usr/include/c++/7/cerrno ........... /usr/include/c++/7/bits/functional_hash.h .......... /usr/include/c++/7/bits/basic_string.tcc ......... /usr/include/c++/7/bits/locale_classes.tcc ........ /usr/include/c++/7/system_error ......... /usr/include/x86_64-linux-gnu/c++/7/bits/error_constants.h .......... /usr/include/c++/7/cerrno ......... /usr/include/c++/7/stdexcept ....... /usr/include/c++/7/streambuf ........ /usr/include/c++/7/bits/streambuf.tcc ....... /usr/include/c++/7/bits/basic_ios.h ........ /usr/include/c++/7/bits/locale_facets.h ......... /usr/include/c++/7/cwctype .......... /usr/include/wctype.h ........... /usr/include/x86_64-linux-gnu/bits/wctype-wchar.h ......... /usr/include/c++/7/cctype ......... /usr/include/x86_64-linux-gnu/c++/7/bits/ctype_base.h ......... /usr/include/c++/7/bits/streambuf_iterator.h ......... /usr/include/x86_64-linux-gnu/c++/7/bits/ctype_inline.h ......... /usr/include/c++/7/bits/locale_facets.tcc ........ /usr/include/c++/7/bits/basic_ios.tcc ...... /usr/include/c++/7/ostream ....... /usr/include/c++/7/bits/ostream.tcc ...... /usr/include/c++/7/bits/istream.tcc ..... /usr/include/c++/7/bits/sstream.tcc .. /usr/include/c++/7/cfenv ... /usr/include/c++/7/fenv.h .... /usr/include/fenv.h ..... /usr/include/x86_64-linux-gnu/bits/libc-header-start.h ..... /usr/include/x86_64-linux-gnu/bits/fenv.h .. /usr/include/c++/7/cinttypes ... /usr/include/inttypes.h .. /usr/include/c++/7/cstdalign ... /usr/lib/gcc/x86_64-linux-gnu/7/include/stdalign.h .. /usr/include/c++/7/cstdbool ... /usr/lib/gcc/x86_64-linux-gnu/7/include/stdbool.h .. /usr/include/c++/7/ctgmath ... /usr/include/c++/7/cmath .. /usr/include/c++/7/cuchar ... /usr/include/c++/7/cwchar ... /usr/include/uchar.h .... /usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h .. /usr/include/c++/7/cwchar .. /usr/include/c++/7/cwctype .. /usr/include/c++/7/algorithm ... /usr/include/c++/7/utility .... /usr/include/c++/7/bits/stl_relops.h ... /usr/include/c++/7/bits/stl_algo.h .... /usr/include/c++/7/cstdlib .... /usr/include/c++/7/bits/algorithmfwd.h .... /usr/include/c++/7/bits/stl_heap.h .... /usr/include/c++/7/bits/stl_tempbuf.h ..... /usr/include/c++/7/bits/stl_construct.h .... /usr/include/c++/7/bits/uniform_int_dist.h ..... /usr/include/c++/7/limits .. /usr/include/c++/7/bitset .. /usr/include/c++/7/deque ... /usr/include/c++/7/bits/stl_uninitialized.h ... /usr/include/c++/7/bits/stl_deque.h ... /usr/include/c++/7/bits/deque.tcc .. /usr/include/c++/7/fstream ... /usr/include/c++/7/bits/codecvt.h ... /usr/include/c++/7/cstdio ... /usr/include/x86_64-linux-gnu/c++/7/bits/basic_file.h .... /usr/include/x86_64-linux-gnu/c++/7/bits/c++io.h ..... /usr/include/c++/7/cstdio ... /usr/include/c++/7/bits/fstream.tcc .. /usr/include/c++/7/functional ... /usr/include/c++/7/tuple .... /usr/include/c++/7/array .... /usr/include/c++/7/bits/uses_allocator.h .... /usr/include/c++/7/bits/invoke.h ... /usr/include/c++/7/bits/std_function.h .... /usr/include/c++/7/bits/refwrap.h .. /usr/include/c++/7/iomanip ... /usr/include/c++/7/locale .... /usr/include/c++/7/bits/locale_facets_nonio.h ..... /usr/include/c++/7/ctime ..... /usr/include/x86_64-linux-gnu/c++/7/bits/time_members.h ..... /usr/include/x86_64-linux-gnu/c++/7/bits/messages_members.h ...... /usr/include/libintl.h ..... /usr/include/c++/7/bits/locale_facets_nonio.tcc .... /usr/include/c++/7/bits/locale_conv.h ..... /usr/include/c++/7/bits/stringfwd.h ..... /usr/include/c++/7/bits/allocator.h ..... /usr/include/c++/7/bits/codecvt.h ..... /usr/include/c++/7/bits/unique_ptr.h ... /usr/include/c++/7/bits/quoted_string.h .. /usr/include/c++/7/iostream .. /usr/include/c++/7/iterator ... /usr/include/c++/7/bits/stream_iterator.h .. /usr/include/c++/7/list ... /usr/include/c++/7/bits/stl_list.h .... /usr/include/c++/7/bits/allocated_ptr.h .... /usr/include/c++/7/ext/aligned_buffer.h ... /usr/include/c++/7/bits/list.tcc .. /usr/include/c++/7/map ... /usr/include/c++/7/bits/stl_tree.h ... /usr/include/c++/7/bits/stl_map.h ... /usr/include/c++/7/bits/stl_multimap.h .. /usr/include/c++/7/memory ... /usr/include/c++/7/bits/stl_raw_storage_iter.h ... /usr/include/c++/7/ext/concurrence.h ... /usr/include/c++/7/bits/unique_ptr.h ... /usr/include/c++/7/bits/shared_ptr.h .... /usr/include/c++/7/bits/shared_ptr_base.h ... /usr/include/c++/7/bits/shared_ptr_atomic.h .... /usr/include/c++/7/bits/atomic_base.h ..... /usr/include/c++/7/bits/atomic_lockfree_defines.h ... /usr/include/c++/7/backward/auto_ptr.h .. /usr/include/c++/7/numeric ... /usr/include/c++/7/bits/stl_numeric.h .. /usr/include/c++/7/queue ... /usr/include/c++/7/vector .... /usr/include/c++/7/bits/stl_vector.h .... /usr/include/c++/7/bits/stl_bvector.h .... /usr/include/c++/7/bits/vector.tcc ... /usr/include/c++/7/bits/stl_queue.h .. /usr/include/c++/7/set ... /usr/include/c++/7/bits/stl_set.h ... /usr/include/c++/7/bits/stl_multiset.h .. /usr/include/c++/7/stack ... /usr/include/c++/7/bits/stl_stack.h .. /usr/include/c++/7/valarray ... /usr/include/c++/7/cmath ... /usr/include/c++/7/bits/valarray_array.h .... /usr/include/c++/7/cstdlib .... /usr/include/c++/7/bits/valarray_array.tcc ... /usr/include/c++/7/bits/valarray_before.h .... /usr/include/c++/7/bits/slice_array.h ... /usr/include/c++/7/bits/valarray_after.h ... /usr/include/c++/7/bits/gslice.h ... /usr/include/c++/7/bits/gslice_array.h ... /usr/include/c++/7/bits/mask_array.h ... /usr/include/c++/7/bits/indirect_array.h .. /usr/include/c++/7/atomic .. /usr/include/c++/7/chrono ... /usr/include/c++/7/ratio ... /usr/include/c++/7/ctime ... /usr/include/c++/7/bits/parse_numbers.h .. /usr/include/c++/7/codecvt .. /usr/include/c++/7/condition_variable ... /usr/include/c++/7/bits/std_mutex.h .. /usr/include/c++/7/forward_list ... /usr/include/c++/7/bits/forward_list.h ... /usr/include/c++/7/bits/forward_list.tcc .. /usr/include/c++/7/future ... /usr/include/c++/7/mutex ... /usr/include/c++/7/thread .... /usr/include/c++/7/cerrno ... /usr/include/c++/7/bits/atomic_futex.h .. /usr/include/c++/7/random ... /usr/include/c++/7/cmath ... /usr/include/c++/7/cstdlib ... /usr/include/c++/7/bits/random.h ... /usr/include/x86_64-linux-gnu/c++/7/bits/opt_random.h ... /usr/include/c++/7/bits/random.tcc .. /usr/include/c++/7/regex ... /usr/include/c++/7/cstring ... /usr/include/c++/7/bits/regex_constants.h ... /usr/include/c++/7/bits/regex_error.h ... /usr/include/c++/7/bits/regex_automaton.h .... /usr/include/c++/7/bits/regex_automaton.tcc ... /usr/include/c++/7/bits/regex_scanner.h .... /usr/include/c++/7/bits/regex_scanner.tcc ... /usr/include/c++/7/bits/regex_compiler.h .... /usr/include/c++/7/bits/regex_compiler.tcc ... /usr/include/c++/7/bits/regex.h .... /usr/include/c++/7/bits/regex.tcc ... /usr/include/c++/7/bits/regex_executor.h .... /usr/include/c++/7/bits/regex_executor.tcc .. /usr/include/c++/7/scoped_allocator .. /usr/include/c++/7/typeindex .. /usr/include/c++/7/unordered_map ... /usr/include/c++/7/bits/hashtable.h .... /usr/include/c++/7/bits/hashtable_policy.h ... /usr/include/c++/7/bits/unordered_map.h .. /usr/include/c++/7/unordered_set ... /usr/include/c++/7/bits/unordered_set.h .. /usr/include/c++/7/shared_mutex 大概就这些。。。 预编译头文件 查阅后发现其实 GCC/G++ 可以预编译头文件的,简单来说就是直接编译头文件成 gch 文件即可,即 1 2 cd /usr/include/x86_64-linux-gnu/c++/9/bits sudo g++ -std=c++17 stdc++.h 而后正常使用编译命令即可,GCC/G++ 会自动使用预编译的内容 耗时对比 同样的命令,同样的代码,仅使用了 bits/stdc++.h 的情况下,两者的耗时情况 先是不使用预编译的 1 2 3 real0m2.476s user0m1.279s sys0m0.154s 差不多 4s 的耗时 然后是使用来预编译的 1 2 3 real0m0.668s user0m0.258s sys0m0.065s 差不多 1s 的耗时 天差地别

2022/4/30
articleCard.readMore

使用 Junit5 和 Mockito 实现 SpringBoot 的单元测试最优美的解决方案

什么是单元测试 单元测试就是一部分代码,但是它 不会在正常的业务流程中被执行 不被打包进入最终的编译程序 不会被任何其他业务代码以任何方式导入 不会影响正常的代码 当然,它通常还要满足下面这些条件 自动化的,不需要人工输入任何数据即可完成 独立的,任何两个单元测试之间都不应该发生调用关系 可重复的,单元测试可以无限重复执行且结果应该一致 传统的单元测试,即是测试一个函数是否正确运行。单元测试可以为这个函数预先伪造一个测试环境,例如用户登录了,且已经有超管权限了,那么运行这个函数是否能够得到我们期望得到的结果 注意上面这段文字中的提到的「为这个函数预先伪造一个测试环境」,这似乎不是很难理解,让我来举个例子: 例如我们现在希望测试登录能力,这是多数的服务中通常都有的能力,按照一般的编码规范,我们将会在 service 层进行逻辑判断。例如取出匹配此账号的数据库的值,并校验密码。 这是非常传统的做法,也同样足够的有效。接下来,让我们来为这个测试伪造一个环境 首先,我们在数据库中插入一个数据,当然,此时我们并不需要考虑到底插入了什么,因为我现在想要模拟假如没有此账号的似乎,登录的结果 然后,我们请求对应的接口,使用新的随机数据,当然,这时候我们期望得到的是失败的登录请求,因为我们输入的数据就是不存在的。 让我们来重新回顾整个过程,这个过程我们做了什么?我们访问了数据库!还修改了里面的数据!这太可怕了! 假如这件事放在业务上,我们需要在发布环境通过单元测试来校验代码是否合理的时候,我们还需要插入一条数据!这仅仅只是一个登录失败的测试,这太可怕了! 那有没有什么可能的方案来解决这个问题? 接下来就轮到 Mock 来伪造这个过程了 还是以登录失败为例,我们现在假定 service 是直接调用了 dao 层接口 首先,我们 mock 了用户的 dao 层接口,并指定「获取用户」的接口若传入 “ABC” 这个字符串,则返回不存在这个用户 然后我们调用用户的登录接口,并传入 “ABC” 作为账号 当用户的 service 遇到需要调用用户的 dao 接口时,会被上面设定的规则将会导致不再请求数据库,而是直接返回不存在 service 收到不存在后,包装好返回值,并返回 虽然看起来与刚才的,最终的结果是一样的,我们测试的代码几乎是相同的,但是我们却解决了最重要的问题——访问数据库 事实上,很多时候 mock 并不是解决这个问题的。我们希望单元测试能够单独测试一个函数是否逻辑正确,那么我们仅需要测试这个函数即可,当这个函数需要调用其他函数的时候,我们会对函数进行 mock 使得得到我们期望的值。这样就可以实现仅仅校验此函数的逻辑是否正确了 单元测试的意义 在不需要启动服务的情况下,检查代码逻辑是否有问题 保证代码在后续的迭代过程中,被其他人更新后导致原来可以正常运行的结果变得不正确了 因为单元测试是负责完成代码测试的,所以当完整的单元测试写完之后,我们就可以通过单元测试来校验代码逻辑是否有问题 同时单元测试将会一直存在与源代码中,后续每一次需要进行校验发布时,都可以通过运行一次单元测试来检查是否因为本次修改,导致之前的逻辑出现错误 单元测试的标准 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元 测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 单元测试是可以重复执行的,不能受到外界环境的影响。 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。 单元测试应当包含「边界值测试」、「正确的输入」、「强制错误信息输入」的测试,而不是仅仅以满足覆盖率为标准 你需要会哪些代码知识 本博客的知识范围是 SpringBoot 框架,所以你必须要掌握下面的技能 能够熟练使用 Java 语言编写代码 了解 SpringBoot 的 AOP 的设计思想,会使用依赖注入 能够看懂上面的基本概念,了解单元测试的重要性 会使用 maven,并知道如何使用 maven 能够阅读中文,并能看懂本博客 开始写单元测试 单元测试的代码应该位于你的项目目录 src/test/java 下,接下来所有的内容目录都指此目录 导入 maven 依赖 我们需要了解下面几个重要的依赖,但是并非都是需要添加的,请继续阅读 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!-- Junit 5 --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> </dependency> <!-- Mockito 核心 --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> </dependency> <!-- Mockito 对 static 支持 --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> </dependency> <!-- Spring 对单元测试支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> 以上这些依赖的相互依赖关系如下 graph LRc([mockito-inline]) ----> b([mockito-core])d([spring-boot-starter-test]) ----> b([mockito-core])d([spring-boot-starter-test]) ----> a([junit-jupiter]) 所以,实际上你只需要最后两个依赖即可完成本片博客的所有内容,但是还是有必要详细解释一下这些依赖在本博客中起到的作用 junit5:必须的组件,提供了最重要的注解和单元测试能力 mockito-core:必须的组件,提供了最重要的 Mock 的能力 mockito-inline:非必须组件,提供了对静态方法的 Mock 能力,如果不需要对静态方法进行 Mock 则可以不需要 spring-boot-starter-test:非必须组件,提供了对类的 private 变量的赋值能力,实际上反射也可以做到,但是通常为了方便期间,可以直接使用已经有的轮子 当你确定好需要的依赖之后,将其最新版本添加到你的 maven 里吧 创建测试类 首先,需要进行逻辑测试的永远是某个实现类,而不是接口,因为接口并不是需要测试的,我们需要测试的是实现的过程是否有问题 创建类用来编写你的单元测试。通常我们会根据需要测试的类进行单独建测试类,即每一个类对应一个测试类,每一个测试类,仅测试对应类的方法。例如,我们有 src/main/java/com/example/service/impl/UserServiceImpl.java 类,那么我们创建 src/test/java/com/example/service/impl/UserServiceImplTest.java 用于测试 UserServiceImpl 类。 例如,我们创建了 src/test/java/com/example/service/impl/UserServiceImplTest.java 类用于测试对应的类。接下来我们需要介绍一些注解和类。 1 2 3 4 5 6 7 @ExtendWith() // 来自 junit 5 的注解,用于测试类上,表示此测试需要额外使用什么扩展工具 MockitoExtension // 来自 Mockito-core 的类,是 Mockito 的扩展工具,用于 junit 5 使用,junit 4 并不是这个 @BeforeAll // 来自 junit5 的注解,用于 static 方法上,表示在进行此类的所有测试方法前,执行一次此函数,仅一次 @AfterAll // 来自 junit 5 的注解,与 @BeforeAll 类似,但是表示所有测试方法结束后执行一次,仅一次 @BeforeEach // 来自 junit 5 的注解,用于非 static 方法上,表示在此类的所有测试方法将被执行前,每个都执行一次 @AfterEach // 来自 junit 5 的注解,与 @BeforeEach 类似,但是是在每个测试方法结束后,都执行一次 @Test // 来自 junit 5 的注解,用于非 static 方法上,表示此方法是一个测试 添加注解 接下来,按照上面的描述,为你的每个测试类都添加这些需要的注解,我们可以得到类似下面的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ExtendWith(MockitoExtension.class) class UserServiceImplTest { @BeforeEach void setUp() { // 这里的代码将会在每个测试前运行 } @AfterEach void tearDown() { // 这里的代码将会在每个测试结束后运行 } /** * 测试登录,用户不存在的情况 */ @Test void testLoginWithNoSuchUser() { // 这里编写你的测试代码 } } 我们已经完成了最基本的类的创建,虽然我们还没有开始调用登录的函数,但是我们已经完成类绝大部分的任务。 注入类 接下来,让我们将需要测试的类注入进来 在类中最开头添加类似下面的代码 1 2 3 4 5 6 7 @InjectMocks private UserServiceImpl userService; // 需要测试的类,需要用 @InjectMocks 注解 @Mock private UserDaoImpl userDao; // 需要 mock 的类,需要用 @Mock 注解 然后,开始测试 1 2 3 4 5 6 7 @Test void testLoginWithNoSuchUser() { Boolean isSuccess = userService.login("handle", "password"); Assertions.assertFalse(isSuccess); // 校验返回值是否正确 } 但是这样肯定是不行的,因为你会发现,这样运行的结果会使得 isSuccess 为 null,而不是我们期望的结果。当然,我们也还没有配置 mock 的内容。 mock it! 接下来让我们开始 mock 吧,尝试类似下面的代码 1 2 3 4 5 6 7 8 9 @Test void testLoginWithNoSuchUser() { // 表示「当调用 userDao#selectUserByHandle 且参数为 "handle" 时,则返回 null」 Mockito.when(userDao.selectUserByHandle("handle")).thenReturn(null); Boolean isSuccess = userService.login("handle", "password"); Assertions.assertFalse(isSuccess); // 校验返回值是否正确 } 再运行一次看看?是不是完美了? 回头看看我们做的过程,是否让单元测试变得更加简单了,编写单元测试仅需要三步 编写 Mockito 内容 调用函数 校验返回值或者参数 下面将会介绍几种常见的情况 应对各种情况 通用匹配类型 有时候我们并不喜欢指明参数必须要是什么,例如无论什么调用时,都返回 null,此时,参数可以使用 Mockito.any() 来表示任意参数,例如: 1 Mockito.when(userDao.selectUserByHandle(Mockito.any())).thenReturn(null); 指定调用的目标函数的返回值 这已经在上面提及到了,也就是最常见的问题 让调用的目标函数抛出错误 将 thenReturn 改为 thenThrow 即可 让调用的目标函数做一些指定的事情 如果希望更加自定义函数的内容,譬如做点什么,则可以使用 thenAnswer 来解决 1 2 3 4 5 6 7 Mockito.when(userDao.selectUserByHandle(Mockito.any())).thenAnswer(invocationOnMock -> { String handle = invocationOnMock.getArgument(0); // 获取第 0 个参数 if (handle == "handle") { return null; } return 1; }) 如何应对没有返回值的函数 把 then 的部分向前提就行,并改为 do 系列 1 2 3 4 5 Mockito.doAnswer(invocationOnMock -> { User argument = invocationOnMock.getArgument(0); argument.setId(1); return null; // 必须要返回些什么 }).when(userDao).insertAccount(Mockito.any()); 如何控制那些静态的函数 假如我们有一个校验密码的静态方法 BCryptEncoder#encode,那么下面就是一个很好的例子 1 2 3 4 5 6 7 MockedStatic<BCryptEncoder> bCryptEncoderMockedStatic; bCryptEncoderMockedStatic = Mockito.mockStatic(BCryptEncoder.class); bCryptEncoderMockedStatic.when(() -> BCryptEncoder.encoder("abc")).thenReturn("123"); // do something bCryptEncoderMockedStatic.close(); 如何测试 private 的方法 private 方法不应该被测试,因为其他类不会调用这方法。应该通过 public 间接测试 private 方法 如何校验函数的参数 我们以注册用户的时候使用的插入用户至数据库为例 1 2 3 ArgumentCaptor<User> userArgumentCaptor = ArgumentCaptor.forClass(User.class); // 创建一个捕获类 Mockito.verify(userManager, Mockito.times(1)).insertAccount(userArgumentCaptor.capture()); // 第一次插入的时候,捕获参数 User userCP = userArgumentCaptor.getValue(); 获取被捕获的参数的值,后面就可以直接校验 userCP 了

2022/4/22
articleCard.readMore

centOS 防火墙 docker-compse 的问题

当 centOS 关闭掉防火墙后,请务必重启 docker 1 systemctl restart docker 否则会导致 docker-compose 出错 1 ERROR: Failed to Setup IP tables: Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i br-7506353a9954 -j RETURN: iptables: No chain/target/match by that name.

2022/4/21
articleCard.readMore

Gitbook 安装出错

执行 npm i gitbook-cli -g 时出现 1 2 3 4 5 if (cb) cb.apply(this, arguments) ^ TypeError: cb.apply is not a function at /home/travis/.nvm/versions/node/v12.18.3/lib/node_modules/gitbook-cli/node_modules/npm/node_modules/graceful-fs/polyfills.js:287:18 at FSReqCallback.oncomplete (fs.js:169:5) 的情况,可以执行下面的命令解决 1 2 3 cd `npm root -g`/gitbook-cli/node_modules/npm/node_modules/ npm install graceful-fs@4.2.4 --save gitbook install 即可

2022/3/24
articleCard.readMore

macOS 更新后导致 sdk 丢失问题

在 macOS 更新后,CLion 可能会出现如下错误 1 2 3 4 5 6 CMake Warning at /Applications/CLion.app/Contents/bin/cmake/mac/share/cmake-3.15/Modules/Platform/Darwin-Initialize.cmake:131 (message): Ignoring CMAKE_OSX_SYSROOT value: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk because the directory does not exist. 这时候,只需要删除 cmake-build-debug 或者对应的目录,然后 reload cmake 就行了

2022/3/22
articleCard.readMore

Java 生成验证码 Captcha

方法效率较低,推荐使用缓存,重复使用验证码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 // 验证码宽度 private static final Integer CAPTCHA_WIDTH = 150; // 验证码高度 private static final Integer CAPTCHA_HEIGHT = 40; // 验证码最大旋转角度(不推荐修改) private static final Integer CAPTCHA_CHAR_ROTATE = 5; // 干扰线数量 private static final Integer CAPTCHA_LINE_NUM = 5; // 验证码数量 private static final Integer CAPTCHA_CHAR_NUM = 4; // 验证码的保存格式 private static final String CAPTCHA_CONTENT_TYPE = "PNG"; public static String createCaptcha(OutputStream outputStream) throws PortableException { BufferedImage image = new BufferedImage(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, BufferedImage.TYPE_INT_BGR); Graphics2D g = (Graphics2D) image.getGraphics(); g.fillRect(0, 0, CAPTCHA_WIDTH, CAPTCHA_HEIGHT); for (int i = 0; i < CAPTCHA_LINE_NUM; i++) { drawRandomLine(g); } for (int i = 0; i < CAPTCHA_LINE_NUM; i++) { drawLeftToRightLine(g); } StringBuilder ans = new StringBuilder(); for (int i = 0; i < CAPTCHA_CHAR_NUM; i++) { ans.append(drawString(g, i + 1)); } try { ImageIO.write(image, CAPTCHA_CONTENT_TYPE, outputStream); } catch (IOException e) { throw PortableException.of("B-01-002"); } return ans.toString(); } private static void drawRandomLine(Graphics2D g) { int xs = RANDOM.nextInt(CAPTCHA_WIDTH); int xe = RANDOM.nextInt(CAPTCHA_WIDTH); int ys = RANDOM.nextInt(CAPTCHA_HEIGHT); int ye = RANDOM.nextInt(CAPTCHA_HEIGHT); g.setFont(ARIAL_FONT); g.setColor(getRandomColor()); g.drawLine(xs, ys, xe, ye); } private static void drawLeftToRightLine(Graphics2D g) { int xs = RANDOM.nextInt(CAPTCHA_WIDTH / 2); int xe = CAPTCHA_WIDTH / 2 + RANDOM.nextInt(CAPTCHA_WIDTH / 2); int ys = RANDOM.nextInt(CAPTCHA_HEIGHT / 2); int ye = CAPTCHA_HEIGHT / 2 + RANDOM.nextInt(CAPTCHA_HEIGHT / 2); g.setFont(ARIAL_FONT); g.setColor(getRandomColor()); g.drawLine(xs, ys, xe, ye); } private static String drawString(Graphics2D g, Integer num) { // 保证左侧和右侧不要贴边,总共留出一个字符的空间 int baseX = (int) (CAPTCHA_WIDTH * 1.0 / (ImageUtils.CAPTCHA_CHAR_NUM + 1) * num); AffineTransform old = g.getTransform(); String c = getRandomChar(); g.setFont(ARIAL_FONT); g.setColor(getRandomColor()); g.rotate(Math.toRadians(RANDOM.nextInt(CAPTCHA_CHAR_ROTATE * 2) - CAPTCHA_CHAR_ROTATE)); g.drawString(c, baseX, CAPTCHA_HEIGHT / 3 * 2); g.setTransform(old); return c; } private static Color getRandomColor() { int upper = 128; int lower = 0; int r = lower + RANDOM.nextInt(upper); int g = lower + RANDOM.nextInt(upper); int b = lower + RANDOM.nextInt(upper); return new Color(r, g, b); } private static String getRandomChar() { return String.valueOf(CAPTCHA_CHAR.charAt(RANDOM.nextInt(CAPTCHA_CHAR.length()))); }

2022/3/19
articleCard.readMore

C++ 模版可变参数列表传递给 C 的 va_list 可变参数列表

C 可变参数 以 printf 为例,常见如下 1 int printf(const char* format, ...); CPP 可变参数 常见如下 1 2 template<class... Args> int printf(const string &format, const Args &... args); 若此时需要为 C 的 printf 进行包装,使其可以接受 string 类型的 format,则可以用如下方式实现 1 2 3 4 template<class... Args> int printf(const string &format, const Args &... args) { return printf(format.c_str(), args...); } 通常会提示警告,因为 c_str() 得到的字符串不能保证是一个可格式化的字符串,而 printf 的函数原型是 1 2 __attribute__((__format__ (__printf__, 1, 0))) int printf(const char* format, ...); 这使得 printf 会被检查第一个参数是否满足 printf, scanf, strftime, 或者 strfmon 风格 但是通过此方法可以将部分 C 语言中的方法扩展到 CPP 的模版化

2022/1/12
articleCard.readMore

GitHub 下载的 zip 代码如何与原仓库再次建立连接

执行如下命令即可(注意替换关键词) 1 2 3 4 5 6 7 unzip <repo>.zip cd <repo> git init git add . git remote add origin https://github.com/<user>/<repo>.git git remote update git checkout master

2021/11/25
articleCard.readMore

2021年浙江工商大学新生赛题解

本篇中的题目顺序为预期难度顺序,并非比赛题目顺序 本篇中所有的“更好的优化”均为标准答案之外的思考,不使用此内容也可以通过题目 比赛预期情况 总共比赛人数:175 (至少通过一道题的人数,没有通过题的不计入总人数) 题目名实际通过次数实际通过比例预期通过比例 chiking 的偶像17298.3%100% chiking 和珂朵莉14180.6%80% chiking 的序列 II5851.3%60% chiking 的序列 I11364.6%50% chiking 的棋盘21.1%30% 乐于助人的 chiking2413.7%20% chiking 的俄罗斯方块00%1% chiking 和大家一起来做签到题00%1% chiking 是一个机器人00%1% 综上来看,整个新生赛除了一道题没有达到预期的成绩,其他题目均与预期相差不大 题解 chiking 的偶像 大致题意 循环输出 \soup_god/,且总共输出的字符串长度为 $n$ 思路 简单的签到题,只需要还记得有 mod 这个运算就能写出来 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 #include "bits/stdc++.h" using namespace std; void solve() { const char *data = "\\soup_god/"; int n; cin >> n; for (int i = 0; i < n; ++i) { cout << data[i % 10]; } cout << endl; } 吐槽 我以为大家被卡在不会 mod,结果大家都卡在 \\,这就挺尴尬的 chiking 和珂朵莉 大致题意 $n$ 个物品,每个物品都有价值和所属类别,让你选择 $n - k$ 种类别的物品,使得所选出来的这些类别的物品的总价值最大 思路 也是签到题之一 在读入数据的时候统计每个类别的物品价值只和,之后排序一下,取出后 $n - k$ 个类别的价值即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "bits/stdc++.h" using namespace std; void solve() { int w[11] = {0}; int m, n, k; cin >> m >> n >> k; for (int i = 0; i < m; ++i) { int d, s; cin >> d >> s; w[s] += d; } sort(w + 1, w + n + 1); int ans = 0; for (int i = k + 1; i <= n; ++i) ans += w[i]; cout << ans << endl; } 吐槽 这道题原来有一个小坑点,即物品的价值可以为负数,所以需要额外增加一个判断,但是想了想还是当签到题,不要故意恶心了,于是就删掉了。所以顺便好奇的问一下,有多少人看到题面之后去想过价值是不是可能为负数呢 chiking 的序列 II 大致题意 有一个非递减的数组,允许你进行任意次数操作,每次操作可以使得其中一个值增加 $1$,问至少需要多少次操作才能使得数组内没有相同的值 思路 因为只能进行加法运算,所以数字只会增加,由于已经排序好了数组,所以最简单的方案就是让每一个值都比前一个值要大即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include "bits/stdc++.h" using namespace std; void solve() { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; long long sum = 0; for (int i = 1; i < n; ++i) { int tmp = max(data[i - 1] + 1, data[i]); sum += tmp - data[i]; data[i] = tmp; } cout << sum << endl; } chiking 的序列 I 大致题意 有一个数组,允许你进行任意次操作,每次执行操作可以将任意一个数字插入到数组到任意位置,问至少需要多少次操作才能使得数组内到每一个值满足 $a_i \leq i$,其中 $i$ 表示下标 思路 首先要让每一位的的数字小于等于其下标,那么插入的数字也不能例外。由于插入的数字可以是任意值,所以理所应当的,选择 $1$ 进行插入是最好的选择,因为无论插入何处均可以使得新插入的数字不再需要考虑 接下来是考虑插入位置的问题,在等式中 $a_i$ 是不可更新的值,所以只能想办法使得 $i$ 增大,那么最容易得到的解决方案就是将数字插入数组开头,这会使得所有原来在数组内的值的下标都增大,最大程度的满足条件 接下来考虑插入数量的问题,由于都是插入数组最前面,所以可以将等式改写为 $a_i <= i + x$,其中的 $x$ 即为需要求解的值。那么对于每一个 $i$ 都要满足这个等式,所以遍历一次数组,找出最大的需要的 $x$ 即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "bits/stdc++.h" using namespace std; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; int ans = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; ans = max(ans, tmp - i - 1); } cout << ans << endl; } } chiking 的棋盘 大致题意 用 $k$ 个 L 形状的方块能否平铺一个矩形方格 思路 我们可以用 L 形状的方块拼出如下两种最简平铺方案 这两个方案都是 $8$ 个方格的倍数,所以起码,方格总数应该是 $8$ 的倍数,即 $n \times m = 8k$ 假设两边都是偶数,那么必然其中一边为 $2$ 的倍数,另外一边为 $4$ 的倍数,所以必然可以被仅靠第一种平铺方案平铺 假设一边为奇数一边是偶数,那么必然其中一边为 $8$ 的倍数,而第一种方案也可以改写为 $2 * 8$ 的方格,即只需要另外一边可以分解为 $2x + 3y$ 的形式即可,易得只要 $ \geq 2$ 的值均可 所以结论:只要满足方格数为 $8$ 的倍数,且两边都 $\geq 2$,则必定可以平铺 接下来只需要计算数量对不对就行 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "bits/stdc++.h" using namespace std; void solve() { #define int long long int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; if (n > 1 && m > 1 && (n * m) % 8 == 0) { int cnt = (n * m / 4); if (cnt == k) cout << "Perfect!\n"; else cout << "Single forever!\n"; } else cout << "Single forever!\n"; } } 乐于助人的 chiking 大致题意 允许将字符串中 oo 和 u 互转,也允许将字符串的 kh 和 h 互转的前提下,计算给出的字符串数组中有几个不同的字符串 思路 由于存在转换,所以最好的办法就是统一转为一种类型,再进行比较 oo 和 u 这对规则,若我们将所有的 oo 转为 u,那么当遇到 ou 和 uo 时,会发现在此条规则下应该是相等的字符串没有相等。所以应该将所有的 u 字符转为 oo kh 和 h 这对规则,若我们将所有的 h 转为 kh,那么就会出现 kh 还可以继续转为 kkh、kkkh、kkkkh 等,所以只能选择将 kh 转为 h。但是请注意 kkkh 这类连续的 k 的情况,可以连续多次转换 处理完成后,统计不同的字符串的数量即可 处理字符串复杂度 $O(nm) = 1e5$ 统计不同字符串数量 $O(n^2m) = 1e7$ 满足要求 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include "bits/stdc++.h" using namespace std; char a[1010], b[2010]; void solve() { int n; cin >> n; vector<string> data; for (int i = 0; i < n; ++i) { cin >> a; int len = (int) strlen(a); int pos = 0; for (int j = 0; j < len; ++j) { if (a[j] == 'u') { b[pos++] = 'o'; b[pos++] = 'o'; } else if (a[j] == 'h') { while (pos > 0 && b[pos - 1] == 'k') pos--; b[pos++] = 'h'; } else { b[pos++] = a[j]; } b[pos] = 0; } data.emplace_back(b); } int cnt = 0; for (int i = 0; i < n - 1; ++i) { bool flag = true; for (int j = i + 1; j < n; ++j) { if (data[i] == data[j]) { flag = false; break; } } if (flag) cnt++; } cout << cnt + 1 << endl; } 更好的优化 实际上,这里需要比较的是字符串是否相同,所以可以使用字符串 hash 来解决,这样,复杂度将会降低至 $O(nm)$。当然,字符串 hash 存在一定的可能错误的隐患,但是可以通过增加多组 hash 来解决 Better Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "bits/stdc++.h" using namespace std; char a[1010], b[2010]; void solve() { int n; cin >> n; set<long long> hashCode; for (int i = 0; i < n; ++i) { cin >> a; int len = (int) strlen(a); int pos = 0; for (int j = 0; j < len; ++j) { if (a[j] == 'u') { b[pos++] = 'o'; b[pos++] = 'o'; } else if (a[j] == 'h') { while (pos > 0 && b[pos - 1] == 'k') pos--; b[pos++] = 'h'; } else { b[pos++] = a[j]; } } long long code = 0; const long long p = 131, m = 1e9+7; for (int j = 0; j < pos; ++j) { code *= p; code %= m; code += b[j]; code %= m; } hashCode.insert(code); } cout << hashCode.size() << endl; } chiking 的俄罗斯方块 大致题意 宽度为 4 的俄罗斯方块游戏,仅有两种方块:$2 \times 2$ 的方块和 $1 \times 4$ 的长条,在已知所有的下落顺序的情况下,求出最优的分数 思路 需要思考的东西比较多,我们先来证明一些东西,方面后面使用 实际上仅会出现 10 分和 3 分两种消方块 这个结论应该是比较简单就可以得出的,因为垂直方向上只有长度为 2 和 4 的方块,所以当我们能遇到创造出 10 分的情况,尽快消除绝对不会亏 长条方块必定可以合并视为 $2 \times 4$ 这个结论指的是如下的情况是不可能发生的(带箭头的蓝色方块是最后落下的) 因为这个情况下,才会出现 $1 \times 4$ 无法插入到原来的 $1 \times 4$ 中使得变成 $2 \times 4$ 而这个情况恰好满足一个 10 分的消除情况,即下图 既然能创造 10 分,那么就没必要纠结是不是能够合并了,先变成 10 分重要,所以不存在单独一根的长条方块的情况 将 2*2 放中间一定不是好的选择 这个结论应该显而易见吧,因为放左/右边既可以与 $1 \times 4$ 消分,还可以与 $2 \times 2$ 消分,比放中间肯定更优 原来的结构不影响当前期望的分数 这句话扩展起来就是 不管之前的方块带来什么影响,若接下来 n 个方块能够创造 10 分的价值,则一定可以创造 10 分的价值,若接下来 n 个方块能创建 3 分的价值,则一定可以创造 3 分的价值 这一条暂且先不证明 所有组合如下 所有能够拿分的组合只有下面这些,其中只有前两个是 10 分,其他的均为 3 分 接下来我们可以证明上面那条的结论 之前方块带来的影响主要是由于其必须要先落下导致占用了一定的位置,使得原来可以消掉的方块没有办法继续消除 最常见的一个影响就是 $2 \times 2$ 影响,简单来说就是原来已经有一块 $2 \times 2$ 方格,此时就不一定能够做到满足上述的消分情况,如下图 例如此时,其实无法完美的满足第二种 10 分的消分情况,因为你不可能将四个 $1 \times 4$ 放在同一行中,即同时下落四个 $1 \times 4$ 的方块时,你不能仅通过这四个方块得到 10 分 但是又如何呢? 实际上我们仍然可以拿到 10 分,而且最后剩下的结果仍然是 $2 \times 2$方块,如下图所示 这里就不再详细介绍每种情况了,供各位思考在 $2 \times 2$ 的影响下,四种得分方案是否都可以得到原来的分数且不带来新的影响 还有一种影响是 $2 \times 4$ 的,比较类似,不再详细说明了 特别的,无论在 $2 \times 2$ 还是 $2 \times 4$ 的影响下,第三种得分方案都有可能会从 3 分变成 10 分,而这虽然增加了分数,但是同时也增加了难于预料的问题,必须予以解决。有趣的是,这种新的得分方案,其实正是第一种方案,所以如果我们能够优先将所有可能的第一种方案计算完,那么此情况其实不再可能出现,那么也就可以忽略了 除了 $2 \times 2$ 还是 $2 \times 4$,还有更多的可能,例如更高的 $2 \times 8$ 等等,但实际上是类似的,也就不再需要证明 当然还有更加离谱的影响,例如 $1 \times 4$ 影响,明显,这个影响确实真的影响到得分了,因为第四种得分方案压根不可能得分了,如下图 但是,注意题目中说的 $1 \times 4$ 一定是偶数个,所以之后必定有一个 $1 \times 4$,那么就回到了开头的那个结论的情况,这里又可以拿到 10 分了 穿插组合并无影响 简单来说就是两个结构需要的方块穿插起来,并不会影响最终的得分,这里就不再详细介绍 当你证明完成后接下来就是模拟讨论所有情况即可 注意第一种 10 分,其要求最后一个落下的必须是 $1 \times 4$ 的长条即可,剩下的,统计数量就行 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include "bits/stdc++.h" using namespace std; void solve() { int n; cin >> n; int tot0 = 0, cnt0 = 0, cnt1 = 0, ans = 0; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp == 1) { cnt1++; if (cnt1 >= 2 && cnt0 >= 2) { cnt1 -= 2; cnt0 -= 2; ans += 10; } } else { tot0++; cnt0++; } } ans += (cnt1 / 4) * 10; cnt1 &= 2; if (tot0 >= 2) { ans += (cnt0 + cnt1) / 2 * 3; } else if (cnt0 && cnt1) { ans += 3; } cout << ans << endl; } chiking 和大家一起来做签到题 大致题意 给定一个 n,允许使用加减乘除和任意括号,找出有多少种不同的四个数字,使得这四个数字能够运算出 n,且运算过程中一定出现了小数,罗列所有的可能的四个数字的组合 思路 其实就是一道暴力题 算一下复杂度,$13^4/2 \times 4! \times 2 \times 3^6 = 499,703,256$ 解释一下,罗列每个位置的每个可能,为 $\rightarrow 13^4 / 2$ (避免重复,遍历过程保证前一个值不大于当前值) 排列数组 $\rightarrow 4!$ 总共有两种括号方式 $((A @ B) @ C) @ D$ 和 $(A @ B) @ (C @ D)$($ABCD$ 为数字,$@$ 为运算符号),所以 $\rightarrow 2$ 枚举所有的运算符号 $\rightarrow 3^6$(6 种运算分别为 $A + B, A - B, B - A, A \times B, A \div B, B \div A$) 做一下剪枝,很容易提前放弃掉部分方案,复杂度还可以降低 所以直接暴力就行 但是,如何优雅的暴力呢? AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include "bits/stdc++.h" using namespace std; const double eps = 1e-6; void solve() { int m; set<int> ans; int curValue[4]; cin >> m; auto isM = [&](double cur) { return abs(cur - m) < eps; }; auto isDouble = [&](double cur) { return abs(cur - int(cur + eps)) > eps; }; auto add = [](double a, double b) { return a + b; }; auto sub1 = [](double a, double b) { return a - b; }; auto sub2 = [](double a, double b) { return b - a; }; auto times = [](double a, double b) { return a * b; }; auto div1 = [](double a, double b) { return a / b; }; auto div2 = [](double a, double b) { return b / a; }; function<double(double, double)> arr[6] = {add, sub1, sub2, times, div1, div2}; auto cal = [&]() { bool reach = false, hasNoDouble = false; do { function<bool(double, int, bool)> dfs1 = [&](double cur, int deep, bool hasDouble) { if (deep == 4) { if (isM(cur)) { reach = true; if (!hasDouble) { hasNoDouble = true; return false; } } return true; } return all_of(arr, arr + 6, [&](function<double(double, double)> &func) { double nxt = func(cur, curValue[deep]); return dfs1(nxt, deep + 1, hasDouble || isDouble(nxt)); }); }; function<bool()> dfs2 = [&]() { for (auto &i: arr) { for (auto &j: arr) { double l = i(curValue[0], curValue[1]); double r = j(curValue[2], curValue[3]); for (auto &k: arr) { double t = k(l, r); if (isM(t)) { reach = true; if (!isDouble(l) && !isDouble(r)) { hasNoDouble = true; return false; } } } } } return true; }; dfs1(curValue[0], 1, false); dfs2(); } while (next_permutation(curValue, curValue + 4)); if (reach && !hasNoDouble) { // 四个小于 16 的 int 数字,可以按位压缩到一个 int 中 int t = 0; for (auto &item: curValue) { t <<= 4; t += item; } ans.insert(t); } }; // 暴力枚举没有必要一定要用 dfs,实际上 for 也可以,甚至更快,因为减少了出入栈的耗时 for (int i = 0; i < 13; ++i) { curValue[0] = i + 1; for (int j = i; j < 13; ++j) { curValue[1] = j + 1; for (int k = j; k < 13; ++k) { curValue[2] = k + 1; for (int l = k; l < 13; ++l) { curValue[3] = l + 1; cal(); } } } } cout << ans.size() << endl; for (auto &item: ans) { int cur = item; for (int i = 3; i >= 0; --i) { curValue[i] = cur % 16; cur >>= 4; } for (int i = 0; i < 4; ++i) cout << curValue[i] << " \n"[i == 3]; } } chiking 是一个机器人 大致题意 有一个地图,有障碍物,三种不同的机器,一个只能下,一个只能右,最后那个可以下右移动,询问 q 次某种机器能否从起始点到终点 思路 考虑前两种机器,其实很好解决,以第一种机器举例,我将整个地图的第一行加到第二行,此时的第二行加到第三行,如此操作后,每个点上保存的是此点的正上方有多少个墙,称其为“前缀墙”。若起点和终点的点的“前缀墙”数量相同,则就可以到达,否则中间必定存在墙 第二种机器就不再过多介绍了 第三种机器则比较难做,考虑一种 dp 的可能:若这个点不是墙,则可以到达这个点的所有点,是能够到达这个点上方点的所有点和能够到达这个点左边的所有点的并集。用公式描述一下就是 \begin{equation} dp[i][j] = \begin{cases} dp[i][j - 1] \cup dp[i - 1][j] \cup \{(i, j)\}, & \text{如果当前节点可达 } \\ 空集合, & \text{如果当前节点不可达 } \end{cases}\end{equation}{% raw %} 如此计算我们可以得到第一份代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include "bits/stdc++.h" using namespace std; struct Node { int x1, y1, x2, y2, i; bool operator<(const Node &rhs) const { return x2 < rhs.x2; } }; void solve() { const int N = 510; const int M = 510; // 读取地图 int n, m; cin >> n >> m; vector<bitset<M>> graph(n); for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { char c; cin >> c; graph[i][j] = c == '0'; } } // 前缀和解决 1 和 2 型号的 vector<vector<int>> modelList[2]; for (auto &model: modelList) { model.resize(n); for (auto &item: model) item.resize(m, 0); } for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { modelList[0][i][j] = (i != 0 ? modelList[0][i - 1][j] : 0) + !graph[i][j]; modelList[1][i][j] = (j != 0 ? modelList[1][i][j - 1] : 0) + !graph[i][j]; } } int q; cin >> q; vector<bool> ans(q); // 离线计算答案 vector<Node> query; query.reserve(q); for (int i = 0; i < q; ++i) { int t, x1, y1, x2, y2; cin >> t >> x1 >> y1 >> x2 >> y2; // 下标从 0 开始 x1 -= 1; y1 -= 1; x2 -= 1; y2 -= 1; if (t == 1) ans[i] = (y1 == y2) and (x1 <= x2) and (modelList[0][x1][y1] == modelList[0][x2][y2]); else if (t == 2) ans[i] = (x1 == x2) and (y1 <= y2) and modelList[1][x1][y1] == modelList[1][x2][y2]; else { if (x1 > x2 || y1 > y2) ans[i] = false; else query.push_back((Node) {x1, y1, x2, y2, i}); } } sort(query.begin(), query.end()); auto hashNode = [&](const int &x, const int &y) { return x * n + y; }; vector<bitset<N * M>> dp(m); for (int i = 0; i < m; ++i) { dp[i].reset(); dp[i][hashNode(0, i)] = graph[0][i]; } int curQuery = 0; for (int i = 0; i < n; ++i) { // 离线计算每个位置哪些可以到达 if (!graph[i][0]) dp[0].reset(); else dp[0][hashNode(i, 0)] = true; for (int j = 1; j < m; ++j) { if (!graph[i][j]) dp[j].reset(); else { dp[j][hashNode(i, j)] = true; dp[j] |= dp[j - 1]; } } while (curQuery < query.size()) { if (query[curQuery].x2 == i) { ans[query[curQuery].i] = dp[query[curQuery].y2][hashNode(query[curQuery].x1, query[curQuery].y1)]; curQuery++; } else break; } } for (int i = 0; i < q; ++i) cout << (ans[i] ? "yes" : "no") << endl; } 让我们计算一下复杂度: $O(n^4) = 500^4 = 62,500,000,000$,这肯定不行,就算压位也不能通过 所以需要优化 问题出在需要计算所有的点可达问题,这就导致了计算一个节点就需要 $O(n^2)$ 的时间,非常不合理 如果试图减少一个 $n$,那么我们只能计算能否到达某一行或者某一列的值,而不能计算全部 但是考虑双向,如果我们知道出发点能够到达某个节点。而目标点可以来自同一个点,那么也同样可以说明可以到达 所以这个特殊的一行或者一列将地图分为两半,同时若询问是跨立在这一行或者这一列,则可以回答,但是对于没有跨立的如何解决? 分治 以横向为例,不断取横向中间轴作为特定行,不断计算跨此轴的询问的解,复杂度为 $O(n^3logn) = 500^3 * log(500) = 337,371,250$,似乎可行 可以得到第二份代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include "bits/stdc++.h" using namespace std; const int QUERY_LEN = 1e5 + 10; const int NUM = 505; const int DIV = 505; char mp[NUM][NUM]; int n, m, ans[QUERY_LEN]; int L[NUM][NUM], U[NUM][NUM]; struct node { int id, x1, y1, x2, y2; }; vector<node> queryList; unsigned vis1[NUM][NUM][DIV], vis2[NUM][NUM][DIV]; void reset(unsigned *v) { memset(v, 0, sizeof(unsigned) * DIV); } void cpFlag(unsigned *dist, const unsigned *from) { memcpy(dist, from, sizeof(unsigned) * DIV); } void orFlag(unsigned *dist, const unsigned *from) { for (int i = 0; i < DIV; ++i) dist[i] |= from[i]; } void setFlagTrue(unsigned *v, int id) { v[id] = 1; } bool andFlagAny(const unsigned *a, const unsigned *b) { for (int i = 0; i < DIV; ++i) if (a[i] & b[i]) return true; return false; } void dfs(int l, int r, vector<node> &q) { if (l > r) return; int mid = (l + r) >> 1; for (int i = mid; i >= l; i--) { for (int j = m; j >= 1; j--) { reset(vis1[i][j]); if (mp[i][j] == '1') continue; if (i == mid) setFlagTrue(vis1[i][j], j); else cpFlag(vis1[i][j], vis1[i + 1][j]); orFlag(vis1[i][j], vis1[i][j + 1]); } } for (int i = mid; i <= r; i++) { for (int j = 1; j <= m; j++) { reset(vis2[i][j]); if (mp[i][j] == '1') continue; if (i == mid) setFlagTrue(vis2[i][j], j); else cpFlag(vis2[i][j], vis2[i - 1][j]); orFlag(vis2[i][j], vis2[i][j - 1]); } } vector<node> vl, vr; for (auto it: q) { if (it.x2 < mid) vl.push_back(it); else if (it.x1 > mid) vr.push_back(it); else ans[it.id] = andFlagAny(vis1[it.x1][it.y1], vis2[it.x2][it.y2]); } dfs(l, mid - 1, vl); dfs(mid + 1, r, vr); } void solve() { cin >> n >> m; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { cin >> mp[i][j]; if (mp[i][j] == '0') L[i][j] = L[i][j - 1] + 1; } } for (int j = 1; j <= m; j++) { for (int i = 1; i <= n; i++) { if (mp[i][j] == '0') U[i][j] = U[i - 1][j] + 1; } } int q; cin >> q; for (int i = 1; i <= q; i++) { int op, x1, x2, y1, y2; cin >> op >> x1 >> y1 >> x2 >> y2; if (x1 > x2 || y1 > y2) { ans[i] = 0; continue; } if (op == 1) { if (y1 != y2 || U[x2][y2] < x2 - x1) ans[i] = 0; else ans[i] = 1; } else if (op == 2) { if (x1 != x2 || L[x2][y2] < y2 - y1) ans[i] = 0; else ans[i] = 1; } else { queryList.push_back({i, x1, y1, x2, y2}); } } dfs(1, n, queryList); for (int i = 1; i <= q; i++) cout << (ans[i] ? "yes\n" : "no\n"); } 但是,还是不对,实际上是空间超限了 仔细思考,实际上我们使用了 int 来模拟一个布尔值组,非常浪费空间,可以进行压位,得到最终的代码,此时的复杂度为 $O(n^3logn / 64) = 500^3 * log(500) / 64 = 5,271,425$, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include "bits/stdc++.h" using namespace std; const int QUERY_LEN = 1e5 + 10; const int NUM = 505; const int DIV = 8; const int LEN = 64; typedef unsigned long long bits; char mp[NUM][NUM]; int n, m, ans[QUERY_LEN]; int L[NUM][NUM], U[NUM][NUM]; struct node { int id, x1, y1, x2, y2; }; vector<node> queryList; bits vis1[NUM][NUM][DIV], vis2[NUM][NUM][DIV]; void reset(bits *v) { memset(v, 0, sizeof(bits) * DIV); } void cpFlag(bits *dist, const bits *from) { memcpy(dist, from, sizeof(bits) * DIV); } void orFlag(bits *dist, const bits *from) { for (int i = 0; i < DIV; ++i) dist[i] |= from[i]; } void setFlagTrue(bits *v, int id) { v[id / LEN] |= ((bits) 1u) << (id % LEN); } bool andFlagAny(const bits *a, const bits *b) { for (int i = 0; i < DIV; ++i) if (a[i] & b[i]) return true; return false; } void dfs(int l, int r, vector<node> &q) { if (l > r) return; int mid = (l + r) >> 1; for (int i = mid; i >= l; i--) { for (int j = m; j >= 1; j--) { reset(vis1[i][j]); if (mp[i][j] == '1') continue; if (i == mid) setFlagTrue(vis1[i][j], j); else cpFlag(vis1[i][j], vis1[i + 1][j]); orFlag(vis1[i][j], vis1[i][j + 1]); } } for (int i = mid; i <= r; i++) { for (int j = 1; j <= m; j++) { reset(vis2[i][j]); if (mp[i][j] == '1') continue; if (i == mid) setFlagTrue(vis2[i][j], j); else cpFlag(vis2[i][j], vis2[i - 1][j]); orFlag(vis2[i][j], vis2[i][j - 1]); } } vector<node> vl, vr; for (auto it: q) { if (it.x2 < mid) vl.push_back(it); else if (it.x1 > mid) vr.push_back(it); else ans[it.id] = andFlagAny(vis1[it.x1][it.y1], vis2[it.x2][it.y2]); } dfs(l, mid - 1, vl); dfs(mid + 1, r, vr); } void solve() { cin >> n >> m; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { cin >> mp[i][j]; if (mp[i][j] == '0') L[i][j] = L[i][j - 1] + 1; } } for (int j = 1; j <= m; j++) { for (int i = 1; i <= n; i++) { if (mp[i][j] == '0') U[i][j] = U[i - 1][j] + 1; } } int q; cin >> q; for (int i = 1; i <= q; i++) { int op, x1, x2, y1, y2; cin >> op >> x1 >> y1 >> x2 >> y2; if (x1 > x2 || y1 > y2) { ans[i] = 0; continue; } if (op == 1) { if (y1 != y2 || U[x2][y2] < x2 - x1) ans[i] = 0; else ans[i] = 1; } else if (op == 2) { if (x1 != x2 || L[x2][y2] < y2 - y1) ans[i] = 0; else ans[i] = 1; } else { queryList.push_back({i, x1, y1, x2, y2}); } } dfs(1, n, queryList); for (int i = 1; i <= q; i++) cout << (ans[i] ? "yes\n" : "no\n"); } 当然,如果你了解 bitset 的话,那么就更好办了

2021/11/21
articleCard.readMore

Dockerfile 中下载 JDK8

openjdk-8-jdk-headless 在 Linux 中常用 apt install openjdk-8-headless 来安装 JDK,但是 dockerfile 中无法正常安装 adoptopenjdk-8-hotspot 通常在 docker 中使用 adoptopenjdk-8-hotspot 来代替 openjdk 1 2 3 RUN ["/bin/bash", "-c", "wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add -"] RUN ["/bin/bash", "-c", "echo 'deb https://adoptopenjdk.jfrog.io/adoptopenjdk/deb buster main' > /etc/apt/sources.list.d/AdoptOpenJDK.list"] apt install adoptopenjdk-8-hotspot

2021/11/18
articleCard.readMore

Mac 使用带 python 的 vim

mac 自带的 vim 并不支持 python 采用 brew 的 macvim 代替即可 1 brew install macvim

2021/11/15
articleCard.readMore

Mac 截图唤起速度慢

打开截屏软件 选中任意一个录制的按钮 点击选项 取消 mac 麦克风按钮

2021/10/24
articleCard.readMore

Strassen算法代码

本文仅代码,无理论解释 实话实说,我觉得这个算法在C系列的语言下,简直垃圾到爆炸……毕竟是一群完全不懂程序数学家对着纸弄出来的,看起来好像非常的有用,实际上耗时是非常爆炸的。 但是《算法导论》里有啊……然后上课又要求手写一个 于是我就手写了一个……我尽可能的减少使用的空间同时加快速度了,而且是通过递归实现 Strassen 算法 其中,in.txt 已经预先准备了 3000000 个范围在 0-100 随机数,避免程序在运算过程中爆 int(虽然完全可以取1000) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 /** * Created by Mauve on 3/29/2020. * Copyright © 2020 Mauve, All Rights Reserved */ #include <bits/stdc++.h> using namespace std; /** * 矩阵相乘 * 最终结果耗时结果保存至 * https://www.desmos.com/calculator/gl4tm5i1zu */ struct mat { unsigned row, col; mat(unsigned r, unsigned c) : row(r), col(c) {} virtual int &pos_ref(unsigned i, unsigned j) = 0; virtual int pos(unsigned i, unsigned j) const = 0; }; struct base_mat; struct sub_mat; stack<sub_mat *> sub_data; struct base_mat : mat { int *data; base_mat(unsigned r, unsigned c) : mat(r, c), data(new int[row * col]) {} ~base_mat() { delete[] data; } inline int &pos_ref(unsigned i, unsigned j) override { return *(data + i * col + j); } inline int pos(unsigned i, unsigned j) const override { return *(data + i * col + j); } }; unsigned min_mul; struct sub_mat : mat { mat *a, *b; bool is_add; unsigned offset_ai, offset_aj, offset_bi, offset_bj; explicit sub_mat(mat *data) : mat(data->row, data->col), a(data), b(nullptr), is_add(false), offset_ai(0), offset_aj(0), offset_bi(0), offset_bj(0) { sub_data.push(this); } sub_mat(mat *data, bool of_i, bool of_j) : mat(data->row >> 1u, data->col >> 1u), a(data), b(nullptr), is_add(false), offset_ai(of_i ? data->row >> 1u : 0), offset_aj(of_j ? data->col >> 1u : 0), offset_bi(0), offset_bj(0) { sub_data.push(this); } inline int &pos_ref(unsigned i, unsigned j) override { assert(b == nullptr); return a->pos_ref(i + offset_ai, j + offset_aj); } inline int pos(unsigned i, unsigned j) const override { if (b == nullptr) return a->pos(i + offset_ai, j + offset_aj); return a->pos(i + offset_ai, j + offset_aj) + (is_add ? 1 : -1) * b->pos(i + offset_bi, j + offset_bj); } inline sub_mat *operator+(sub_mat &other) { auto res = new sub_mat(this); res->b = &other; res->is_add = true; return res; } inline sub_mat *operator-(sub_mat &other) { auto res = new sub_mat(this); res->b = &other; res->is_add = false; return res; } mat *operator*(sub_mat &other) { assert(col == other.row); auto res = new base_mat(row, other.col); if (col & 1u || row & 1u || other.col & 1u || col <= min_mul || row <= min_mul || other.col <= min_mul) { memset(res->data, 0, sizeof(int) * res->row * res->col); for (int k = 0; k < col; k++) for (int i = 0; i < row; ++i) for (int j = 0; j < other.col; ++j) res->pos_ref(i, j) += pos(i, k) * other.pos(k, j); } else { size_t sub_data_size = sub_data.size(); #define a(i, j) (*new sub_mat(this, i == 2 , j == 2)) #define b(i, j) (*new sub_mat(&other, i == 2 , j == 2)) auto m1 = *(a(1, 1) + a(2, 2)) * *(b(1, 1) + b (2, 2)); auto m2 = *(a(2, 1) + a(2, 2)) * b(1, 1); auto m3 = a(1, 1) * *(b(1, 2) - b(2, 2)); auto m4 = a(2, 2) * *(b(2, 1) - b(1, 1)); auto m5 = *(a(1, 1) + a(1, 2)) * b(2, 2); auto m6 = *(a(2, 1) - a(1, 1)) * *(b(1, 1) + b(1, 2)); auto m7 = *(a(1, 2) - a(2, 2)) * *(b(2, 1) + b(2, 2)); #undef a #undef b unsigned half_row = row >> 1u, half_col = col >> 1u; #define m(t) (m##t->pos(i, j)) // C11 for (unsigned i = 0; i < half_row; ++i) for (unsigned j = 0; j < half_col; ++j) res->pos_ref(i, j) = m(1) + m(4) - m(5) + m(7); // C12 for (unsigned i = 0; i < half_row; ++i) for (unsigned j = 0; j < half_col; ++j) res->pos_ref(i, j + half_col) = m(3) + m(5); // C21 for (unsigned i = 0; i < half_row; ++i) for (unsigned j = 0; j < half_col; ++j) res->pos_ref(i + half_row, j) = m(2) + m(4); // C22 for (unsigned i = 0; i < half_row; ++i) for (unsigned j = 0; j < half_col; ++j) res->pos_ref(i + half_row, j + half_col) = m(1) - m(2) + m(3) + m(6); #undef m delete dynamic_cast<base_mat *>(m1); delete dynamic_cast<base_mat *>(m2); delete dynamic_cast<base_mat *>(m3); delete dynamic_cast<base_mat *>(m4); delete dynamic_cast<base_mat *>(m5); delete dynamic_cast<base_mat *>(m6); delete dynamic_cast<base_mat *>(m7); while (sub_data.size() > sub_data_size) { delete sub_data.top(); sub_data.pop(); } } return res; } }; unsigned N = 2; void solve() { cerr << "N = " << N << endl; base_mat a(N, N), b(N, N); for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j) cin >> a.pos_ref(i, j); for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j) cin >> b.pos_ref(i, j); for (int t = 1; t < min(10u, N); t += 3) { auto x = new sub_mat(&a), y = new sub_mat(&b); min_mul = t; auto time_1 = clock(); auto z = *x * *y; auto time_2 = clock(); cerr << "t = " << t << " time: " << double(time_2 - time_1) / CLOCKS_PER_SEC << endl; delete dynamic_cast<base_mat *>(z); while (!sub_data.empty()) { delete sub_data.top(); sub_data.pop(); } } auto x = new sub_mat(&a), y = new sub_mat(&b); min_mul = 10000; auto time_1 = clock(); auto z = *x * *y; auto time_2 = clock(); cerr << "tradition: " << double(time_2 - time_1) / CLOCKS_PER_SEC << endl; delete dynamic_cast<base_mat *>(z); while (!sub_data.empty()) { delete sub_data.top(); sub_data.pop(); } N *= 2; if (N >= 1000) exit(0); } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug && acm_local_for_debug != '~') { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2021/10/13
articleCard.readMore

C++ 语言实现动态变化的线程池

线程池 Job Job 作为任务的类型 1 2 3 4 5 6 7 8 class Job { void *data; function<void(void *data)> func; public: Job(void *data, function<void(void *data)> func); void exec(); }; 其中定义两个变量,data,和 func。 func 用来保存需要调用的方法,当执行任务时,调用此函数即可。考虑到需要传递参数的可能,所以定义参数为一个指针,而另一个变量 data 则为需要传递给 func 的参数指针 函数的实现为 1 2 3 4 5 6 7 8 Job::Job(void *data, function<void(void *)> func) { this->data = data; this->func = move(func); } void Job::exec() { func(data); } 线程池核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class ThreadPool { private: Mutex<map<pthread_t, thread *>> threadPool; // 线程池 Mutex<queue<Job *>> enqueue; // 任务队列 Mutex<vector<thread *>> deathThread; // 已经死亡的线程 Mutex<int> needKill; // 需要杀死的线程数量 condition_variable noTaskCv; // 无任务时的条件信号量 mutex noTaskCvMutex; // 无任务的条件信号量的锁 int maxCore; // 核心线程数 bool killed; // 已经终止了 Job *takeJob(); // 获取一个任务 virtual void addThread(); // 添加一个线程 void clean(); // 清理所有死亡的线程 public: explicit ThreadPool(int core); // 构造函数 void submit(Job *job); // 提交任务,需要提交一个指针类型,且不需要主动 delete,当任务完成后,会被线程池 delete 掉 int getAccumulation(); // 获取当前堆积任务数量 void updateCore(int newCount); // 更新核心线程数,若增加则会新增线程,若减少则会在空闲时间关闭部分线程 void wait(); // 设定线程池为终止,不再可以提交任务,并等待所有任务完成 void close(); // 强制关闭线程池,不等待任务完成 }; 首先通过 init 函数初始化核心线程数 通过 submit 的函数提交任务,必须是一个 job 指针,且必须是单独 new 出来的,线程池会自动清理已经完成的任务 可以随时通过 getAccumulation 来获取到当前堆积的任务,使得可以手动调整线程池数量 使用 upateCore 来调整核心线程数量 建议通过 wait 来实现终止线程池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 ThreadPool::ThreadPool(int core) : maxCore(core), killed(false), needKill(0) { for (int i = 0; i < core; ++i) addThread(); } Job *ThreadPool::takeJob() { Job *cur = nullptr; enqueue.run([&](queue<Job *> &q) { if (q.empty()) return; cur = q.front(); q.pop(); }); return cur; } void ThreadPool::addThread() { auto work = [&]() { while (true) { Job *cur = takeJob(); if (cur != nullptr) { cur->exec(); delete cur; } else { bool dead = false; needKill.run([&](int &count) { if (count <= 0) return; dead = true; count--; }); if (dead) break; clean(); unique_lock<mutex> lk(noTaskCvMutex); noTaskCv.wait(lk); } } threadPool.run([&](map<pthread_t, thread *> &data) { auto iter = data.find(pthread_self()); deathThread.run([&](vector<thread *> &data) { data.push_back(iter->second); }); data.erase(iter); }); }; auto *newThread = new thread(work); threadPool.run([&](map<pthread_t, thread *> &data) { data.insert({newThread->native_handle(), newThread}); }); } void ThreadPool::clean() { if (deathThread.get().empty()) return; deathThread.run([&](vector<thread *> &data) { for (auto &item: data) delete item; }); } void ThreadPool::submit(Job *job) { if (killed) return; enqueue.run([&](queue<Job *> &q) { q.push(job); noTaskCv.notify_one(); }); } int ThreadPool::getAccumulation() { return (int) enqueue.get().size(); } void ThreadPool::updateCore(int newCount) { if (killed) return; needKill.run([&](int &cleaned) { if (newCount > this->maxCore) for (int i = this->maxCore; i < newCount; ++i) addThread(); else { cleaned += this->maxCore - newCount; noTaskCv.notify_all(); } }); } void ThreadPool::wait() { updateCore(0); killed = true; map<pthread_t, thread *> tmp = threadPool.get(); for (auto &item: tmp) item.second->join(); } void ThreadPool::close() { killed = true; map<pthread_t, thread *> tmp = threadPool.get(); for (auto &item: tmp) { pthread_kill(item.first, SIGKILL); delete item.second; } } 线程任务流程 尝试获取一个任务 若有任务 执行任务 删除任务 若无任务 检查是否有需要杀死的线程 若有需要杀死的线程 将当前线程添加进入已经结束线程组 将当前线程从线程池中移除 若无需要杀死的线程 清理需要删除的任务 进入等待状态

2021/10/13
articleCard.readMore

Codeforces Round#744 (Div. 3)

自从退役之后,打了三个月的工,然后再来打这一场 Div3,庆幸自己还能打打,在最后还剩 4 分钟的时候 A 掉了 G 题,终于在比赛期间 AK A. Casimir’s String Solitaire 大致题意 给你一个字符串,仅还有 'A', 'B', 'C' 三个字符,每次可以同时删除任意两个 'A', 'B',也可以同时删除两个 'B', 'C'。判断一个字符串能过上述操作变为空字符串 题解 统计了一下所有字符串中每个字符串的数量,然后若 'B' 的数量和 'A' 和 'C' 的数量之和相同,则 OK AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { string str; cin >> str; int a = 0, b = 0, c = 0; for (auto &item : str) { switch (item) { case 'A': a++; break; case 'B': b++; break; case 'C': c++; break; } } cout << (a + c == b ? "YES" : "NO") << endl; } } B. Shifting Sort 大致题意 一个字符串,每次允许选择其中一个区间,对这个区间进行移位运算,使得这个数组最终有序,使用此操作不能超过整个数组长度次数 题解 这已经把插入排序写在脸上了 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; vector<pair<int, int>> ans; for (int i = 1; i < n; ++i) { int l = i + 1, r = i + 1; for (int j = i - 1; j >= 0; --j) { if (data[j] > data[j + 1]) { l--; swap(data[j], data[j + 1]); } else break; } if (l != r) ans.emplace_back(l, r); } cout << ans.size() << endl; for (auto &item: ans) { cout << item.first << ' ' << item.second << " " << item.second - item.first << endl; } } } C. Ticks 大致题意 一个矩形网格,在上面画 'V' 字形,问,当前对矩形网格上,是否是可以通过画若干个至少为 'k' 大的 'V' 来满足 思路 首先所有 'V' 的特点是最下面的点,每个 'V' 都可以用最下面的点来标记 'V',而其两臂则可以有多长就多长即可。所以可以很轻松得出,应该从下往上遍历来解决问题 如果从下向上遍历,那么若遇到一个 '*' 点,有可能是之前 'V' 的臂,也有可能是新的 'V',同时也可以是两者的结合。所以需要一个标记数组,表示每个点是否已经被下面的 'V' 给画过了,若没有,则这里必定是 'V' 的起点。 但是若为画过,则需要同时考虑两种情况 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m, k; cin >> n >> m >> k; vector<string> data(n); vector<vector<bool>> vis(n); for (int i = 0; i < n; ++i) { cin >> data[i]; vis[i].resize(m, false); } bool flag = true; auto findCell = [&](int x, int y) { int cur = -1; for (int i = 0; i < n; ++i) { bool left = x >= i && y >= i && data[y - i][x - i] == '*'; bool right = x + i < m && y >= i && data[y - i][x + i] == '*'; if (left && right) { cur++; vis[y - i][x - i] = true; vis[y - i][x + i] = true; } else break; } if (cur < k) flag = false; }; auto tryCell = [&](int x, int y) { int cur = -1; for (int i = 0; i < n; ++i) { bool left = x >= i && y >= i && data[y - i][x - i] == '*'; bool right = x + i < m && y >= i && data[y - i][x + i] == '*'; if (left && right) { cur++; } else break; } if (cur >= k) { for (int i = 0; i < cur + 1; ++i) { vis[y - i][x - i] = true; vis[y - i][x + i] = true; } } }; for (int i = n - 1; i >= 0; --i) for (int j = 0; j < m; ++j) if (data[i][j] == '*') { if (!vis[i][j]) findCell(j, i); else tryCell(j, i); } cout << (flag ? "YES" : "NO") << endl; } } D. Productive Meeting 大致题意 有 $n$ 堆石头,每堆石头有若干数量,每次从两堆不同堆石头中取出各一个,如何取使得最后所有堆的石头和最少 题解 第一反应过来以为是背包问题,就是普通的分为两组然后尽可能均分。但是很快意识到不对,因为可以一个人在两堆中变换。然后就简单了,简单的不断取出最大的两堆,各取一个,直到不能取出两个即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; priority_queue<pair<int, int>> prq; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (tmp == 0) continue; prq.push({tmp, i}); } vector<pair<int, int>> ans; while (prq.size() >= 2) { auto a = prq.top(); prq.pop(); auto b = prq.top(); prq.pop(); ans.emplace_back(a.second, b.second); if (a.first > 1) prq.push({a.first - 1, a.second}); if (b.first > 1) prq.push({b.first - 1, b.second}); } cout << ans.size() << endl; for (auto &item : ans) cout << item.first + 1 << ' ' << item.second + 1 << endl; } } E1. Permutation Minimization by Deque 大致题意 一个双向队列,按照一定顺序往其中插入一组值,在已知接下来要插入的值的顺序后,如何确定每一次插入队列前面还是后面,使得整个序列的字典序最小 题解 设计的逻辑很简单,其实每次插入时,若比第一个值大,那么插入到后面,否则一定会使整体值增加,反正则插入到前面即可 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; list<int> res; res.push_back(data[0]); for (int i = 1; i < n; ++i) { if (res.front() > data[i]) res.push_front(data[i]); else res.push_back(data[i]); } for (auto &item : res) { cout << item << ' '; } cout << endl; } } E2. Array Optimization by Deque 大致题意 和上一题差不多的同时,这次需要的是使得逆序对尽可能少 题解 贪心解决了,每次插入的时候,若插入到最前面产生的逆序对数量少于最后面,则插入到前面,否则后面。而计算数量,应该是很久没训练了,一下子只能想到线段树,所以就直接上一个动态开点的线段树解决了 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include "bits/stdc++.h" using namespace std; const int N = 3e6; const int L = -1e9 - 10; const int R = 1e9 + 10; struct SegTree { int s[N], l[N], r[N]; int tot; void init() { tot = 1; s[0] = 0; l[0] = -1; r[0] = -1; } int newNode() { s[tot] = 0; l[tot] = -1; r[tot] = -1; return tot++; } int lc(int x) { if (l[x] == -1) l[x] = newNode(); return l[x]; } int rc(int x) { if (r[x] == -1) r[x] = newNode(); return r[x]; } void up(int x) { s[x] = 0; if (l[x] != -1) s[x] += s[l[x]]; if (r[x] != -1) s[x] += s[r[x]]; } void add(int x, int cur, int ll, int rr) { if (ll == rr) { s[cur]++; return; } int mid = (ll + rr) >> 1; if (x <= mid) add(x, lc(cur), ll, mid); else add(x, rc(cur), mid + 1, rr); up(cur); } int query(int x, int y, int cur, int ll, int rr) { if (ll == x && rr == y) { return s[cur]; } if (s[cur] == 0) return 0; int mid = (ll + rr) >> 1; if (y <= mid) { return query(x, y, lc(cur), ll, mid); } else if (x > mid) { return query(x, y, rc(cur), mid + 1, rr); } else { return query(x, mid, lc(cur), ll, mid) + query(mid + 1, y, rc(cur), mid + 1, rr); } } } segTree; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; long long ans = 0; segTree.init(); segTree.add(data[0], 0, L, R); for (int i = 1; i < n; ++i) { int left = segTree.query(data[i] + 1, R, 0, L, R); int right = segTree.query(L, data[i] - 1, 0, L, R); ans += min(left, right); segTree.add(data[i], 0, L, R); } cout << ans << endl; } } F. Array Stabilization (AND version) 大致题意 给你一个 01 字符串,每次进行对此字符串的某个移位运算后的值进行 AND 运算的,直到此字符串不再改变,需要多少次才能使得整个字符串变为纯 0 的字符串 题解 根据移位操作,建图,然后在拓扑,找出最长链就行了,若不能完整拓扑,则不能 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include "bits/stdc++.h" using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, d; cin >> n >> d; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; bool flag = false; for (int i = 0; i < n; ++i) { if (data[i] == 1) { flag = true; break; } } if (!flag) { cout << 0 << endl; continue; } vector<int> to(n, -1); vector<bool> deg(n, false); for (int i = 0; i < n; ++i) { int nxt = (i + n - d) % n; if (data[i] == 1 && data[nxt] == 1) { to[i] = nxt; deg[nxt] = true; } } queue<pair<int, int>> q; for (int i = 0; i < n; ++i) if (!deg[i]) q.push({i, 0}); int ans = 0; int vis = 0; while (!q.empty()) { auto cur = q.front(); q.pop(); vis++; ans = max(ans, cur.second + 1); if (to[cur.first] == -1) continue; deg[to[cur.first]] = false; q.push({to[cur.first], cur.second + 1}); } if (vis == n) cout << ans << endl; else cout << "-1" << endl; } } G. Minimal Coverage 大致题意 有 $n$ 段线段,首尾相连,连接处可以折叠,求出折叠后,这些线段占用的最小总长度 题解 借用一下数据量并不大的特点,可以直接暴力找所有可能的情况。创建一个布尔数组,若此处为 true 则表示可以从这里开始,否则不能。通过滚动的方式进行 dp 最后找到任意一处为 true 则为成功。 当然此方法仅适合用于 check,所以加一个二分就能解决了 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include "bits/stdc++.h" using namespace std; bool vis[2][3100]; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; int l = 0, r = 2000; auto cal = [&](int len) { int cur = 0, nxt = 1; memset(vis[nxt], true, sizeof(vis[nxt])); for (auto &item: data) { memset(vis[cur], false, sizeof(vis[cur])); for (int i = 0; i < len; ++i) { if (vis[nxt][i]) { if (i - item >= 0) vis[cur][i - item] = true; if (i + item < len) vis[cur][i + item] = true; } } swap(cur, nxt); } for (int i = 0; i < len; ++i) if (vis[nxt][i]) return true; return false; }; while (l + 3 < r) { int mid = (l + r) >> 1; if (cal(mid + 1)) r = mid; else l = mid; } for (int i = l + 5; i >= l - 5; --i) { if (!cal(i + 1)) { cout << i + 1 << endl; break; } } } }

2021/9/29
articleCard.readMore

计算机图形学

相关概念 分辨率 屏幕分辨率:用水平和垂直方向所能显示光点数总和的乘积表示 显示分辨率:用水平和垂直方向所能显示像素点总和的乘积表示 存储分辨率:指帧缓存区域的大小 帧缓存计算 $水平分辨率 垂直分辨率 每个像素所占用的字节$ 图的表示和数据结构 复杂的图形通常被看作是由一些基本图形元素(图元)构成的。基本二维图元包括点、直线、圆弧、多边形、字体符号和位图等 图元通常是指不可再分的独立的图形实体。一个图元中的所有像素点、直线、顶点等是作为一个整体存在的,不再细分为独立的图元。 基本图形生成算法 直线生成算法 数值微分法 定义 $$\epsilon = \frac{1}{max(|\Delta x|, |\Delta y|)}$$ 则递推公式为 $$\left\{\begin{aligned}x' = Math.round(x + \epsilon \cdot \Delta x) \\y' = Math.round(y + \epsilon \cdot \Delta y)\end{aligned}\right.$$ 逐点比较法 略 Bresenham 直线算法 假定 $\Delta x > \Delta y$ 计算得到 $\Delta x = (x_1 - x_0), \Delta y = (y_1 - y_0)$ 定义 $x = x_0, d = - \Delta x, y = y_0$ 绘制点 $(x_0, y_0)$ 将 $x = x + 1$ 将 $d = d + 2 \cdot \Delta y$ 若 $d > 0$ 则 $d = d - 2 \cdot \Delta x, y = y + 1$ 绘制点 $(x, y)$ 若 $x \neq x_1$ 则跳到第三步 二次曲线生成算法 Bresenham 整圆 按照八分法画圆,先绘制 $\frac{\pi}{2} 至 \frac{\pi}{4}$ 的圆,即下图的 $1b$ 区域 定义圆的半径 $R$,则 定义 $d = 1 - R, x = 0, y = R$ 绘制点 $(x, y), (x, -y), (-x, y), (-x, -y), (y, x), (y, -x), (-y, x), (-y, -x)$ $x = x + 1$ 若 $d < 0$ 则 $d = d + 2x + 3$ 反之 $d = d + 2(x - y) + 5, y = y - 1$ 若 $x < y$ 则返回步骤 2,否则结束 区域填充算法 种子填充算法 在区域内部找到一个像素,通过在这个像素的基础上,对邻接的像素进行搜索,并将邻接的像素作为下一个种子 扫描线种子填充算法 给定的种子点开始,填充当前扫描线上种子点所在的区间,然后确定与这一区间相邻上下两条扫描线上需要填充的区间,在这些区间上取最左侧或最右侧的一个点作为新的种子点。不断重复以上过程,直至所有区间都被处理完 初始化一个栈用来存放种子点 将初始的种子放入栈中 若栈为空,则结束算法 取出栈上的第一个点,作为当前种子 从当前种子出发,向左右两边延伸,直到遇到边界 从左往右扫描这条扫描线相邻的 $y - 1$ 和 $y + 1$ 的像素,若不是边界,则将其中所有相邻线段的最右边的像素放入栈中 回到第三步 射线法 从外部点出发,沿任意方向发射射线,若射线与多边形的交点个数为奇数,则为内部,否则为外部 弧长法 略 有效边表算法 考虑对于每一条直线 $y = kx + b$,当 $y’ = y + 1$ 时, $x’ = x + \frac{1}{k}$。所以求交点时,若已知一个交点 $(x, y)$,则可以通过上述公式推导出下一个交点为 $(x + \frac{1}{k}, y + 1)$ 所以依照上述结论,得出如下的操作(以下图为例) 构建一个长度等于几何图形的最大高度的表格 将几何图形的每一条边的最低点的 $x, y_{max}, \frac{1}{k}$ 保存进入对应的 $y_{min}$ 表格中,(这三个值的顺序可以任意交换,例如下面的所有图中的数据顺序为 $y_{max}, x, \frac{1}{k}$ ),对于每一个单元,按照 $x$ 进行从小到大排序,若 $x$ 相同,则按照 $\frac{1}{k}$ 从小到大排序。所以可以得到下面的表格 从最小的 $y$ 开始,不断增大 $y$,根据上一次的 $y$,计算 $y’ = y + 1$ 时,每一个元素对应的 $x’ = x + \frac{1}{k}$ ,对于当前的 $y$ 从第一个节点遍历到最后一个节点,若经过的节点数为奇数,则将这块区域内都进行染色,然后移除所有 $y = y_{max}$ 的数据,可以得到下图 字符 略 反走样技术 形式 倾斜的直线和区域的边界呈阶梯状、锯齿状 图像细节失真,由于离散像素的四舍五入导致了本应均匀的纹理图案变得不均匀显示 很细小的线和点由于分辨率低而不能被显示出来 解决方法 超采样:以高于物理设备的分辨率完成光栅化,然后再回归到物理设备的分辨率 计算线段跨越的面积,确定颜色值 二维观察 使用齐次坐标表示一个点的坐标 $$\left(\begin{matrix}x & y\end{matrix}\right)\rightarrow\left(\begin{matrix}x & y & 1\end{matrix}\right)$$ 为什么使用齐次坐标系:使图形变换转化为表示图形的点集矩阵与某一变换矩阵相乘,可以借助计算机的高速运算 几何变换 平移 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 \\0 & 1 & 0 \\T_x & T_y & 1\end{matrix}\right]=\left[\begin{matrix}x + T_x & y + T_y & 1\end{matrix}\right]$$ 比例缩放 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}S_x & 0 & 0 \\0 & S_y & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}xS_x & yS_y & 1\end{matrix}\right]$$ 或$$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 \\0 & 1 & 0 \\0 & 0 & S\end{matrix}\right]=\left[\begin{matrix}\frac{x}{S} & \frac{y}{S} & 1\end{matrix}\right]$$ 旋转 关于原点进行逆时针旋转 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}cos(\theta) & sin(\theta) & 0 \\-sin(\theta) & cos(\theta) & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}xcos(\theta) - ysin(\theta) & xsin(\theta) + ycos(\theta) & 1\end{matrix}\right]$$ 关于原点进行顺时针旋转 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}cos(\theta) & -sin(\theta) & 0 \\sin(\theta) & cos(\theta) & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}xcos(\theta) + ysin(\theta) & -xsin(\theta) + ycos(\theta) & 1\end{matrix}\right]$$ 对称变换 关于 x 轴对称 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 \\0 & -1 & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}x & -y & 1\end{matrix}\right]$$ 关于 y 轴对称 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}-1 & 0 & 0 \\0 & 1 & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}-x & y & 1\end{matrix}\right]$$ 关于 原点 轴对称 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}-1 & 0 & 0 \\0 & -1 & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}-x & -y & 1\end{matrix}\right]$$ 关于 $y = x$ 轴对称 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}0 & 1 & 0 \\1 & 0 & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}y & x & 1\end{matrix}\right]$$ 关于 $y = -x$ 轴对称 $$\left[\begin{matrix}x' & y' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & 1\end{matrix}\right]\left[\begin{matrix}0 & -1 & 0 \\-1 & 0 & 0 \\0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}-y & -x & 1\end{matrix}\right]$$ 错切变换 略 二维图形几何变换 定义 $$\mathbf P = \left[\begin{matrix}x_1 & y_1 & 1 \\x_2 & y_2 & 1 \\x_3 & y_3 & 1 \\\dots \\x_n & y_n & 1\end{matrix}\right]$$ 为这个二维多边形的所有顶点的坐标矩阵,此时再乘上变换矩阵,得到最终结果 复合变换 相对任意参考点的二维变换 先通过平移变换将参考点移动至原点,然后进行变换,然后再重做平移变换进行撤销一开始的变换 相对任意方向的二维变换 先通过旋转变换将参考点移动至原点,然后进行变换,然后再重做旋转变换进行撤销一开始的变换 二维观察 略 裁剪 点的裁剪 对于点 $P(x, y)$,若满足 $x_{wl} \leq x \leq x_{wr}$ 且 $y_{wb} \leq y \leq y_{wt}$ 则在窗口内,否则在窗口外 直线的裁剪 Cohen-Sutherland 算法 对一条直线的两个顶点进行编码 如图,若 $x < x_l$ 则 $D_0 = 1$。若 $x > x_r$ 则 $D_1 = 1$。若 $y < y_b$ 则 $D_2 = 1$。若 $y > y_t$ 则 $D_3 = 1$ 若两个点 $p_1, p_2$ 的编码 $code_1, code_2$ 满足 $code_1 | code_2 = 0$ 则这条直线就在窗口内,若 $code_1 \& code_2 \neq 0$ 则这条直线可以直接抛弃掉。其他情况只需要求出这条直线和四条边的交点即可 所以可以得到如下的流程 输入点 $p_1, p_2$ 对这两个点进行编码,结果为 $code_1, code_2$ 若 $code_1 = 0$ 且 $code_2 = 0$ 则绘制直线 $p_1, p_2$,然后退出 若 $code_1 \& code_2 \neq 0$ 则直接退出 若 $code_1 = 0$ 则交换 $p_1, p_2$,同时交换 $code_1, code_2$ 若 $code_1 的 D_0 \neq 0$ 则计算直线和 $x = x_l$ 的交点,并将其赋值给 $p_1$,返回第二步 若 $code_1 的 D_1 \neq 0$ 则计算直线和 $x = x_r$ 的交点,并将其赋值给 $p_1$,返回第二步 若 $code_1 的 D_2 \neq 0$ 则计算直线和 $y = y_b$ 的交点,并将其赋值给 $p_1$,返回第二步 若 $code_1 的 D_3 \neq 0$ 则计算直线和 $y = y_t$ 的交点,并将其赋值给 $p_1$,返回第二步 计算交点时,可以借用比例的方式计算,例如计算与 $x = x_l$ 的交点时,可以得到 $$\left\{\begin{aligned}x' & = & x_l \\y' & = & y_1 + (y_2 - y_1) \times (x_l - x_1) / (x_2 - x_1)\end{aligned}\right.$$ 中点分割算法 使用了和 Cohen-Sutherland 完全相同的编码方式,但在求解交点时略有不同。此方法包含一个“求出距离一个点最远的,且在窗口内的点”。所以分别对 $p_1, p_2$ 进行一次求解,并代替掉对方(即对于 $p_1$ 求解的答案,代替掉 $p_2$)即可,以下方法的是对 $p_1$ 进行求解的操作,对 $p_2$ 求解时,交换两个值即可 若 $code_2 = 0$ 则返回 $p_2$ 求出 $p_1$ 和 $p_2$ 的中点 $p_3$ 若 $code_3 = 0$ 则用 $p_3$ 代替 $p_1$(仅算法内代替) 若 $code_3 \neq 0$ 则若 $code_1 \& code_3$ 则用 $p_3$ 代替 $p_1$,反之,则用 $p_3$ 代替 $p_2$,回到第二步 Liang-Barsky 算法 计算$$\left\{\begin{aligned}p_1 & = & -(x_2 - x_1) \\p_2 & = & x_2 - x_1 \\p_3 & = & -(y_2 - y_1) \\p_4 & = & y_2 - y_1 \\q_1 & = & x_1 - x_l \\q_2 & = & x_r - x_1 \\q_3 & = & y_1 - y_b \\q_4 & = & y_t - y_1\end{aligned}\right.$$ 若满足 $p1 = 0 \ AND \ (q1 < 0 \ OR \ q2 < 0)$ 则直线不在窗口内 若满足 $p3 = 0 \ AND \ (q3 < 0 \ OR \ q4 < 0)$ 则直线不在窗口内 准备两个数组 $pos, neg$,将 $1$ 加入到数组 $pos$ 中,将 $0$ 加入到数组 $neg$ 中 若 $p1 = 0$ 则跳到第七步 若 $p1 < 0$ 则将 $r1$ 放入 $neg$,把 $r2$ 放入 $pos$。反之,则将 $r1$ 放入 $pos$,把 $r2$ 放入 $neg$($r1 = q1 / p1$,$r2 = q2 / p2$,下同) 若 $p3 = 0$ 则跳到第九步 若 $p3 < 0$ 则将 $r3$ 放入 $neg$,把 $r4$ 放入 $pos$。反之,则将 $r3$ 放入 $pos$,把 $r4$ 放入 $neg$ 定义 $rn1$ 为 $neg$ 中的最大值,$rn2$ 为 $pos$ 中的最小值 若 $rn1 > rn2$ 则直线不在窗口内 得到交点为 $(x1 + p2 \times rn1, y1 + p4 \times rn1), (x1 + p2 \times rn2, y1 + p4 \times rn2)$ 多边形的裁剪 Sutherland-Hodgeman 算法 适合凸多边形,对于凹多边形则需要先分解为多个凸多边形 对于窗口的所有边界,进行一轮裁剪,裁剪对象是多边形的所有的边。 对于一个多边形,可以认为是一系列的顶点集合,顶点之间的连线即为一个多边形。沿着顶点的连线,进行如下的裁剪操作 若从窗口内到窗口外,则输出交点 $I$ 若从窗口外到窗口内,则输出交点 $I$ 和到达点 $P$ 若从窗口内到窗口内,则输出到达点 $P$ 若从窗口外到窗口外,则不输出 将所有输出的点连接,得到新的多边形 对于窗口的所有边界都进行一次如上的操作,即可得到最终的图像,例如下图,为左边界对此多边形进行裁剪的结果 Weiler-Atherton 算法 从多边形 $P_s$ 的任意一点出发,顺时针遍历所有点 若出现从窗口外进入窗口内,则输出在窗口内的直线 若一直在窗口内,则输出直线 若出现从窗口内进入窗口外,则输出在窗口内的直线,并从此交点 $p_1$ 出发,顺时针的遍历窗口边界的所有点,直到找到第一个与窗口边界相交的多边形的点 $p_2$,则输出 $p_1, p_2$ 的这条路线 三维观察 三维变换 平移 $$\left[\begin{matrix}x' & y' & z' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & z & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 & 0 \\0 & 1 & 0 & 0 \\0 & 0 & 1 & 0 \\T_x & T_y & T_z & 1\end{matrix}\right]=\left[\begin{matrix}x + T_x & y + T_y & z + T_z & 1\end{matrix}\right]$$ 缩放 \left[\begin{matrix}x' & y' & z' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & z & 1\end{matrix}\right]\left[\begin{matrix}a & 0 & 0 & 0 \\0 & e & 0 & 0 \\0 & 0 & i & 0 \\0 & 0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}ax & ey & iz & 1\end{matrix}\right] 旋转 将右手大拇指指向旋转轴的正方向,然后四个手指的弯曲方向即为正旋转方向 绕z轴旋转 \left[\begin{matrix}x' & y' & z' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & z & 1\end{matrix}\right]\left[\begin{matrix}cos \theta & sin \theta & 0 & 0 \\-sin \theta & cos \theta & 0 & 0 \\0 & 0 & 1 & 0 \\0 & 0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}xcos \theta - y sin \theta & xsin \theta + ycos \theta & z & 1\end{matrix}\right] 绕x轴旋转 \left[\begin{matrix}x' & y' & z' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & z & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 & 0 \\0 &cos \theta & sin \theta & 0 \\0 &-sin \theta & cos \theta & 0 \\0 & 0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}x & ycos \theta - zsin \theta & zsin \theta + zcos \theta & 1\end{matrix}\right] 绕y轴旋转 \left[\begin{matrix}x' & y' & z' & 1\end{matrix}\right]=\left[\begin{matrix}x & y & z & 1\end{matrix}\right]\left[\begin{matrix}cos \theta & 0 & -sin \theta & 0 \\0 & 1 & 0 & 0 \\sin \theta & 0 & cos \theta & 0 \\0 & 0 & 0 & 1\end{matrix}\right]=\left[\begin{matrix}zsin \theta + xcos \theta & y & zcos \theta - xsin \theta & 1\end{matrix}\right] 三维投影 投影类型 透视投影 一点透视 两点透视 三点透视 正投影 斜投影 三维投影变换 略 曲线与曲面 概念 拟合:对已经存在的离散点列构造出尽可能光滑的曲线或曲面,用以直观(而忠实)地反映出实验特性、变化规律和趋势等。 插值:通过所有的特征点 逼近:不通过或部分通过特征点,并在整体上接近这些特征点 连续性条件 参数连续 C0 连续:交点处的两条曲线段相交 C1 连续:交点处的两条曲线段相交,且此点的一阶导函数值相同 C2 连续:交点处的两条曲线段相交,且此点的一阶和二阶导函数都相同 几何连续 G0 连续:交点处的两条曲线段相交 G1 连续:交点处的两条曲线段相交,且此点的一阶导函数值成比例 G2 连续:交点处的两条曲线段相交,且此点的一阶和二阶导函数都成比例 三次样条(二维) 对于 $n$ 个点,用 $n - 1$ 条曲线连接,对于每一条直线看,可以用一个三阶函数表示,所以得到 \left\{\begin{aligned}f_1(x) & = a_1 + b_1x + c_1x^2 + d_1x^3 & x \in [x_0, x_1] \\f_2(x) & = a_2 + b_2x + c_2x^2 + d_2x^3 & x \in [x_1, x_2] \\f_3(x) & = a_3 + b_3x + c_3x^2 + d_3x^3 & x \in [x_2, x_3] \\\dots \\f_n(x) & = a_n + b_nx + c_nx^2 + d_nx^3 & x \in [x_{n - 1}, x_n]\end{aligned}\right. 则对于所有的曲线,由于所有曲线必须保证 G0 连续,所以可以得到如下等式 \left\{\begin{aligned}f_0(x_0) & = & & & y_0 \\f_1(x_1) & = & f_0(x_1) & = & y_1 \\f_2(x_2) & = & f_1(x_2) & = & y_2 \\\dots \\f_n(x_n) & = & f_{n - 1}(x_n) & = & y_n \\\end{aligned}\right. 又为了保证 G1 连续,则可以得到他们的导数相同 \left\{\begin{aligned}f_1'(x_1) & = & f_0'(x_1) \\f_2'(x_2) & = & f_1'(x_2) \\f_3'(x_3) & = & f_2'(x_3) \\\dots \\f_n'(x_n) & = & f_{n - 1}'(x_n) \\\end{aligned}\right. 为了保证 G2 连续,则他们的导数的导数相同,所以还可以得到 \left\{\begin{aligned}f_1''(x_1) & = & f_0''(x_1) \\f_2''(x_2) & = & f_1''(x_2) \\f_3''(x_3) & = & f_2''(x_3) \\\dots \\f_n''(x_n) & = & f_{n - 1}''(x_n) \\\end{aligned}\right. 由此,可以计算出所有的参数 Bézier 曲线 P(t) = \sum^n_{k=0}P_kBEN_{k, n}, t \in [0, 1]BEN_{k, n}(t) = \frac{n!}{k!(n - k)!}t^k(1 - t)^{n - k} = C^k_nt^k(1 - t) ^ {n - k} 一阶导数 P'(0) = n(P_1 - P_0)P'(1) = n(P_n - P_{n - 1}) 二阶导数 P''(0) = n(n - 1)((P_2 - P_1) - (P_1 - P_0))P''(1) = n(n - 1)((P_{n - 2} - P_{n - 1}) - (P_{n - 1} - P_n)) 对称性 颠倒控制顶点,Bézier 曲线仍然保持,走向相反 凸包性 略 几何不变性 Bézier 曲线与坐标轴无关 G1 连续 由于一阶导数可知,若需要满足 G1 连续,则必须要满足第一条曲线的最后两个控制点和第二条曲线的开始两个控制点在同一条直线上,且保证不在同一侧,即 (P_n - P_{n - 1}) = \alpha (Q_1 - Q_0') G2 连续 ((P_{n - 2} - P_{n - 1}) - (P_{n - 1} - P_n)) = \beta ((Q_2 - Q_1) - (Q_1 - Q_0)) 消隐 z-buffer 向 z 轴的负方向作为观察方向,以其 z 轴的大小作为深度值,保存每一个像素的颜色值和深度值,当此像素被再次覆盖时,若新的深度比之前保存的大,则用新的深度和颜色覆盖之前的值,否则不更换 画家算法 将物品从远到近排列,先绘制远处的图形,再由近处的图形进行覆盖 光线投射算法 对于屏幕上的每一个像素点,构造一条模拟视线的射线,由射线的交点来确定深度最大的点 光照 光照模型 用于物体表面采样点处光强度的计算 明暗处理 恒定光强的多边形绘制 取一个平面内的任意一个点的光强来表示整个平面的光强 计算量非常小,粗糙,亮度变化大,出现马赫带效应 Gouraud 根据多边形在顶点处的光强,线性插值求出平面内其他点的光强 计算量小,算法简单,出现马赫带效应,对镜面反射效果不佳 Phong 根据多边形在顶点处的法向量,线性插值求出平面内其他点的法向量 计算量大,效果好,精度高 光线跟踪算法 基于几何光学原理,模拟光的传播路径来确定反射、折射和阴影。通过“过采样”的方式,实现反走样

2021/6/10
articleCard.readMore

Windows 通过网络访问 WSL2

localhost 当使用 localhost 时,Windows 直接访问到 WSL 内的进程,即看起来似乎是一台电脑, 127.0.0.1 当使用本地 IP 时,即使用 127.0.0.1 时,Windows 将会无法访问到 WSL,Windows 认为这是强调它自己。 JVM(SpringBoot) 由于此问题是在使用 SpringBoot 时遇到的问题,并不确定是不是 JVM 的问题还是 SpringBoot 的问题 问题 当使用 WSL2 中的 docker 来启动一个 mongo 镜像,使用的命令是 1 2 3 docker run -itd --name mongo -p 27017:27017 mongo --auth docker exec -it mongo mongo admin db.createUser({ user:'root',pwd:'123456',roles:[ { role:'userAdminAnyDatabase', db: 'admin'},"readWriteAnyDatabase"]}); 然后在 Navicat 中,可以直接使用 localhost 对此 mongo 进行连接,但是在 SpringBoot 中,无法连接到此 mongo 数据库,配置如下 报错信息: 1 com.mongodb.MongoSocketOpenException: Exception opening socket 后测试发现,Navicat 也无法使用 127.0.0.1 来访问 WSL,由此推测,JVM 或者 SpringBoot 是否是将 localhost 直接解析为 127.0.0.1 了 解决策略 直接使用 WSL 的 IP 来代替 localhost 在 WSL 中使用 ifconfig 来获取 VM 的 IP,例如下图中,应该选择 eth0 的 IP 172.31.18.91 来代替 localhost 测试 在 Navicat 和 SpringBoot 中,均连接数据库成功

2021/6/7
articleCard.readMore

原生 JavaScript 实现图片裁剪

原生 JavaScript 实现图片裁剪 由于最近做的一个项目里,需要把用户头像上传。但是要求用户头像必须是正方形,所以需要将矩形图片裁剪为正方形 在花了接近 5 个小时之后,总算是将功能上线了,在此记录下整个思路经过 前提 首先,单纯靠前端,是不可能实现将图片截取其中的部分,然后将部分上传到后端。 所以,需要靠伪装裁剪效果的方式来实现裁剪 思路 首先把裁剪图片的页面进行分层 思路1——clip 层级内容 1一个灰白色的背景的 div 2半透明的原图 3不透明的原图的裁剪部分 层级 3 的效果,打算靠 css 的 clip-path 来实现, 思路2 反手打开 Google,打开 Google 的头像上传页面,然后检查元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div class="ee-dm-Ch"> <img src="https://lh3.googleusercontent.com/-ij1qBucmZrI/YJ5t2R6hBPI/AAAAAAAAAJY/DyrS8qSCb4keWB2-fIvIv42m9oW8B9ZnQCLcBGAsYHQ/s180/79468803_p0_master1200.jpg" style="transform: rotate(0deg); width: 127px; height: 180px; opacity: 1; left: 279px; top: 0px; position: absolute;"> <div style="width: 127px; height: 180px; left: 279px; top: 0px; position: absolute;"> <div class="ee-im" style="top: 42px; left: 16px; width: 95px; height: 95px;"></div> <div class="ee-fm" style="cursor: ne-resize; left: 96px; top: 27px;"> <div class="ee-gm"></div> </div> <div class="ee-fm" style="cursor: sw-resize; left: 1px; top: 122px;"> <div class="ee-gm"></div> </div> <div class="ee-fm" style="cursor: se-resize; left: 96px; top: 122px;"> <div class="ee-gm"></div> </div> <div class="ee-fm" style="cursor: nw-resize; left: 1px; top: 27px;"> <div class="ee-gm"></div> </div> <div> <div class="ee-km" style="width: 127px; top: 0px; left: 0px; height: 42px;"></div> <div class="ee-km" style="top: 42px; left: 111px; width: 16px; height: 95px;"></div> <div class="ee-km" style="left: 0px; width: 127px; top: 137px; height: 43px;"></div> <div class="ee-km" style="left: 0px; top: 42px; width: 16px; height: 95px;"></div> </div> <div class="ee-em" style="top: 41px; left: 15px; width: 95px; height: 95px;"></div> </div> </div> 首先是一个 img 作为底图,而且其宽和高同时被限制在 180px 中(上传了一张高大于宽的图片),这可以用 max-height 和 max-width 实现 其次是一个 div ,而且恰好与上面的 img 重合,这应该是作为覆盖在图片上,然后其中有一堆的 div 第一个 div 应该是作为上传图片时,覆盖在最中央的透明方块,用来接受鼠标的拖拽和缩放事件 第二个至第五个,是中间选择框的四个角落,用来拖拽缩放图片 第六个又是一个 div,内部有四个 div,观察后发现,这四个 div 分别是“左上+正上+右上”,“正右”,“右下+正下+左下”,“正左”,而且这四个 div 都是带有半透明属性,且背景为纯白色 最后一个 div 其恰好比图片大一个像素,恰好可以作为黑色的外框 分析 虽然看起来,第一个的思路更加的简单容易,但是第二个思路更合理,毕竟对于低版本的浏览器,更适合此方案 实现 首先需要一个能够选择文件的 UI,由于 <input type="file"> 的页面实在过于丑陋,于是我先对其进行美化 美化 input 首先准备一个 relative 的 div 用于容纳新的 UI,同时将 <input type="file"> 设置为 absolute,并使其覆盖整个 div,然后将不透明度调整为 0 1 2 3 <div class="upload"> <input id="file" type="file" class="fileInput"> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .upload { position: relative; width: 100px; height: 100px; } .fileInput { position: absolute; overflow: hidden; width: 100%; height: 100%; top: 0; left: 0; opacity: 0; } 然后找来了一个上传图片的 svg,直接丢进去 1 2 3 4 5 6 7 8 9 10 11 12 13 <div class="upload"> <svg t="1620918057417" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100"> <path d="M1035.3664 1035.82151111h-86.47111111v-27.30666666h59.16444444v-59.16444445h27.30666667zM881.69813333 1035.82151111H747.30382222v-27.30666666h134.39431111v27.30666666z m-201.59146666 0H545.71235555v-27.30666666h134.39431112v27.30666666z m-201.59146667 0H344.12088889v-27.30666666h134.39431111v27.30666666z m-201.59146667 0H142.52942222v-27.30666666h134.39431111v27.30666666zM75.33226667 1035.82151111h-86.47111112v-86.47111111h27.30666667v59.16444445h59.16444445zM16.16782222 882.15324445h-27.30666667V747.75893333h27.30666667v134.39431112z m0-201.59146667h-27.30666667V546.16746667h27.30666667v134.39431111z m0-201.59146667h-27.30666667V344.576h27.30666667v134.39431111z m0-201.59146666h-27.30666667V142.98453333h27.30666667v134.39431112zM16.16782222 75.78737778h-27.30666667v-86.47111111h86.47111112v27.30666666h-59.16444445zM881.69813333 16.62293333H747.30382222v-27.30666666h134.39431111v27.30666666z m-201.59146666 0H545.71235555v-27.30666666h134.39431112v27.30666666z m-201.59146667 0H344.12088889v-27.30666666h134.39431111v27.30666666z m-201.59146667 0H142.52942222v-27.30666666h134.39431111v27.30666666zM1035.3664 75.78737778h-27.30666667v-59.16444445h-59.16444444v-27.30666666h86.47111111zM1035.3664 882.15324445h-27.30666667V747.75893333h27.30666667v134.39431112z m0-201.59146667h-27.30666667V546.16746667h27.30666667v134.39431111z m0-201.59146667h-27.30666667V344.576h27.30666667v134.39431111z m0-201.59146666h-27.30666667V142.98453333h27.30666667v134.39431112z" fill="#bfbfbf"></path> <path d="M599.74674456 523.1642475H424.34544577c-6.10760857 0-11.06283816-4.95062007-11.06283815-11.06283815 0-6.10760857 4.95522958-11.06283816 11.06283814-11.06283815h175.4012988c6.10760857 0 11.06283816 4.95522958 11.06283816 11.06283815 0 6.11221808-4.95062007 11.06283816-11.06283816 11.06283815z" fill="#bfbfbf"></path> <path d="M512.04609516 610.86489689c-6.10760857 0-11.06283816-4.95522958-11.06283816-11.06283815V424.40075995c0-6.10760857 4.95522958-11.06283816 11.06283816-11.06283814s11.06283816 4.95522958 11.06283814 11.06283814v175.40129879c0 6.11221808-4.95522958 11.06283816-11.06283814 11.06283815z" fill="#bfbfbf"> </path> </svg> <input id="file" type="file" class="fileInput"> </div> 准备覆盖物 直接创建了一堆的 div,为了简化,我把 Google 的拖动缩放的方块改为了滚轮缩放 1 2 3 4 5 6 7 8 9 10 11 12 <div id="clip" class="overlay" style="display: none"> <div class="overlayInline"> <img id="img" src="" alt="" class="image"/> <div id="base" class="imageOverlayBase"> <div class="imageOverlay" id="top"></div> <div class="imageOverlay" id="bottom"></div> <div class="imageOverlay" id="left"></div> <div class="imageOverlay" id="right"></div> <div class="imageOverlayCenter" id="center"></div> </div> </div> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 .overlay { background-color: rgba(0, 0, 0, 0.5); position: fixed; width: 100%; height: 100%; left: 0; top: 0; z-index: 2021 } .overlayInline { position: absolute; background: white; width: 300px; height: 300px; left: 50%; top: 50%; transform: translate(-50%, -50%); } .image { position: absolute; max-width: 250px; max-height: 250px; left: 50%; top: 50%; transform: translate(-50%, -50%); } .imageOverlayBase { position: relative; } .imageOverlay { background-color: #fff; opacity: 0.5; position: absolute; } .imageOverlayCenter { position: absolute; border: 1px solid rgba(0, 0, 0, 0.6); } 我准备了一个 overlay 用于让整个屏幕的剩下部分变成灰色,这样可以避免在拖动的时候点击到其他元素 然后就是无尽的 JavaScript 时间 JavaScript 1 2 3 4 5 6 7 let width = 300 // 最大图片宽度 let height = 300 // 最大图片高度 let scroll = 0.1 // 单次滚轮缩放的最大比例 let imageWidth, imageHeight // 当前图片的大小 let clipLeft, clipTop, clipWidth, clipHeight let topOverlay, bottomOverlay, leftOverlay, rightOverlay, centerOverlay let lastX, lastY // 记录鼠标上次位置 其中 clipLeft, clipTop, clipWidth, clipHeight 分别是“裁剪图片距离左边界的百分比”,“裁剪图片距离上边界的百分比”,“裁剪图片宽度百分比”,“裁剪图片高度百分比” 而 topOverlay, bottomOverlay, leftOverlay, rightOverlay, centerOverlay 则是一些保存所有的元素的变量 然后是当渲染完成时 1 2 3 4 5 6 7 8 9 10 11 12 13 14 window.onload = function () { let input = document.getElementById("file") input.onchange = function (e) { let reader = new FileReader() reader.readAsDataURL(input.files[0]) reader.onload = () => { let img = document.getElementById("img") img.src = reader.result; img.onload = function () { prepareClip() // 准备裁剪 } } } } 获取 input 中的图片,并将其读出,然后让 img 显示出来 然后是一些渲染的准备工作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function prepareClip() { // 获取图片的尺寸 let clip = document.getElementById("clip") clip.style.display = "block" let base = document.getElementById("base") let img = document.getElementById("img") imageWidth = img.width imageHeight = img.height // 将一个 div 移动至恰好覆盖整个图片,方便后续的相对位移的计算 base.style.width = imageWidth base.style.height = imageHeight base.style.left = ((width - imageWidth) / 2).toString() base.style.top = ((height - imageHeight) / 2).toString() // 保存下所有的遮罩 div topOverlay = document.getElementById("top") bottomOverlay = document.getElementById("bottom") leftOverlay = document.getElementById("left") rightOverlay = document.getElementById("right") centerOverlay = document.getElementById("center") // 为部分的 div 设置不会改变的固定值 topOverlay.style.left = "0" topOverlay.style.top = "0" topOverlay.style.right = "0" bottomOverlay.style.left = "0" bottomOverlay.style.bottom = "0" bottomOverlay.style.right = "0" leftOverlay.style.left = "0" rightOverlay.style.right = "0" // 由于项目要求,所以图片将会是正方形,在最开始的时候,裁剪图片的尺寸为最短边的 80% let tmp = Math.min(imageWidth * 0.8, imageHeight * 0.8) clipWidth = tmp / imageWidth clipHeight = tmp / imageHeight clipLeft = (1 - clipWidth) / 2 clipTop = (1 - clipHeight) / 2 // 添加事件 centerOverlay.addEventListener("mousedown", mouseDown) centerOverlay.addEventListener("mousewheel", mouseWheel) resetOverlay() } 然后为每一块 div 计算出他们的位置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function resetOverlay() { let position = { left: clipLeft * imageWidth, right: (1 - clipLeft - clipWidth) * imageWidth, top: clipTop * imageHeight, bottom: (1 - clipTop - clipHeight) * imageHeight } topOverlay.style.bottom = (imageHeight - position.top).toString() bottomOverlay.style.top = (imageHeight - position.bottom).toString() leftOverlay.style.top = position.top.toString() leftOverlay.style.bottom = position.bottom.toString() leftOverlay.style.right = (imageWidth - position.left).toString() rightOverlay.style.top = position.top.toString() rightOverlay.style.bottom = position.bottom.toString() rightOverlay.style.left = (imageWidth - position.right).toString() centerOverlay.style.left = position.left.toString() centerOverlay.style.right = position.right.toString() centerOverlay.style.top = position.top.toString() centerOverlay.style.bottom = position.bottom.toString() } 然后是三个事件的处理过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function clamp(l, r, v) { if (v < l) return l else if (v > r) return r return v } function mouseMove(event) { clipLeft += (event.pageX - lastX) / imageWidth clipLeft = clamp(0, 1 - clipWidth, clipLeft) clipTop += (event.pageY - lastY) / imageHeight clipTop = clamp(0, 1 - clipHeight, clipTop) lastX = event.pageX lastY = event.pageY resetOverlay() } function mouseUp(event) { centerOverlay.removeEventListener('mousemove', mouseMove); } function mouseDown(event) { lastX = event.pageX lastY = event.pageY centerOverlay.addEventListener("mousemove", mouseMove) centerOverlay.addEventListener("mouseup", mouseUp) } function mouseWheel(event) { let op = event.wheelDelta / Math.abs(event.wheelDelta) let maxDelta = op > 0 ? Math.min(imageWidth * (1 - clipWidth), imageHeight * (1 - clipHeight), scroll * Math.min(imageWidth, imageHeight)) : Math.min(imageWidth * (clipWidth - 0.1), imageHeight * (clipHeight - 0.1), scroll * Math.min(imageWidth, imageHeight)) clipWidth += maxDelta / imageWidth * op clipHeight += maxDelta / imageHeight * op clipLeft -= maxDelta / imageWidth / 2 * op clipLeft = clamp(0, 1 - clipWidth, clipLeft) clipTop -= maxDelta / imageHeight / 2 * op clipTop = clamp(0, 1 - clipHeight, clipTop) resetOverlay() } 然后是效果图

2021/5/17
articleCard.readMore

面试复习(计算机图形学)

渲染管线 渲染流程 应用程序阶段 加载数据到显存 设置渲染模式 调用 DrawCall 几何阶段 顶点着色器:将三维的顶点坐标信息映射到 $[-1, 1]$ 的平面上 裁剪:裁剪出摄像机视野内的顶点 屏幕映射:将 $[-1, 1]$ 内的图片映射到屏幕上 光栅化阶段 三角形设置与三角形遍历:找出片元 片元着色器:对每一个片元进行着色,计算其颜色值 逐片元操作:模板测试,深度测试,混合 高斯模糊 高斯模糊方式 按照与当前像素的距离值,计算出高斯函数的值作为权值,进行颜色混合

2021/3/27
articleCard.readMore

面试复习(算法)

链表有环问题 判断单向链表是否有环 定义两个指针,从链表的开始,同时沿着链表指针移动,速度分别为一个单位和两个单位。当某个时刻,两个指针指向同一个节点的时候则此链表有环 理论上,速度可以是任意的两个不同的值,因为只要速度差和环的长度有最小公倍数,则必定存在相交的时间点 找出链表的环的交点位置 通过上述步骤(速度取一步两步),当两个指针第一次相遇的时候,将速度为二步的指针移动至开头,并将速度调整为一步,然后两个指针继续前进,直到第一次相遇,此相遇点即为链表的环的交点 原理:把整个链表认为长度就是两步的那个指针所走过的路程,把重复的那部分复制一份,将链表展开为无环。此时可以认为整个链表长度为 $2n$,且一步的那个指针恰好位于整个链表最中间的地方,即 $n$,同时,在展开之前,这两个节点是“相同的节点”,此时如果再有一个指针以一步的速度从开头开始走,那么当这两个指针分别走到链表的正中间和最后的时候,此时这两个指针实际上相同了,而这两个指针速度相同,所以在之前应该也有一段时间已经相同了。反过来,当他们第一次重合的时候,即为环的入口 判断两个链表是否最终交在同一个节点 利用上述的规则,将其中一个链表的头尾相连,然后从另一个链表开头进行一步两步的判断 洗牌问题 问题样式 在 $n$ 个不相同的数中随机取出 $m$ 个数,使得这 $m$ 个数字不同 一个长度为 $n$ 的数组,将其打散 解题方法 Fisher-Yates Shuffle算法 优点:逻辑简单 缺点:时间复杂度高($O(n^2)$),空间复杂度也较高($O(n)$),而且需要提前知道数组长度 设定 $x = n$ 获得一个在 $[1, x]$ 之间的随机数 $t$ 将原数组中第 $t$ 个没有被取出的数据拿出 使得 $x = x - 1$ 重复 2-4 步直到取出 $m$ 个整数 Knuth-Durstenfeld Shuffle算法 优点:时间复杂度($O(n)$)和空间复杂度($O(1)$)都低 缺点:会修改原数组,而且需要提前知道数组长度 设定 $x = 1$ 获得一个在 $[x, n]$ 之间的随机数 $t$ 交换数组中的第 $x$ 个和第 $t$ 个值 使得 $x = x + 1$ 重复 2-4 步直到 $x = m + 1$ Inside-Out Algorithm算法 优点:时间复杂度($O(n)$)低,不需要提前知道数组长度 缺点:空间复杂度高($O(n)$) 将整个数据拷贝至一个新的数组 $a$ 设定 $x = 1$ 获取一个在 $[1, x]$ 之间的随机数 $t$ 交换 $a_x$ 和 $a_t$ 使得 $x = x + 1$ 重复 3-5 步直到 $x = m + 1$ 快速排序 快速排序的实现 快速排序的优缺点 优点:平均时间复杂度 $O(Nlog_2N)$,空间复杂度 $O(1)$ 缺点:不稳定,初始序列有序或基本有序时,时间复杂度降为 $O(n^2)$

2021/3/27
articleCard.readMore

Codeforces Round#706(Div. 2)-Let's Go Hiking

Let’s Go Hiking 大致题意 有一个数组,两个人,第一步,两个人先后选择数组中的两个下标,要求两个人选择的下标不可以相同。随后按照选择顺序以此进行选择,要求选择一个新的下标,使得新的下标是原来下标的左边或者右边,且不超出数组边界,其中第一个选择的人需要保证新的下标对应的值严格小于原来的下标对应的值,而第二个选择的人则相反,需要选择严格大于的,且保证两人在任意时刻选择的下标不相同,第一个不能选择的人为失败,问第一个人第一次选择哪些下标能够使得他必赢,仅需要输出下标个数 解题思路 按照题目要求,肯定是尽量让第一个人去选择最长的严格单调的子串的最大端,这样第一个人的可选个数最多。而若仅仅如此选择,那么作为第二手,他必定可以选择与这个下标相邻的且比第一个人选的值小的,则可以堵死第一个人的下一步选择 例如 1, 2, 3, 4, 5, 6, 7 对于第一个人而言,他肯定是选择 7 比较好,因为他接下来可以走 6、5、4、3、2、1,拥有最多的步数 但是第二个人完全可以直接选择 6,使得第一个人无法走,因为不能选择相同下标 既然单调的子串会因为第二个人的直接掐断而注定失败,那必定需要为第一个人做第二手的准备,也就是必须要构造一个峰,使得左右两侧都可以下坡,避免出现上述的情况 由于上面得到的结论,假定这个峰左侧的坡较长,而右侧较短。此时第二个人必然选择左侧的峰底,或者峰底以上的一个值。这样无论第一个人往左走还是往右走,都可以保证第一个人会率先遇到无法选择的问题 例如 1, 2, 3, 4, 5, 4, 3, 2 作为第一个人,必定应该选 5 那么第二个人可以直接选 2 (左坡峰底以上的一个值) 若选择往左坡走,那么恰好第一个人遇到无法选择 若往右坡走,那么第一个人必定会把 5 腾出来,使得第二个人拥有和第一个人一样的路程,第一个人仍然输 那么可以很简单的想到,这个峰必须是左右坡长度相同的,但似乎不一定,例如下面这个例子 例如 1, 2, 3, 4, 3, 2, 1 作为第一个人,则必定是选 4 第二个人也没太多选择的余地,肯定是选 1 这时会发现仍然第一个人输了 而如果是下面这个例子 例如 1, 2, 3, 4, 5, 4, 3, 2, 1 可以容易得出,第一个人能够赢的结论 所以答案就是找出存在的一个峰,使得它的左坡和右坡的长度恰好相等且为奇数,则为答案 但是似乎并不是很完整,如果出现下面这样的情况 例如 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 5 按照上面的规律,第一个人只能选择 4 而实际上有更长的坡可以使用,所以第二个人会选择更长的坡,所以第一个人仍然输 同时,还需要保证这个峰是独一无二的,没有其他的坡比他的要大,或者相等 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <bits/stdc++.h> using namespace std; void solve() { int n; cin >> n; vector<int> data(n), l(n), r(n); for (int i = 0; i < n; ++i) cin >> data[i]; int cur = 1; l[0] = l[n - 1] = r[0] = r[n - 1] = 1; for (int i = 1; i < n; ++i) { if (data[i] > data[i - 1]) cur++; else cur = 1; l[i] = cur; } cur = 1; for (int i = n - 2; i >= 0; --i) { if (data[i] > data[i + 1]) cur++; else cur = 1; r[i] = cur; } int maxLen = 0, count = 0; for (int i = 0; i < n; ++i) if (l[i] == r[i] && l[i] & 1) maxLen = max(maxLen, l[i]); for (int i = 0; i < n; ++i) count += (l[i] == maxLen) + (r[i] == maxLen); cout << (count == 2 ? 1 : 0) << endl; } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); signed localTestCount = 1, localReadPos = cin.tellg(); char localTryReadChar; do { if (localTestCount > 20) throw runtime_error("Check the stdin!!!"); auto startClockForDebug = clock(); solve(); auto endClockForDebug = clock(); cout << "Test " << localTestCount << " successful" << endl; cerr << "Test " << localTestCount++ << " Run Time: " << double(endClockForDebug - startClockForDebug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } while (localReadPos != cin.tellg() && cin >> localTryReadChar && localTryReadChar != '$' && cin.putback(localTryReadChar)); #else solve(); #endif return 0; }

2021/3/11
articleCard.readMore

面试复习(Java)

线程池 线程池的原因 不断的创建和删除线程,会带来较大的系统资源负载 线程缺乏统一的管理,可能会出现无限制的创建线程 线程之间抢占资源 线程池的属性 核心线程数:保持存在的线程数量,这些线程会一直存在,不会被删除 任务缓冲队列:当所有的核心线程都在运行时,新的任务会被加入到缓冲队列中 非核心线程数:当任务缓冲队列满后,将会创建新的线程来执行队列中的任务,且额外创建的线程数不会超过非核心线程数 空闲线程的存活时间:当非核心线程空闲时,且持续了一段时间后,此线程将会被删除 拒绝策略:当非核心线程和任务缓存队列满后,对待新的任务的策略 Java 默认的线程池类型 名称核心线程数线程池大小非核心线程存活时间等待队列大小 CachedThreadPool0$\infty$60s0 SingleThreadExecutor110$\infty$ FixedThreadPool$n$$n$0$\infty$ ScheduledThreadPoolExecutor Java 默认的线程池有什么问题,为什么会引起 OOM 异常(OutOfMemoryError) CachedThreadPool 允许创建无线的线程,从而引起 OOM 异常 SingleThreadExecutor 和 FixedThreadPool 请求队列为无限长,可能会堆积大量的消息,从而引发 OOM 异常 Java 内存 内存模型 主内存:线程之间共享的变量储存在主内存中 本地内存:每个线程独立拥有的内存 本地内存保存的是主内存的共享变量的副本 垃圾回收 根搜索算法(可达性分析算法)从 GC ROOT 节点沿着引用链搜索,无法到达的节点即为不可到达的对象 垃圾回收器 G1 收集器 独特的分代垃圾回收器,分代GC:分代收集器,同时兼顾年轻代和老年代 使用分区算法,不要求eden,年轻代或老年代的空间都连续 并行性:回收期间,可由多个线程同时工作,有效利用多核cpu资源 空间整理:回收过程中,会进行适当对象移动,减少空间碎片 可预见性:G1可选取部分区域进行回收,可以缩小回收范围,减少全局停顿 G1 收集器的过程 初始标记(它标记了从GC Root开始直接可达的对象)Stop-The-World 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象) 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)Stop-The-World 筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)Stop-The-World Map hashMap 的结构 采用链地址法,当发生哈希冲突时使用链表解决 当链表过长时,在 JDK 1.8 下采用红黑树代替链表,当数据量较少时,转回链表 当存储的数据量超过一个阈值后,hashMap 的哈希表长度将会扩容到原来的两倍,然后将所有的数据重新分配到新的内存中 hashMap 的这样扩容的理由 通过恰好两倍扩容,可以让原来在第 $i$ 个链表的值被恰好分配到第 $i$ 和第 $2i$ 个链表的位置 每一个值,只需要判断其哈希值在某个二进制位上的值即可直接完成分配 treeMap 的结构 treeMap 是一棵红黑树 设计模式 设计原则 开闭原则:对扩展开放,对修改关闭 里氏替换原则:子类必须拥有所有的父类功能,子类可以开发自己的新功能 依赖倒置原则:高层实现不能依赖于低层实现,而是依赖于低层的抽象类 单一职责原则:一个类应当只负责一个职责 接口隔离原则:接口应该更小更具体,而不是去实现很庞大的接口来适应所有的需求 迪米特法则:避免与无关实体进行通信 合成复用原则:尽量先使用组合或者聚合等关联,其次考虑继承 设计模式 创建型模式 单例模式:限制一个类只能有一个实例 原型模式:以一个此类型的实例为模板,通过拷贝内存中的二进制值来直接创建一个对象 工厂模式:将创建对象的过程由另一个类进行封装 建造者模式:将一个复杂的对象分为多个简单的对象的组合,并实现将多个小对象进行拼装的过程 结构型模式 代理模式:使得两个对象之间不能直接引用访问,只能通过第三方,可以保护目标对象,扩展目标对象的功能 适配器模式:将一个类的接口转换为另外一个类的接口,通常是为了适配两个接口的对接问题 桥接模式:将抽象与实现分离,使得他们可以独立变化,用组合关系来代替继承关系 装饰器模式:为类增加新的功能的同时,避免了继承 外观模式:隐藏系统的复杂性 享元模式:重复使用已经创建的同类对象 组合模式 行为型模式 模板方法模式:仅实现一个操作中的骨干步骤,具体步骤由其子类实现 策略模式 命令模式 责任链模式 状态模式 观察者模式 中介者模式 迭代器模式 访问者模式 备忘录模式 解释器模式 String String 类型 String 类型是不可变的,对 String 类型进行操作的时,会重新生成新的 String 对象 StringBuilder 和 StringBuffer StringBuilder 和 StringBuffer 都是可变的 StringBuilder 没有线程同步,存在线程安全问题,但是其效率略高于 StringBuffer StringBuffer 能够保证线程安全,但效率较低 接口和抽象类 接口和抽象类的区别 抽象类可以写非抽象方法,而接口类只能有抽象方法 一个类只能继承自一个抽象类,而一个类能实现多个接口 继承是一个“是不是”的关系,而 接口 实现则是“有没有”的关系 构造函数 子类实例化总是默认调用父类的无参构造方法 为了让父类初始化属性和方法 equals equals 和 == 的区别 == 对于基本类型时,比较的是两个对象的值是否相同,而对于对象时,则比较的是这两个引用是否指向了同一个对象 equals 则可以由类进行重写,使得其满足正常的比较关系。若不进行重写,则与 == 等价 equals 和 hashCode 为什么需要同时进行重写 hashCode 在 Object 类下的默认行为是将此值的地址取出作为 hashCode,但这与 hashCode 本意不同,hashCode 的值应当满足对于 $\forall x$ 若 x.equals(y) = true,则 x.hashCode() = y.hashCode()。所以当重写 equals 时,通常意味着这个值的相等概念与 == 不同,所以必然需要重写 hashCode 避免在 hashMap 中出现意料之外的情况

2021/2/25
articleCard.readMore

面试复习(Git)

版本回退 revert 重做某个版本,在当前版本后,重新将某个版本的操作重做一次,得到一个新的版本,将会保留所有其他版本的操作 reset 返回到某个版本,所有在这个版本之后的版本都会被撤销

2021/2/24
articleCard.readMore

面试复习(Linux)

查看进程占用系统资源情况 top ps 终端 session 一个终端窗口对应一个会话(session) 一个会话包含多个进程组 一个会话只有一个前台进程组,可以有多个后台进程组 所有在终端内的输入都会发送给前台进程组 当会话关闭时,所有会话内的进程将会被终止 fork 进程的 fork 过程 给新进程分配一个标识符 在内核中分配一个PCB 复制它的父进程的环境(PCB中大部分的内容) 为其分配资源(程序、数据、栈等) 复制父进程地址空间里的内容(代码共享,数据写时拷贝) 将进程置成就绪状态,并将其放入就绪队列,等待CPU调度 守护进程 什么是守护进程 在后台运行的一种特殊的进程 一般在系统启动时开始运行,系统关机后结束运行 常常以 root 权限启动 不需要输入,独立与终端 名称常常以 d 结尾 创建守护进程的过程 在父进程中执行 fork 并 exit 退出 在子进程中调用 setsid 函数创建新的会话 在子进程中调用 chdir 函数,让根目录 / 成为子进程的工作目录 在子进程中调用 umask 函数,设置进程的 umask 为 0 在子进程中关闭任何不需要的文件描述符 IO模式 IO 模式有哪些 阻塞 IO:当进行 IO 时,线程阻塞,不再占用 CPU,直到 IO 完成 非阻塞 IO:当进行 IO 时,线程不阻塞,而是进行循环判断是否 IO 完成,一直占用 CPU IO 多路复用:允许同时进行多个 IO,线程仍然阻塞 异步 IO:在 IO 时不会阻塞,也不会关心 IO 状态,而是继续执行代码,直到 IO 完成再回来执行 IO 完成的代码 IO 多路复用的机制 select:当有任意一个 IO 完成时,通知程序。缺点在于单个进程只能打开有限的文件,同时其采用轮番查询,效率较低,而且仅通知程序有 IO 完成,并不知道到底哪个文件的 IO 完成了 poll:与 select 相同,但其提供了无限制的文件打开数量 epoll:可以自定义打开的文件数量,同时当 IO 完成时会已消息的形式通知程序,包括了到底是哪个 IO 完成了

2021/2/23
articleCard.readMore

面试复习(数据库)

B 树和 B+ 树 B 树的特点 一个节点上包含至多 $m - 1$ 个值 根节点至少有两个孩子 非叶子节点如果包含了 $k$ 个值,则其包含了 $k + 1$ 个孩子节点 所有叶子节点都位于同一层 B+ 树的特点 所有的非叶子节点不再保存值,而是只保存了中间值 所有值保存在叶子节点上 所有的叶子节点通过链表按照顺序进行连接 为什么数据库会采用 B+ 树而不是 B 树或者 AVL 树 AVL 的节点访问次数更多,而对于数据库而言,每个节点通常被存储在一个文件中,所以需要读取的文件数量更多,导致效率低 在数据库中有时需要进行范围的搜索,此时 B+ 树的链表结构可以快速的找到某个节点的下一个节点位置 联合索引 什么是联合索引 多个列同时组成索引 聚簇索引和非聚簇索引 聚簇索引 叶子节点上直接保存值 非聚簇索引 叶子节点上保存的是值所在的地址 聚簇索引的优点 理论上速度更快,因为聚簇索引少一次文件的读取过程 非聚簇索引的优点 更新主键的代价低 主键插入的顺序可以混乱 MySQL 锁 类型 全局锁 表级锁 行级锁 读写锁 读锁:可以与其他的读锁共存,但是不可以与写锁共存 写锁:不可以与其他任何锁共存 悲观锁 普通的锁,锁定此行/表/数据库以防止其他操作进行修改,会导致其他事务被阻塞 乐观锁 通过比较版本号的区别的方法,来确定此数据是否经过修改,如果修改则需要读取最新的值 MyISAM和InnoDB 区别 MyISAM 是 MySQL 之前默认数据库,不支持事务、行级锁和外键,崩溃后无法安全恢复,支持全文索引,强调性能 InnoDB 是 MySQL 目前的默认数据库,支持了ACID兼容的事务,支持行级锁,不支持全文索引 适合范围 MyISAM 适合读密集场所 InnoDB 适合写密集场所 数据库联合查询和连接查询 联合查询(JOIN) 将两张表按照一定规律进行拼接组成结果并返回 SELECT table1.*, table2.* FROM table1 JOIN table2 ON table1.id=table2.id INNER JOIN 仅当左右两个表同时存在对应的数据时才返回 LEFT JOIN 当左边的表存在则返回 RIGHT JOIN 当右边的表存在则返回 连接查询(UNION) 将两个或更多查询的结果集组合为单个结果集,查询来自同一个表的相同列 UNION ALL 不做重复性检查 数据库的索引类型 普通索引 没有限制,普通的索引 唯一索引 索引列的值必须唯一,但允许有空值 主键索引 特殊的唯一索引,不允许有空值 全文索引 对全文的索引,耗时耗空间 数据库隔离级别 隔离级别脏读(Dirty Read)不可重复读(NonRepeatable Read)幻读(Phantom Read) 未提交读(Read uncommitted)可能可能可能 已提交读(Read committed)不可能可能可能 可重复读(Repeatable read)不可能不可能可能 可串行化(Serializable)不可能不可能不可能 隔离级别 未提交读:最朴素的数据库形式 已提交读:在事务完成之后再更新数据库的值 可重复读:每个事务开始前锁定所更新的行 可串行化:单一线程,所有事务必须按照顺序进行 三种错误 脏读:读取到其他事务正在修改过程中的值 不可重复读:一个事务中,同一条语句得到的对应行的内容不同 幻读:一个事务中,同一条语句得到的数据数量不同 MySQL 的存储引擎 MySQL 的存储引擎类型 InnoDB(默认):支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback) MyISAM(旧版本的 MySQL 默认):插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用 Memory:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择 Memory。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表 MySQL 主从复制 MySQL 主从复制作用 实现数据的多处自动备份 实现读写分离还能进一步提升数据库的负载性能 MySQL 主从复制的原理 从服务器将日志与主服务器同步,同时重放日志的内容实现数据同步 回表查询 MySQL 的索引逻辑 MySQL 会为主键保存一棵聚集索引数树(叶子节点上保存了此节点的所有属性值),而其他的索引则为普通的索引树,普通的索引树进仅保存了主键值而没有保存属性值 当需要进行查询且 WHERE 条件不是主键时,需要先通过查询对应的索引,然后通过索引得到主键值,然后再去聚集索引树上搜索对应的主键值,所以需要查询两次索引,效率低 覆盖索引 如果使用 SELECT 的时候,恰好只需要主键和此搜索值,则可以不搜索聚集索引树,因为仅靠普通索引树即可得到答案 为了避免回表,可以通过建议一些可能的联合索引,使得进行 SELECT 的时候不会进行回表操作 连接池 为什么使用连接池 与 Java 的线程池相同,为了节约系统资源 常用的连接池 DBCP:使用量最大的连接池 C3P0 分表 水平分表 将一个表的记录分割到数个表中,可以减少索引的大小,加快索引 垂直分表 将部分字段划分至其他的表,部分字段数据量大,进行索引时会带来大量的 IO 负担,进行分表有利于查询效率

2021/2/23
articleCard.readMore

面试复习(计算机网络)

OSI 模型 哪七层 应用层:协议与端口 表示层 会话层 传输层:TCP、UDP 网络层:IP、ARP 数据链路层:mac地址 物理层:物理字节流传输 ARP 协议 ARP 协议的作用 将 IP 地址转为下一跳的 mac 地址 TCP 三次握手 序号数据发送内容发送方向客户端状态服务器状态 0CLOSEDLISTEN 1SYN=1 seq=x客户端 -> 服务器发送后转为 SYN_SENT接收后转为 SYN_RCVD 2SYN=1 ACK=1 ack=x+1 seq=y服务器 -> 客户端接收后转为 ESTABLISHEDSYN_RCVD 3ACK=1 ack=y+1客户端 -> 服务器ESTABLISHED接收后转为 ESTABLISHED 为什么需要最后一次握手 在最后一次握手前,不能确定服务器本身不能确定自己发送的数据能否被客户端所接收到 如果最后一次的 ACK 包服务器没有接收到,即客户端已经进入了 ESTABLISHED 而服务器仍然没有进入到 ESTABLISHED,此时会发生什么 服务器:由于仍然处于 SYN_RCVD 状态下,当超过一定时间后,服务器会重新发送 SYN+ACK 包,直到达到次数上限后,直接关闭本次连接 客户端:因为已经进入了 ESTABLISHED,所以实际上已经可以发送数据了,当发送数据给服务器时,服务器会发现此连接并未建立,此时服务器将会按照 TCP 规则,发送 RST 包给客户端,当客户端接收到此 RST 包后,关闭连接 TCP 四次挥手 序号数据发送内容发送方向客户端状态服务器状态 0ESTABLISHEDESTABLISHED 1FIN=1 seq=x客户端 -> 服务器发送后转为 FIN_WAIT_1接收后转为 CLOSE_WAIT 2ACK=1 ack=x+1服务器 -> 客户端接收后转为 FIN_WAIT_2CLOSE_WAIT 3FIN=1 seq=y服务器 -> 客户端接收后转为 TIME_WAIT发送后转为 LAST_ACK 4ACK=1 ack=y+1客户端 -> 服务器发送后等待 2MSL 后转为 CLOSED接收后转为 CLOSED 四次挥手的理解 四次挥手可以认为是两次+两次,一次是由客户端发起的,客户端表示自己的数据已经发送完毕了,通过 FIN 包通知服务器,而服务器接收到后通过 ACK 包向客户端回复表示收到。另一次是由服务器发起的,因为客户端发送完成数据并不代表服务器发送完成数据了,所以还有服务器单独发起的 FIN 包,此时客户端发送 ACK 包表示确定 HTTP协议 特点 基于 TCP/IP (从 HTTP 3.0 开始,改为采用 UDP 协议) 由客户端发起请求,服务器进行响应 通过 URL 来区分服务 无状态 无连接 提供了八种方法,其中最常见的两种为 GET 和 POST 端口 HTTP 协议默认为 80 端口,HTTPS 协议为 443 端口 状态码 状态码含义 1XX信息状态码 2XX成功状态码 3XX重定向状态码 4XX客户端错误码 5XX服务器错误码 常见的状态码 状态码含义 200成功 302重定向 404客户端的 URL 不存在 500服务器错误 HTTP 和 HTTPS 的区别 HTTP 采用明文传输的方式,而 HTTPS 采用的是加密的方式传输 HTTPS 通常需要申请证书 GET 和 POST 的区别 GET 通常用于获取数据,GET 的请求参数会附在 URL 后,用 ? 分割 URL 和参数,用 & 分割多个参数,特殊字符进行 base64 转码,以明文的方式显示 POST 通常用于更新数据,POST 的请求参数会放在数据包的 Body 部分,相对更加安全,浏览器不会进行保存 HTTP 1.0 和 1.1 的区别 持久连接 管道机制:同一个连接中,客户端可以发送多个请求 分块传输:服务器每产生一个数据,就发送一个数据 新增了部分请求方式 HTTP 1.1 和 2.0 的区别 完全采用二进制进行传输 完全多路复用 报头压缩 TCP 的可靠传输 TCP 的可靠传输的实现 数据校验 数据的合理分片和排序 滑动窗口机制 发送方保留了已经发送的数据的副本,用于重传 发送方接收到 ACK 确认包后才会丢弃副本 发送方维护一个重传时间,当没有接收到 ACK 确认包时将会进行重传 通过拥塞控制来管理滑动窗口的大小 发送方和接收方商讨发送速度,通过控制发送速度保证接收方有足够的内存空间来接受数据 TCP “粘包” 现象 原因:TCP 是面向字节流的,而且在遇到多个连续的数据包时,会合并至一个数据包内发送,此时接收方将难以识别并读取内部的数据 解决办法:在每个数据包前加上数据包的长度,使得每次读取数据时可以获取到数据包的长度而读出足够长的数据再进行解析

2021/2/23
articleCard.readMore

面试复习(操作系统)

用户态和内核态 什么时候从用户态转为内核态 程序在用户态执行时,当需要进行系统调用的时候,或者遇到异常,或者外围设备引发的中断,如文件读取与写入,程序报错,键盘输入,网络操作等行为时,程序会从用户态转为内核态,直到执行此行为结束时,再返回用户态 为什么要转至内核态 通过限制用户态的权利,使得有限的系统资源能够受到系统的控制与管理,由系统进行资源的分配 用户态和内核态的切换原理 实质上就是中断,保存当前用户态的所有寄存器信息等,然后将代码指针指向中断处理程序 进程和线程 区别 进程是相对于操作系统的最小单位,每个进程都有唯一一个 PID 与之对应,每个进程都有独立的内存空间,代码段,数据段。进程之间相互独立且不会相互影响,一个进程可以包含多个线程。CPU在多个进程之间切换时会带来较大的开销。进程可以由CPU单独启动 线程是相对于处理器的最小单位,单个CPU只能同时处理一个线程,相同进程的线程之间共用内存空间,共用代码段和数据段,线程不可以单独执行,线程没有 PID 用于区别,线程出现错误或者异常时会影响此进程内的所有线程,CPU在同一个进程的线程内切换所带来的开销相对较小 进程之间的通信 管道(无名和有名管道) 消息队列 共享内存 不同的进程可以同时将同一个内存页面映射到自己的地址空间中 信号 套接口(网络) 线程之间的通信 锁机制(互斥锁、读写锁) wait 和 notify violate 进程切换代价 切换页目录 切换内核栈 切换上下文 线程切换代价 切换内核栈 切换上下文 内存 内存寻址是如何实现的 段页式,程序进行分段,包括代码段,数据段等等,每一段再分页,并由 MMU 保存页表 MMU 通过页表将逻辑地址转为物理地址 缓存 缓存列 每次计算机读取数据放入缓存的单位长度 文件系统 iNode 结构 文件树结构保存了一个目录的子文件/目录的名称以及对应的 iNode 号码 需要访问文件内容时,需要通过 iNode 号码来获取文件的详细信息 iNode 不包含文件名信息,指包含文件的 “元信息” 中断

2021/2/23
articleCard.readMore

面试复习(C++)

C++的特性与轨迹 C++ 的特性(C++11及以上) 需要在不同的平台上进行编译 编译后的程序可以在操作系统上直接运行 可以面对过程和面对对象两种设计方式 可以直接操作本地的系统库 可以直接使用操作系统提供的接口 编译后仅对变量的类型进行了存储,不可以进行类似反射的操作 支持无符号整型 变量类型的字节长度受操作系统影响 支持指针、引用、值传递 没有默认提供的 GC 系统 由程序员负责管理变量所储存的位置 严格的 RAII 支持重写、重载,包括运算符的重载 多重继承 支持预编译,编译宏定义 支持函数指针,函数对象,lambda 表达式 C++ 11 新增的特性 foreach auto 自动类型推断 lambda 匿名函数 后置返回类型 override 关键字 nullptr 代替原来的 NULL 当存在 void a(int x); 和 void a(char *x) 时,若使用 a(NULL) 则会调用前者,这与通常的理解不同,而使用 a(nullptr) 则会明确的调用后者 元组 tuple,可以使用 get<>() 取出其中的一个值,或者使用 tie() 装包或解包 struct 和 class 区别 struct 默认使用 public 而 class 默认使用 private struct 可以直接使用 {} 进行初始化,而 class 则需要所有成员变量都是 public 的时候才可以使用 堆和栈的区别 操作系统角度 堆是操作系统为进程所分配的空间,在 C、C++ 语言中用来存放全局变量。由程序员管理,主动申请以及释放的空间,可能会出现内存泄漏。在进程结束后,由操作系统回收 栈是由编译器进行管理,由编译器进行申请,释放的空间,通常比堆要小很多,在 C、C++ 语言中,当调用一个函数时会创建一个栈,当函数结束时则会回收栈的空间 数据结构的角度 堆是一棵完全二叉树,常见的有最大堆和最小堆,以最大堆为例,其满足二叉树中的任意一个节点的孩子节点都比此节点小。通常用来实现优先队列的效果,插入和删除的复杂度均为 O(logN) 栈是一种线性数据结构,满足先进后出的特点,即最先进入的数据最后离开,常见于 DFS 中。也可以通过单调栈的方式求解一些问题。插入和删除的复杂度均为 O(1) 虚函数 虚函数 虚函数由 virtual 标记 普通的虚函数仍然需要进行实现,所有继承此类的派生类可以重新实现此函数也可以不实现 纯虚函数 纯虚函数在普通的虚函数后,加上 =0 当一个类拥有纯虚函数后,则此类变成抽象类,不可以进行实例化 纯虚函数不需要实现,且所有继承自此类的派生类必须实现此函数,否则派生类也是抽象类,不可以实例化 虚函数的实现原理 在类中保存一张虚函数表,表内保存了函数所在的代码段 当其他类继承自此类时,复制一份此虚函数表。当其中的虚函数进行实现后,将虚函数表中此函数的指针指向新的函数的地址 定义类的实例的时候,在类的开头保存了一个指向此虚函数表的指针,当需要调用此函数的时候,通过此指针找到对应的函数地址 静态函数和虚函数 静态函数和虚函数的区别 静态函数在编译时就确定了运行的时机,而虚函数则是在运行的过程中动态的得知虚函数地址 vector 扩容规则 当空间不足的时候,vector 会扩容至当前空间的 2(GCC下)/1.5(MSVC) 为什么这样扩容 以两倍空间为例,当扩容次数为 30 次左右时,vector 的空间达到 1e9 (十亿)而通常每次扩容,都会需要在堆上重新分配空间,需要重新移动整个数组到新的空间。由此,可以得出重新分配空间的次数越少越好,同时也要节约内存的占用,因为按照此增长,其内存的重复的分配次数始终在常数范围内,所以采用了上述的扩容方式。 MSVC下的 1.5 倍的空间相对于 GCC 下的 2 倍有什么好处和坏处 好处:因为 2 倍空间下,任意一个空间都大于之前所有分配过的空间之和,这就意味着每次进行扩容的时候都需要分配一个新的空间。而在 1.5 倍下,可以重复使用之前的空间,1.5 倍相对会节约内存 坏处: 1.5 倍下的重新分配次数更多,也就意味着需要更多的重新分配空间和重新移动的次数,更加浪费时间 clear 的复杂度 复杂度与已有的元素数量成线性,因为每个元素都需要析构 clear 后,并不会改变 vector 的容量上限,只会更新 vector 内的 size 大小 队列和堆栈的模拟 用两个堆栈模拟队列 将两个堆栈命名为 A、B 若 B 堆栈为空,则将 A 堆栈的所有值都推入 B 中 若需要推入,则推入到 A 中 若需要推出,则从 B 中推出 用两个队列模拟堆栈 将两个队列命名为 A、B 若需要推入,则推入到 A 中 若需要弹出,则将 A 中的值除了最后一个,其他都推入到 B 中,且仅留下一个值,然后弹出这个值,并将 A、B 队列命名为 B、A 队列 四个类型转换 转换特点 static_cast普通的转换,与普通的 C 语言的强制类型转换相同,在编译期间进行转换,所以会检查转换是否合法 const_cast去除 const 属性,但是不能去除其本身的 const 属性 reinterpret_cast无条件强制转换 dynamic_cast将基类转换为派生类 三个访问限制 描述 private私有的,仅此类可以访问此属性 protect保护的,仅此类已经此类的派生类可以访问此属性 public公有的,任意对象和方法可以访问此属性 下面这段代码最终结果是 1 2 3 4 main () { printf("xxx"); fork(); } 为 xxxxxx 由于有输出缓存,实际上在 fork 的时候,并没有输出至屏幕,而是保存在缓存中,当程序结束时,将缓存中的值输出至终端,所以得到 xxxxxx static static 变量的特点 该变量在全局数据区分配内存 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化) 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的 指针和引用 指针和引用的区别 指针有自己的内存空间,是一个变量类型,而引用没有占用内存空间,只是一个别名 使用 sizeof 可以求得在 32 位操作系统下,指针的大小为 $4$ 个字节,而引用则为原对象的大小 指针可以初始化为任意正整数值,而引用必须初始化为一个已经存在的变量 参数传递时,指针需要先进行指针转为引用然后再使用,而引用可以直接操作原对象 指针可以有 const 属性,而引用没有 const 属性 指针可以重新赋值,而引用不可以更改 指针可以进行多级指针,而引用只有一级 指针可以引用进行 ++(自增)操作的逻辑和结果都不同 当需要返回动态内存分配的对象时,需要使用指针而不是引用,因为引用可能会产生内存泄漏 平面几何问题 判断一个点是否在一个三角形内 定义三角形为 $ \vartriangle ABC$,点为 $P$,计算 $ S{\vartriangle ABC} = S{\vartriangle ABP} + S{\vartriangle ACP} + S{\vartriangle BCP}$ 是否成立。而三角形面积可以通过割补法或者叉积求 c++智能指针 auto_ptr(已弃用) 采用所有权模式,任何一个 new 的对象只能由一个 auto_ptr 来指向,进行赋值操作会使得原来的指针丢失指向的对象 unique_ptr 与 auto_ptr 相同,但是进行赋值操作时,会直接报错,而 auto_ptr 不会 shared_ptr 共享指针,允许多个指针指向此对象,同时当所有指向此对象的指针都被析构后,此对象将会被删除 weak_ptr 弱共享指针,允许指向其他的 shared_ptr 对象,此指针不会影响 shared_ptr 的析构行为,通常用来避免相互指向问题 构造函数 构造函数有哪些特征 名字和类名相同 没有返回值 生成类的自动执行,不需要调用 为什么构造函数不可以是虚函数 因为虚函数表指针是在构造函数期间创建的,没有虚函数表就没有办法调用虚函数 析构函数 析构函数的作用 如果一个类中有指针,且这个指针指向了一段由此类的实例请求分配的空间,那么需要由析构函数来实现对这块区域的释放,否则会造成内存泄漏 C++ 为什么习惯把析构函数定义为虚函数 当这个类需要作为父类派生的时候,如果程序得到的是此父类的指针,那么此时就无法析构子类,出现内存泄漏 C++ 为什么默认的析构函数不是虚函数 虚函数需要额外的虚函数表和虚函数表指针,对于不会派生的类而言,浪费空间 重载和覆盖 重载和覆盖的区别 重载:两个相同的函数名,但是参数列表不同 覆盖:父类创建的虚函数,派生类重新定义 锁 C++ 中的锁类型 互斥锁:对于同一个变量只允许一个线程进行读写,若不满足时则会进入阻塞,并且 CPU 不会进入忙等 条件锁:当满足某个条件时,再唤醒此线程,否则一直处于阻塞状态 自旋锁:不断的检查锁是否满足条件,不释放 CPU,比较耗费 CPU 读写锁:允许有读锁的时候再加读锁,但是有写锁时不再能加任何锁 递归锁:允许同一个线程对同一个锁进行多次加锁 new和malloc new 和 malloc 的区别 new 是一个 c++ 关键字,不需要头文件支持,而 malloc 是一个函数,需要头文件支持 malloc 需要给出需要的空间大小,而 new 不需要 new 返回的是对象的指针,而 malloc 返回的是 void* 类型的指针 new 分配失败时会抛出错误,而 malloc 失败时返回空指针 new 会调用被构造的类型的构造函数,而 malloc 只是分配内存空间 可以重载 new 操作,但是不能重载 malloc 操作 delete 和 free 的区别 delete 会调用析构函数,而 free 直接回收空间 C++ 编译 从源文件到可执行文件的过程 预处理,宏定义替换 编译,生成汇编代码 汇编,将汇编代码转为机器代码,生成目标文件 链接,将多个目标文件连接成最终可执行文件 内存管理 C++ 的内存分布 代码段:存储机器代码和字符串常量 数据段:存储程序中已初始化的全局变量和静态变量 BSS段:存储未初始化的全局变量和静态变量,以及所有被初始化为 0 的全局变量和静态变量 堆区:调用 new/malloc 函数时动态管理分配的内存,同时需要用 delete/free 来手动释放 栈区:使用栈空间存储的函数的变量和返回值等 映射区:存储动态链接库以及调用 mmap 函数进行的文件映射 C++ 内存泄漏检查 通过 valgrind 检查一个调试程序 valgrind 可以检查出内存泄漏、越界访问、未初始化内存 静态方法 静态方法和实例方法有何不同 调用时,静态方法既可以用 类名.方法名 和 对象名.方法名,而实例方法只能用后者 静态方法只能访问静态变量,而实例方法可以访问静态变量和成员变量 右值引用 如何确定一个值是左值还是右值 提供了地址的为左值,左值可以没有值,但是一定有地址 提供了值的为右值,右值可以没有地址,但是一定有值 右值引用的功能 移动语句 完美转发 更详细的内容见此处 C++ hash C++ 的内置 hash 函数的实现 对于基础变量,hash 函数返回值为此变量的值,不做修改 对于 string,hash 函数对每四个字节(64位操作系统下)进行位运算最终得到结果,实际的内部过程使用了两个特殊的固定值,下面是 C++ 的字符串 hash 函数的实际内部实现(C++11) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 inline std::size_t unaligned_load(const char *p) { std::size_t result; __builtin_memcpy(&result, p, sizeof(result)); return result; } size_t _Hash_bytes(const void *ptr, size_t len, size_t seed = 0xc70f6907UL) { const size_t m = 0x5bd1e995; size_t hash = seed ^len; const char *buf = static_cast<const char *>(ptr); // Mix 4 bytes at a time into the hash. while (len >= 4) { size_t k = unaligned_load(buf); k *= m; k ^= k >> 24; k *= m; hash *= m; hash ^= k; buf += 4; len -= 4; } // Handle the last few bytes of the input array. switch (len) { case 3: hash ^= static_cast<unsigned char>(buf[2]) << 16; [[gnu::fallthrough]]; case 2: hash ^= static_cast<unsigned char>(buf[1]) << 8; [[gnu::fallthrough]]; case 1: hash ^= static_cast<unsigned char>(buf[0]); hash *= m; }; // Do a few final mixes of the hash. hash ^= hash >> 13; hash *= m; hash ^= hash >> 15; return hash; }

2021/2/22
articleCard.readMore

Codeforces Round#699 (Div. 2)

暂时还没有 AK,留坑 A. Space Navigation 大致题意 给你一段在棋盘上的移动指令,问能否通过在不改变原指令顺序,仅删除部分或者全部或者不删除的情况下,到达某个指定地点 分析 可以通过指令直接得出可以到达的范围,比如删除所有的“向左”指令后,能够到达的最右,同理可以得到四个方向的极值,最后判断目标地点是否在极值范围内即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int x, y; string str; cin >> x >> y >> str; int cnt[4] = {0, 0, 0, 0}; for (auto &item : str) { switch (item) { case 'R': cnt[0]++; break; case 'L': cnt[1]++; break; case 'U': cnt[2]++; break; case 'D': cnt[3]++; break; } } if (-cnt[1] <= x && x <= cnt[0] && -cnt[3] <= y && y <= cnt[2]) cout << "YES" << endl; else cout << "NO" << endl; } } B. New Colony 大致题意 给你一段台阶,然后你从第一阶开始往下面滚砖块,砖块遇到下坡就走到下一阶,遇到上坡就停止,并增加这一阶的高度。给你 $n$ 个砖块,请问最后一块砖块停止在哪里 分析 首先这道题如果不能使用暴力的话,是很麻烦的一道题。但是注意观察这道题的数据范围 $$1 \leq n \leq 100, 1 \leq k \leq 10^9, 1 \leq h_i \leq 100, \sum n \leq 100$$ 假如阶梯是一直下坡或者平坡,那么砖块就会直接掉出阶梯。而如果砖块没有掉出阶梯,那么它一定会不会处于整个阶梯的最高处。所以考虑一个可能,即不断的滚落的方块将整个阶梯都填满成平地,则最多需要 $n \times h_i = 10000$ 个砖块 也就是说,当 $k > 10000$ 时,则最后一块砖块必然掉出阶梯。那么当 $k \leq 10000$ 时,暴力求解即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <bits/stdc++.h> using namespace std; #define int long long int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; vector<int> data(n); for (int i = 0; i < n; ++i) cin >> data[i]; if (k > 10000) cout << -1 << endl; else { int last = -1; for (int i = 0; i < k; ++i) { int cur = 0; while (cur < n - 1 && data[cur + 1] <= data[cur]) cur++; if (cur == n - 1) last = -1; else { data[cur]++; last = cur + 1; } } cout << last << endl; } } } C. Fence Painting 大致题意 一个篱笆有 $n$ 个木条组成,每个木条有一个初始的颜色。每一个木条都有一个指定的目标颜色。有 $m$ 个油漆工,每个油漆工都只携带一种颜料,按照顺序来给这个篱笆上色,他们每次必须选择一块且恰好一块木板,把它的颜色变成油漆工所携带的颜色。问每个油漆工应该选择哪一块,使得最后的木条都变成指定的颜色 分析 首先,肯定找出目前没有达到目标的木条,然后找到合适的油漆工给它上色。如果找不到合适的油漆工,那么肯定没戏 接下来是剩下的多出来的没有使用的油漆工,也需要为他们找一块木板去上色。 注意到,由于后来的可以覆盖前面的颜色,所以可以直接考虑最后那个油漆匠。 如果根据上面的操作,这个油漆匠已经有了一块木板去上色的话,那么剩下多出来的油漆工可以都给这块木板上色,这样的话,无论最后上色是什么,最后一个油漆匠一定能把这块木板染回指定的颜色。 而如果最后一个油漆工也在那些没有使用的油漆工内,那么他只需要找到一块最终颜色和自己所携带的颜色相同的,然后指定他去给这块木板上色。剩下的那些没有使用的油漆工也去给这块木板上色即可。 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <bits/stdc++.h> using namespace std; void solve() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<int> a(n), b(n), c(m), res(m, 0); map<int, vector<int>> data; for (int i = 0; i < n; ++i) cin >> a[i]; for (int i = 0; i < n; ++i) { cin >> b[i]; if (a[i] != b[i]) data[b[i]].push_back(i + 1); } for (int i = 0; i < m; ++i) cin >> c[i]; for (int i = 0; i < m; ++i) { auto iter = data.find(c[i]); if (iter == data.end()) continue; res[i] = iter->second.back(); iter->second.pop_back(); if (iter->second.size() == 0) data.erase(iter); } if (res.back() == 0) for (int i = 0; i < n; ++i) if (c.back() == b[i]) res.back() = i + 1; for (auto &item : res) if (item == 0) item = res.back(); if (res.back() == 0 || !data.empty()) cout << "NO" << endl; else { cout << "YES" << endl; for (int i = 0; i < res.size(); ++i) cout << res[i] << " \n"[i == res.size() - 1]; } } } D. AB Graph 大致题意 给你一个完全有向图,每条边都有一个字母标记,标记的字母只有可能是 “a” 或者 “b”。你需要找出一条可以包含重复点重复边的路径,满足这条路径的长度等于给定的要求,并且这条路径组成的字符串为回文串 分析 首先,由于可以有重复点和重复边,那么考虑下面的情况 如果这个图只有两个点。 此时只有两条边。如果这两条边恰好字母还相同(例如下面的 mermaid 图),那么任意长度的路径都可以画出来,只需要在这两个点之间来回走即可 graph LRA -- a --> BB -- a --> A 如果这两条边的字母还不相同(例如下面的 mermaid 图),那么来回的路径为奇数的时候,可以画出来,只需要在这两个点之间来回走即可 graph LRA -- a --> BB -- b --> A 如果这个图至少有三个点 取出整个图中任意三个点,假定这三个点为 $a, b, c$,这里定义 $g(a, b)$ 为从点 $a$ 到点 $b$ 的路径的标记字母。那么我们可以知道,一定满足 $g(a, b) = g(b, c)$ 或 $g(b, c) = g(c, a)$ 或 $g(c, a) = g(a, b)$ 其中至少一个 证明 反证法,假设上述不成立,则 由上述得 $g(a, b) \not = g(b, c)$,那么 $g(a, b)$ 与 $g(b, c)$ 相异。 由上述得 $g(b, c) \not = g(c, a)$,那么 $g(b, c)$ 与 $g(c, a)$ 相异。由于 $g(a, b)$ 与 $g(b, c)$ 相异,而所有边只有两种可能,所以 $g(c, a)$ 与 $g(b, c)$ 必定相同 由上述得 $g(c, a) \not = g(a, b)$,那么 $g(c, a)$ 与 $g(a, b)$ 相异,这与上面的结论相反,所以假设不成立,即,上述结论成立 这时,通过交换 $a, b, c$ 三个点,使得 $g(a, b) = g(b, c)$ 得到满足,则可以得到下图 graph LRa -- x --> bb -- x --> cc -- y --> a 这里,我们不需要关心 $y$ 的情况,无论它是否与 $x$ 相同 这里,只需要进行 $a \rightarrow b \rightarrow c \rightarrow a$ 这样的循环,即可实现一个回文串。只需要确定是从哪一个节点开始循环即可 若从 $a$ 开始循环,则得到的串为 $xxyxxyxxyxx \dots$ 当长度为 $3t + 2$ 时为回文串 若从 $b$ 开始循环,则得到的串为 $xyxxyxxyxxyx \dots$ 当长度为 $3t$ 时为回文串 若从 $c$ 开始循环,则得到的串为 $yxxyxxyxxyxxy \dots$ 当长度为 $3t + 1$ 时为回文串 所以只需要判断所需要的字符串长度与 $3$ 取模即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<string> g(n); for (auto &item : g) item.reserve(n); for (int i = 0; i < n; ++i) cin >> g[i]; if (n == 2) { if (g[0][1] == g[1][0] || m % 2 == 1) { cout << "YES" << endl; for (int i = 0; i < m + 1; ++i) cout << (i % 2) + 1 << " \n"[i == m]; } else { cout << "NO" << endl; } } else { vector<int> tmp(3); for (int i = 0; i < 3; ++i) tmp[i] = i; do { if (g[tmp[0]][tmp[1]] == g[tmp[1]][tmp[2]]) break; } while (next_permutation(tmp.begin(), tmp.end())); assert(g[tmp[0]][tmp[1]] == g[tmp[1]][tmp[2]]); int cur = (m + 1) % 3; cout << "YES" << endl; for (int i = 0; i < m + 1; ++i) cout << tmp[(i + cur) % 3] + 1 << " \n"[i == m]; } } } E. Sorting Books 大致题意 给你一段数列,进行如下的操作使得整个序列中相同的数字处于连续的子段中 操作:将这个数字放到整个字符串的最后 问,最少需要进行多少次操作 分析 首先,如果进行 $n$ 次操作,那么必然可以使得整个序列满足上述的要求。因为只需要按照排序后的顺序来移动所有的数字,则必定可以满足条件 那么接下来就是想办法减少次数。 如果我选出一个值,当数字与这个值相同的时候,不对其进行操作。由于其他值被移动到了最后。所以与这个值相同的值都自然而然的被移动到了最前面,例如下面这个序列 $$1, 3, 2, 2, 1, 3$$ 如果我选择 $3$ 作为固定值,那么当 $1, 2$ 都进行过移动后,$3$ 自然而然的也就满足了条件 再进一步,如果稍微修改一下这个序列,将其改为 $$1, 3, 1, 2, 3, 2$$ 这时,发现当我仅对 $3$ 进行移动之后,$1, 2$ 自然而然的也满足条件了。由此,可以得到一个这样的推广结论: 在某一个子区间内,如果这个子区间内存在某一个数值 $x$,满足整个区间的所有 $x$ 都在这个子区间内,那么,这个 $x$ 可以固定不动,且这个子区间内的其他所有值都需要进行操作。 所以此时可以进行 dp 求解,方法如下: 找出所有值所对应的最小子区间 假如这个子区间内选择这个值作为固定值,那么它所带来的代价就是这个子区间的长度减去这个值的个数 由于需要进行减法比较麻烦,可以改变思路用加法,即求出有多少个值是可以固定不操作的 则对于每一个区间的固定值,那么其带来的固定值数量就是这个区间内的值的个数 对每一位进行 dp 求解即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; vector<int> data(n), l(n + 1, n), r(n + 1), dp(n + 1), cnt(n + 1); for (int i = 0; i < n; ++i) { cin >> data[i]; l[data[i]] = min(l[data[i]], i); r[data[i]] = max(r[data[i]], i); } for (int i = n - 1; i >= 0; --i) { cnt[data[i]]++; dp[i] = max(dp[i + 1], cnt[data[i]]); if (l[data[i]] == i) dp[i] = max(dp[i], cnt[data[i]] + dp[r[data[i]] + 1]); } cout << n - dp[0] << endl; }

2021/2/6
articleCard.readMore

清理 WSL2 的磁盘占用

方法来源:https://github.com/microsoft/WSL/issues/4699#issuecomment-627133168 由于 vhdx 格式的磁盘镜像文件只支持自动扩容但不支持自动缩容,所以需要手动来实现缩容,即利用 Windows 自带的 diskpart 来实现

2021/1/27
articleCard.readMore

Codeforces Round#697 (Div. 3)

自南京区域赛结束之后就一直在准备期末考试,直到最近结束考试之后开始了复建的生活,这场 Div3 除了 D 题因为爆了 int 然后卡了,G题真的没在比赛期间想出来,其他题目都是非常顺利的解决掉了,且只用了一个小时 A. Odd Divisor 题目大意 给你一个整数,请问它是否存在一个不为 1 的奇因子 题解 因为除 1 以外的所有奇因子都可以分解出至少一个奇质因子,那么只需要找到那些不包含奇质因子的数进行排查就行。而不包含奇质因子的数字很明显就是所有的 2 的幂次,所以打表就可以了。注意别忘记范围超过了 int AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <bits/stdc++.h> using namespace std; #define int long long int main() { set<int> err; int cur = 2; for (int i = 0; i < 62; ++i) { err.insert(cur); cur <<= 1; } int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int tmp; cin >> tmp; cout << (err.count(tmp) ? "NO" : "YES") << endl; } } B. New Year’s Number 题目大意 给你一个数,请问它是不是 n 个 2020 和 m 个 2021 相加得到的 题解 把 2021 看成 2020 + 1,那么就变成了 (n + m) 个 2020 和 m 个 1 相加得到,由于 n 肯定是自然数,则只要满足这个数除以 2020 的商(也就是 n + m 部分)大于等于余数(也就是 m)即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int tmp; cin >> tmp; cout << (tmp / 2020 >= tmp % 2020 ? "YES" : "NO") << endl; } } C. Ball in Berland 题目大意 有两组人,分别为 a 和 b ,有 k 对组合,每对组合都是从 a 中选出一个,从 b 中选出一个。你现在需要选出两对组合,使得这两对组合不会发生冲突,即不会出现 a 中的人同时参与了这两个组合或者 b 中的人同时参与了这两个组合或者两者都同时参与 题解 可以用类似容斥的办法解决。因为保证了每一对组合都不同,所以当我选出一对的时候,那么还有 k - cnt[a] - cnt[b] + 1 对我可以选,其中的 cnt 为这个人参与的组合数量。只需要遍历所有的组合,然后对于每对组合进行求解即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int a, b, k; cin >> a >> b >> k; vector<int> ca(a + 1), cb(b + 1); vector<pair<int, int>> p(k); for (int i = 0; i < k; ++i) { int u; cin >> u; p[i].first = u; ca[u]++; } for (int i = 0; i < k; ++i) { int u; cin >> u; p[i].second = u; cb[u]++; } long long ans = 0; for (int i = 0; i < k; ++i) ans += k - ca[p[i].first] - cb[p[i].second] + 1; cout << ans / 2 << endl; } } D. Cleaning the Phone 大致题意 有一组物品,他们有各自的代价和价值,其中代价只有 1 或者 2 两种,请问如何选择物品,使得代价尽可能小的情况下满足所需要的价值 题解 直接考虑枚举,比如枚举选择了 x 件代价为 1 的物品,求出这时候至少需要多少件代价为 2 的物品,然后枚举所有情况,输出最小的情况即可。 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <bits/stdc++.h> using namespace std; #define int long long int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, m; cin >> n >> m; vector<int> mem(n); vector<int> data[2]; for (int i = 0; i < n; ++i) cin >> mem[i]; for (int i = 0; i < n; ++i) { int op; cin >> op; data[op - 1].push_back(mem[i]); } sort(data[0].begin(), data[0].end(), greater<int>()); sort(data[1].begin(), data[1].end(), greater<int>()); int ans = INT_MAX; for (int i = 1; i < data[0].size(); ++i) data[0][i] += data[0][i - 1]; for (int i = 1; i < data[1].size(); ++i) data[1][i] += data[1][i - 1]; auto iter = lower_bound(data[1].begin(), data[1].end(), m); if (iter != data[1].end()) ans = min(ans, (int) (iter - data[1].begin() + 1) * 2); iter = lower_bound(data[0].begin(), data[0].end(), m); if (iter != data[0].end()) ans = min(ans, (int) ((iter - data[0].begin() + 1))); for (int i = 0; i < data[0].size(); ++i) { iter = lower_bound(data[1].begin(), data[1].end(), m - data[0][i]); if (iter != data[1].end()) ans = min(ans, (int) ((iter - data[1].begin() + 1) * 2 + i + 1)); } for (int i = 0; i < data[1].size(); ++i) { iter = lower_bound(data[0].begin(), data[0].end(), m - data[1][i]); if (iter != data[0].end()) ans = min(ans, (int) ((iter - data[0].begin() + 1) + 2 * (i + 1))); } cout << (ans == INT_MAX ? -1 : ans) << endl; } } E. Advertising Agency 大致题意 给你一组数据,要求你从中取出 k 个数据,使得这 k 个数据的之和最大,问有几种取法 题解 首先取最大必然只能从大到小取,直到取满 k 个。但是在取最后几个相同的值的时候,由于有多个选择,则可以产生多个方案。而这个方案数量很明显即为组合数。 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <bits/stdc++.h> using namespace std; typedef long long ll; const int mod = 1e9 + 7; const int N = 1100; ll qpow(ll a, ll b) { ll res = 1; while (b) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res; } ll fac[N], ifac[N]; void init(int siz) { fac[0] = 1; for (int i = 1; i <= siz; i++) fac[i] = i * fac[i - 1] % mod; ifac[siz] = qpow(fac[siz], mod - 2); for (int i = siz; i >= 1; i--) ifac[i - 1] = ifac[i] * i % mod; } ll C(ll n, ll m) { if (m == 0 || n == m) return 1; if (m > n) return 0; if (m == 1) return n; return fac[n] * ifac[m] % mod * ifac[n - m] % mod; } int main() { init(1050); int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n, k; cin >> n >> k; map<int, int> mp; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; mp[tmp]++; } auto iter = mp.rbegin(); while (iter != mp.rend()) { if (k > iter->second) { k -= iter->second; iter++; } else { cout << C(iter->second, k) << endl; break; } } } } F. Unusual Matrix 大致题意 给你两个 01 矩阵,问能否通过下面两个方式将第一个矩阵转为和第二个矩阵一样 将一行的值翻转 将一列的值翻转 题解 由于是翻转相同,那么首先直接对这两个矩阵做异或,可以得到一个矩阵,接下来只需要把这个矩阵给转为只有 0 或者只有 1 的矩阵即可 这时候其实可以模拟,假定这行第一个值为 1 则翻转,否者不翻转,然后最后判定是否为纯 0 矩阵 但是这样太麻烦了,其实可以直接判断相邻两行之间是否相同或者相异,即任意两行或者两列的异或结果全为 0 或者 全为 1 则可以,否则不可以 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<string> data1(n), data2(n); for (int i = 0; i < n; ++i) cin >> data1[i]; for (int i = 0; i < n; ++i) cin >> data2[i]; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) data1[i][j] = (data1[i][j] - '0') ^ (data2[i][j] - '0'); bool flag = true; for (int i = 1; i < n; ++i) { char tmp = data1[i][0] ^ data1[i - 1][0]; for (int j = 0; j < n; ++j) { if ((data1[i][j] ^ data1[i - 1][j]) != tmp) { flag = false; break; } } if (!flag) break; } cout << (flag ? "YES" : "NO") << endl; } } G. Strange Beauty 大致题意 给你一组数列,请问至少需要删除几个数字,使得整个数列的任意两个值满足大数取模小数为 0 题解 利用素数筛的方式来 dp 求算最多能有多少个值能满足此条件,相减就能得到答案 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <bits/stdc++.h> using namespace std; int main() { int _; cin >> _; for (int ts = 0; ts < _; ++ts) { int n; cin >> n; vector<int> cnt(2e5 + 1), dp(2e5 + 1); for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; cnt[tmp]++; } int ans = 0; for (int i = 1; i <= 2e5; ++i) { dp[i] += cnt[i]; for (int j = i + i; j <= 2e5; j += i) dp[j] = max(dp[j], dp[i]); ans = max(ans, dp[i]); } cout << n - ans << endl; } }

2021/1/26
articleCard.readMore

Windows 下的 NTFS 驱动器索引 BUG

NTFS BUG 警告,请千万不要在 Windows 下的命令行中运行此命令,或者以其他等价的方式访问 1 cd c:\:$i30:$bitmap 警告,请千万不要在 Windows 下的命令行中运行此命令,或者以其他等价的方式访问 当你试图进入、访问此目录时,就有机会导致 NTFS 驱动器索引损坏,此问题的触发方式包括但不限于 在极低权限的命令行中执行此命令 在 powershell 中执行此命令 在浏览器中试图访问此 c:\:$i30:$bitmap 地址 其他试图访问此地址的程序 触发后,系统将会提示磁盘错误,需要修复,并提示重启电脑。重启后,电脑将会进入磁盘修复,Windows 将会试图修复此问题 部分电脑可以修复此问题,并且正常进入系统,而部分电脑将会无法修复此 BUG,且无法进入系统。 我的经历 当我第一次得到此命令的时候,我尝试在 Google 中搜索此地址含义,我直接将其输入到 chrome 的地址栏里,然后我直接按下了回车键!!! 然后悲剧诞生了,我的电脑提示我,磁盘出错需要修复 尝试修复-1 使用 Windows 的磁盘修复工具。 驱动卷-属性-工具-检查驱动器中的文件系统错误 提示我需要重启电脑,重启 进入系统前尝试修复错误 没有找到错误,但是无法进入系统 尝试修复-2 直接删除 C 盘,然后通过 DG 等软件重建分区表 进入 DG,备份分区表 直接删除 C 盘 使用 DG 直接找回分区表 找回了分区表,但是重启之后仍然无法进入系统 尝试修复-3 仔细想想,为什么我不能进入 Windows,但是 PE 可以进入 C 盘呢?同样是同一块硬盘,如果真的是 NTFS 的问题,那为什么我的 PE 仍然能够正确的读取出我的硬盘内的文件内容?使用了好几个不同的软件试图扫描 NTFS 的结果均为正常、无错误 开始怀疑这是不是 Windows 因为遇到了这个问题而产生的 BUG 而并非 NTFS 的问题 使用 Dism++,进行恢复系统受损 修复完成重启系统 手动跳过磁盘检查 成功进入系统

2021/1/18
articleCard.readMore

编译原理

引言 编译:将高级语言翻译成汇编语言或机器语言的过程 编译过程的五个阶段 词法分析 语法分析 词义分析与中间代码生成 优化 目标代码生成 编译过程的八个部分 词法分析程序 语法分析程序 语义分析程序 中间代码生成 代码优化程序 目标代码生成程序 错误检查和处理程序 信息表管理程序 文法和语言 基本概念 字母表 字母表 $\sum$ 是一个有穷的符号集合,符号包括了字母、数字、标点符号…… 字母表上的运算 乘积:$\sum_1 \sum_2 = {ab | a \in \sum_1 , b \in \sum_2 }$。即从 $\sum_1$ 中选择一个字符串,和 $\sum_2$ 中的一个字符串连接 幂:即多次进行自我乘积的过程。字母表的 $n$ 次幂指长度为 $n$ 的符号串构成的集合 正闭包:长度正数的符号串构成的集合(任意一个合理的符号串均属于字母表的正闭包)记作:$\sum^+$ 克林闭包:允许长度为 $0$ 的正闭包,记作:$\sum^*$ 串 串是字母表的克林闭包的一个元素,是字母表中符号的一个有穷序列 串的长度通常记作 $|s|$,指的是串 $s$ 中的符号个数 空串是长度为 $0$ 的串,用 $\varepsilon$ 表示,$|\varepsilon | = 0$ 串上的运算 连接 幂 文法的定义 对于文法 $G$,可以定义为 $G = (V_T, V_N, P, S)$ $V_T$:终结符集合,表示的是一系列不可以被符号替换的符号,例如“宾语”不是一个终结符,而“水果”则是一个终结符。非空有穷集 $V_N$:非终结符集合,表示的是一系列一定可以被符号替换的符号,在等式中至少会出现一次在等号左侧的符号。非空有穷集 $P$:产生式/规则,表示的是替换过程,可以表示为 $\alpha \rightarrow \beta$,其中 $\alpha \in (V_N \cup V_T)^*$且至少包含一个非终结符。$\beta \in (V_N \cup V_T)^*$。非空有穷集 $S$:开始符号,表示的是该文法中最大的语法成分,例如 $S = 句子$ 产生式的简写 对于含有相同左部的产生式,可以通过“或”运算简写 例如对于 \alpha \rightarrow \beta_1 , \alpha \rightarrow \beta_2 , \dots , \alpha \rightarrow \beta_n 可以简写为 \alpha \rightarrow \beta_1 | \beta_2 | \dots | \beta_n 符号约定 下列符号是终结符 字母表中排在前面的小写字母,如 a、b、c 运算符,如+、*等 标点符号,如括号、逗号 数字,0、1、……、9 粗体字符串,如id、if 下列符号是非终结符 字母表中排在前面的大写字母,如 A、B、C 字母 S,通常表示开始符号 小写、斜体的名字,如expr、stmt 代表程序构造的大写字母,如 $E$(表达式)、$T$(项)、$F$(因子) 字母表中排在后面的大写字母表示文法符号(终结符或非终结符),如 $X、Y、Z$ 字母表中排在后面的小写字母表示终结符号串,如 $u、v、\dots 、z$ 小写希腊字母表示文法符号串 $\alpha 、\beta 、 \gamma$ 除非特别说明,第一个产生式的左部就是开始符号 语言的定义 推导 从语言的规则推导生成语言的过程 根据规则,可以将符号串推成另一个符号串的过程称为推导。用产生式的右部替换产生式的左部 如果是经过多次推导得到,则记为 $a_0 \Rightarrow^n a_n$ $a_0 \Rightarrow^+ a_n$ 表示经过至少一步推导得到 $a_0 \Rightarrow^* a_n$ 表示经过任意步数推导得到 规约 根据语言的规则识别语言的过程 根据规则,可以将符号串还原成。将产生式的右部替换为产生式的左部 句型和句子 如果 $S \Rightarrow^* \alpha, \alpha \in (V_T \cup V_N)^*$ 则称 $\alpha$ 是语法 $G$ 的一个句型 一个句型中既可以包含终结符,也可以包含非终结符,也可以是空串 如果 $S \Rightarrow^* w, w \in V_T^*$ 则称 $w$ 是 $G$ 的一个句子 句子是不包含任何非终结符的句型 由文法推导出的所有句子构成的集合称为:文法 $G$ 生成的语言,记为 $L(G)$,即 L(G) = \{w | S \Rightarrow^* w, w \in V_T^* \} 文法的分类 0型文法 对于 $\forall \alpha \rightarrow \beta \in P$,满足 $\alpha$ 中至少包含一个非终结符 1型文法(上下文有关文法) 对于 $\forall \alpha \rightarrow \beta \in P$,满足 $|\alpha| \leq |\beta|$ 也可以理解为对于 $\forall \alpha A \beta \rightarrow \alpha \gamma \beta \in P$,其中 $\alpha$、$\beta$ 可以为 $\varepsilon$,满足 $\gamma \neq \varepsilon$(在 $A = S$ 的情况下,此等式可以成立) 2型文法(上下文无关文法) 对于 $\forall \alpha \rightarrow \beta \in P$,满足 $\alpha$ 是一个非终结符 3型文法(正规文法) 对于 $\forall \alpha \rightarrow \beta \in P$,满足 $\alpha$ 是一个非终结符,而 $\beta$ 只能是空串、一个终结符号或者一个终结符号和一个非终结符号 CFG 分析树 分析树是推导的图形化表示 二义性文法 如果一个文法可以为某个句子生成多棵分析树,则称这个文法是二义性的 词法分析 正则表达式 正则表达式(简称:RE)是一种描述正则语言的表示方法,正则表达式可以由较小的正则表达式按照特定规则递归地构建。每个正则表达式 $r$ 定义(表示)一个语言,记为 $L(r)$。这个语言也是根据 $r$ 的子表达式所表示的语言递归定义的 正则表达式的定义 $\varepsilon$ 是一个 RE,L(\varepsilon) = \{ \varepsilon \} 如果 $a \in \sum(字母表)$,则 $a$ 是一个 RE,L(a) = \{a\} 加入 $r$ 和 $s$ 都是 RE,表示的语言分别是 $L(r)$ 和 $L(s)$,则 $r|s$ 是一个 RE,$L(r|s) = L(r) \cup L(s)$ $rs$ 是一个 RE,$L(rs) = L(r)L(s)$ $r^*$ 是一个 RE,$L(r^*) = (L(r))^*$ $(r)$ 是一个 RE,$L((r)) = L(r)$ 可以用RE定义的语言叫做正则语言(regular language)或正则集合(regular set) 对任何正则文法 $G$,存在定义同一语言的正则表达式 $r$ 对任何正则表达式 $r$,存在生成同一语言的正则文法 $G$ 正则定义 给一些RE命名,并在之后的RE中像使用字母表中的符号一样使用这些名字 有穷自动机 具有一系列离散的输入输出信息和有穷数目的内部状态的系统 有穷自动机的分类 确定的有穷自动机 不确定的有穷自动机 确定的有穷自动机(DFA) 对于任意的一个输入,自动机都唯一地确定了下一个状态 定义 $M=(S, \sum, \delta, s_0, F)$ 为一个确定的有穷自动机 $S$:有穷状态集 $\sum$:输入的字母表,即输入的符号集合($\varepsilon \notin \sum $) $\delta$:$\forall s \in S, a \in \sum, \delta(s, a)$ 表示从状态 $s$ 出发,沿着标记为 $a$ 的边所能到达的状态 $s_0$:开始状态,$s_0 \in S$ $F$:接收状态(终止状态)的集合,$F \subseteq S$ 确定的有穷自动机可以通过状态图来表示 初始结点通常用 $\Rightarrow$ 表示,终态结点通常为双圈表示 确定的有穷自动机还可以使用表格来表示其状态,例如上述的状态图可以表示为 状态01 $S_0$$S_1$$S_0$0 $S_1$$S_1$$S_2$0 $S_2$$S_1$$S_0$1 通常,在表格对应行的右端通过01的标注表示这个状态为终态 对于 $\sum^*$ 中的任意符号串 $t$,如果在状态图中存在一条从初态到某一终态的路径,且这条路径上所有弧的标记符连接起来等于 $t$,则称 $t$ 可以被此 DFA 接受 不确定的有穷自动机(NFA) 收到一个符号,可能进入不同的状态 定义 $M=(S, \sum, \delta, s_0, F)$ 为一个确定的有穷自动机 $S$:是一个有穷集,它的每一个元素称为一个状态 $\sum$:是一个有穷字母表,它的每一个元素称为一个输入符号 $\delta$:表示一个转移函数,可以描述为S \times (\sum \cup \{\varepsilon \}) \rightarrow P(S)。$P(S)$ 是 $S$ 的一个子集 $s_0$:$S_0 \in S$,表示自动机的初始态 $F$:接收状态(终止状态)的集合,$F \subseteq S$ NFA 也可以通过状态图来表示和表格进行表示,与 DFA 的状态图类似 DFA 和 NFA 的等价性 对任何非确定的有穷自动机 N ,存在定义同一语言的确定的有穷自动机 D 对任何确定的有穷自动机 D ,存在定义同一语言的非确定的有穷自动机 N 从正则表达式转为 NFA 参考下面两张图 NFA 转 DFA 步骤: 从起始状态开始,通过所有的路径,得到新的状态集的组合 将所有新的状态集组合重新通过路径,重复操作直到没有新的组合 将状态集作为 DFA 的结点,建成 DFA 状态机 如果新的状态包含原来的可接受状态(终止状态),则认为新的状态也是可接受状态 以下图为例 画出如下的表格 序号状态ab 100,11 20,10,11 310 将序号代替状态,重新绘图 DFA 最小化 删除从起始结点开始的不可到达的状态 合并所有的等价状态 两个状态必须同时是可接受状态或不可接受状态 对于所有输入的符号,两个状态都必须转换到等价的状态里 以上方的图为例,易得状态 1 和状态 2 是等价状态,所以合并得到 序号状态ab 1012 211 最终得到 自顶向下语法分析方法 自顶向下的分析思想 从分析树的顶部(根节点)向底部(叶节点)方向构造分析树,可以看成是从文法开始符号S推导出词串w的过程 最左推导 在最左推导中,总是选择每个句型的最左非终结符进行替换,即优先满足表达式左侧 最右推导 在最右推导中,总是选择每个句型的最右非终结符进行替换,即优先满足表达式右侧 自顶向下的语法分析采用最左推导方式 预测分析 预测分析是递归下降分析技术的一个特例,通过在输入中向前看固定个数(通常是一个)符号来选择正确的A-产生式。 可以对某些文法构造出向前看k个输入符号的预测分析器,该类文法有时也称为LL(k) 文法类 预测分析不需要回溯,是一种确定的自顶向下分析方法 文法转换 消除左递归 直接左递归 当出现了类似如下的推导公式时 A \rightarrow A \alpha | \beta 时,此时可以出现左递归的情况,这会导致无法正确的进行最左推导,因为可以出现下面的情况: A \Rightarrow A \alpha \Rightarrow A \alpha\alpha \Rightarrow A \alpha\alpha\alpha \Rightarrow A \alpha\alpha\alpha\alpha \dots 此时可以将上述的推导公式转换为 \begin{cases}A \rightarrow \beta A' \\A' \rightarrow \alpha A' | \varepsilon\end{cases} 将左递归的公式转换为右递归即可 这样的操作的代价是 引入了非终结符 $\varepsilon$ 产生式 间接左递归 例如 \begin{cases}S \rightarrow Aa | b \\A \rightarrow Sd | \varepsilon\end{cases} 此时可以产生这样的推导 S \rightarrow Aa \rightarrow Sda \rightarrow Aada \rightarrow Sdada \dots 这时,应该将改为先进行替换得到 A \rightarrow Aad | bd | \varepsilon\begin{cases}A \rightarrow bdA' | A' \\A' \rightarrow abA' | \varepsilon\end{cases} 多个候选式 当出现 S \rightarrow aAd | aBe 之类的结构,当读入的第一个字符为 $a$ 时,无法确定应该选择哪个产生式的时候,应该进行左公因子提取,即改编为 \begin{cases}S \rightarrow aS' \\S' \rightarrow Ad|Be\end{cases} LL(1) 文法 FIRST 集 $FIRST(A)$ 集表示非终结符 $A$ 能够推导出的所有等式的第一个终结符的集合 FIRST(\alpha) = \{ a | \alpha \Rightarrow a \beta, a \in V_T , \alpha, \beta \in V^* \} 这条等式可以用下面三个原则来求算 对于一个产生式 $A \rightarrow B$而言 若 $B$ 的第一个符号是终结符,则将此终结符加入到 $FIRST(A)$ 中 若 $B$ 的第一个符号是非终结符,则将此非终结符的 $FIRST$ 中除了 $\varepsilon$ 的加入到 $FIRST(A)$ 中 若 $B$ 的第一个符号是非终结符,它的 $FIRST$ 集中含有 $\varepsilon$ 则将下一个符号也进行这三条规则的判断,如果没有,下一个字符了,则将 $\varepsilon$ 加入到 $FIRST(A)$ 中 FOLLOW 集 $FOLLOW(A)$ 集表示非终结符 $A$ 后可以跟随哪些终结符 FOLLOW(A) = \{ a | S \Rightarrow^* \dots A a \dots,a \in V_T \}若 A \Rightarrow^* \dots A,则 \# \in FOLLOW(A) 这两条等式可以用下面三个原则来求算 对于文法开始符号 $S$,则 \# \in FOLLOW(S) 若存在类似 $B \rightarrow \alpha A \beta$ 的表达式,则 FIRST(\beta) - \{\varepsilon\} \subseteq FOLLOW(A) 若存在类似 $B \rightarrow \alpha A$ 或者 $B \rightarrow \alpha A \beta 且 \beta \Rightarrow^* \varepsilon$,则 FOLLOW(B) \subseteq FOLLOW(A) SELECT 集 表示使用某个产生式的选择符号 \begin{cases}SELECT(A \rightarrow \alpha) = (FIRST(\alpha) - \{\varepsilon\}) \cup FOLLOW(A),\alpha \Rightarrow^* \varepsilon \\SELECT(A \rightarrow \alpha) = FIRST(\alpha),\alpha \not\Rightarrow^* \varepsilon\end{cases} 同时满足 SELECT(A \rightarrow \alpha) \cap SELECT(A \rightarrow \beta) = \varnothing LL(1) 方法一 满足 LL(1) 的文法有下面三个条件: 若文法存在语句 A \rightarrow \alpha | \beta 则 不存在终结 $a$ 使得 $\alpha$ 和 $\beta$ 都能推导出以 $a$ 开头的串,即 $FIRST(\alpha) \cap FIRST(\beta) = \varnothing$ $\alpha$ 和 $\beta$ 至多有一个能推导出 $\varepsilon$ 满足下面的等式 \begin{cases}FIRST(\alpha) \cap FOLLOW(A) = \varnothing, \beta \Rightarrow^* \varepsilon \\FIRST(\beta) \cap FOLLOW(A) = \varnothing, \alpha \Rightarrow^* \varepsilon\end{cases} 方法二 对于所有的 A \rightarrow \alpha | \beta 若满足 SELECT(A \rightarrow \alpha) \cap SELECT(A \rightarrow \beta) = \varnothing 则为 LL(1) 非递归的预测分析法(表驱动的预测分析) 首先需要根据 $SELECT$ 集来构建一个分析表。通过表的信息实现语法分析 LL(1) 文法分析示例 以下面的表达式文法为例 \begin{cases}E \rightarrow E + T | T \\T \rightarrow T * F | F \\F \rightarrow i | (E)\end{cases} 消除左递归 首先,消除左递归,易得,前两个式子均为左递归 得到 \begin{cases}E \rightarrow TE' \\E' \rightarrow +TE' | \varepsilon \\T \rightarrow FT' \\T' \rightarrow *FT' | \varepsilon \\F \rightarrow i | (E)\end{cases} 求出 FIRST 得到各个符号的 FIRST 集: 首先根据2、4、5式得到 \begin{cases}FIRST(E') = \{+, \varepsilon\} \\FIRST(T') = \{*, \varepsilon\} \\FIRST(F) = \{i, (\}\end{cases} 然后根据 FIRST 集的求出剩下的 FIRST 集 \begin{cases}FIRST(E) = \{i, ( \} \\FIRST(T) = \{i, ( \}\end{cases} 求出 FOLLOW 集 然后再求出 FOLLOW 集 首先 \# \in FOLLOW(E) 然后根据第一个等式得到 FIRST(E') - \varepsilon \subseteq FOLLOW(T) \Rightarrow FOLLOW(T) = \{+\}FOLLOW(E) \subseteq FOLLOW(E') \Rightarrow FOLLOW(E') = \{\#\} 然后继续重复做,直到没有 FOLLOW 集发生更新为止,最终得到 \begin{cases}FOLLOW(E) = \{ \#, ) \} \\FOLLOW(E') = \{ \#, ) \} \\FOLLOW(T) = \{ \#, +, )\} \\FOLLOW(T') = \{ \#, +, )\} \\FOLLOW(F) = \{ \#, +, ), *\}\end{cases} 求出 SELECT 集 再得到 SELECT 集 \begin{cases}SELECT(E \rightarrow TE') = FIRST(TE') = FIRST(T) = \{i, ( \} \\SELECT(E' \rightarrow +TE') = FIRST(+TE') = FIRST(+) = \{+\} \\SELECT(E' \rightarrow \varepsilon) = (FIRST(\varepsilon) - \{\varepsilon \}) \cup FOLLOW(E') = \{ \#, ) \} \\SELECT(T \rightarrow FT') = FIRST(FT') = FIRST(F) = \{i, (\} \\SELECT(T' \rightarrow * FT') = FIRST(*FT') = FIRST(*) = \{*\} \\SELECT(T' \rightarrow \varepsilon) = (FIRST(\varepsilon) - \{\varepsilon \}) \cup FOLLOW(T') = \{ \#, +, )\} \\SELECT(F \rightarrow i) = FIRST(i) = \{ i \} \\SELECT(F \rightarrow (E)) = FIRST((E)) = FIRST(() = \{ ( \} \\\end{cases} 此时,判断是否为 LL(1) 文法 \begin{cases}SELECT(E' \rightarrow +TE') \cup SELECT(E' \rightarrow \varepsilon) = \varnothing \\SELECT(T' \rightarrow * FT') \cup SELECT(T' \rightarrow \varepsilon) = \varnothing \\SELECT(F \rightarrow i) \cup SELECT(F \rightarrow (E)) = \varnothing\end{cases} 所以是 LL(1)文法 求出分析表 根据 SELECT 得出下表 i+*()# E$\rightarrow TE’$$\rightarrow TE’$ E’$\rightarrow +TE’$$\rightarrow \varepsilon$$\rightarrow \varepsilon$ T$\rightarrow FT’$$\rightarrow FT’$ T’$\rightarrow \varepsilon$$\rightarrow *FT’$$\rightarrow \varepsilon$$\rightarrow \varepsilon$ F$\rightarrow i$$\rightarrow (E)$ 分析输入串 采用三列分析法来分析,使用 # 表示尾部,假设输入的串为 i+i*i 栈剩余输入输出 E#i+i*i# TE'#i+i*i#匹配了表中的 $\rightarrow TE’$ FT'E'#i+i*i#$\rightarrow FT’$ iT'E'#i+i*i#$\rightarrow i$ T'E'#+i*i# E'#+i*i#$\rightarrow \varepsilon$ +TE'#+i*i#$\rightarrow +TE’$ TE'#i*i# FT'E'#i*i#$\rightarrow FT’$ iT'E'#i*i#$\rightarrow i$ T'E' #*i# *FT'E'#*i#$\rightarrow *FT’$ FT'E'#i# iT'E'#i#$\rightarrow i$ T'E'## E'##$\rightarrow \varepsilon$ ##$\rightarrow \varepsilon$ 匹配成功 LL(1) 分析中的出错处理 略 自底向上优先分析 自底向下的分析思想 从分析树的底部(叶节点)向顶部(根节点)方向构造分析树,可以看成是将输入串w归约为文法开始符号S的过程,自底向上的语法分析采用最左归约方式(反向构造最右推导) LR(0) 和 SLR(1) 分析法 L: 对输入进行从左到右的扫描 R: 反向构造出一个最右推导序列 由于 SLR(1) 的操作和 LR(0) 分析法相似,且兼容,所以这里直接写 SLR(1) 的操作过程,LR(0) 文法也可以直接用此方法,得到的结果完全相同 以此文法为例 \begin{cases}S \rightarrow L*L | L \\L \rightarrow LB | B \\B \rightarrow 0 | 1\end{cases} 构造分析表 将所有的或运算式子转换为多个式子 得到 \begin{cases}S \rightarrow L*L \\S \rightarrow L \\L \rightarrow LB \\L \rightarrow B \\B \rightarrow 0 \\B \rightarrow 1\end{cases} 合并相同开始符号 LR分析法不适用于有多个开始符号的产生式,所以应当对上面的产生式进行处理得到 \begin{cases} 0) S' \rightarrow S \\ 1) S \rightarrow L*L \\ 2) S \rightarrow L \\ 3) L \rightarrow LB \\ 4) L \rightarrow B \\ 5) B \rightarrow 0 \\ 6) B \rightarrow 1\end{cases} 每行开头的为编号,后续通过编号来指代产生式 建立状态 对于一个状态,首先,判断.后面是否为非终结符号。如果是,那我们就得找所有由此非终结符推出的产生式,并将它们添加进入此状态里。循环做即可。 使用 . 表示当前匹配到的位置 首先建立初状态,将第一个表达式加入初状态 State 0 State 0 此状态中有 $S’ \rightarrow .S$ 检查 . 后是否为非终结符,得到 $S$,由于 $S$ 是非终结符,所以将 $S$ 的产生式加入此状态得到 \begin{cases}S' \rightarrow .S \\S \rightarrow .L*L \\S \rightarrow .L \\\end{cases} 再检查新加入的,得到 $L$ 也需要加入此状态 \begin{cases}S' \rightarrow .S \\S \rightarrow .L*L \\S \rightarrow .L \\L \rightarrow .LB \\L \rightarrow .B \\\end{cases} 最后发现 $B$ 也是需要加入此状态的得到 State 0 的最终结果 \begin{cases}S' \rightarrow .S \\S \rightarrow .L*L \\S \rightarrow .L \\L \rightarrow .LB \\L \rightarrow .B \\B \rightarrow .0 \\B \rightarrow .1\end{cases} 接下来的每个状态都是从 State 转换过来的,即将小数点向后移动,可以得到不同的状态 根据其状态内的产生式,可以得到 State 0 可以有 $S, L, B, 0, 1$ 这五个转移方式 State 1 设定 State 1 是从 State 0 通过 $S$ 转移过来的 所以可以得到,仅 S' \rightarrow S. 满足,所以 State 1 即只有此产生式,且不可以再转移 注意,State 1 是第一个产生式的最后的结果,所以此状态作为 Accept 状态,简称 acc State 2 设定 State 2 是从 State 0 通过 $L$ 转移过来 所以可以直接得到的有 \begin{cases}S \rightarrow L.*L \\S \rightarrow L. \\L \rightarrow L.B \\\end{cases} 对于第三个式子,其满足条件(.后为非终结符),所以需要把 $B$ 加入此状态 即得到 \begin{cases}S \rightarrow L.*L \\S \rightarrow L. \\L \rightarrow L.B \\B \rightarrow .0 \\B \rightarrow .1\end{cases} 其他 State 不断重复上述步骤,得到下面的图和结果 State 0 =\begin{cases}S' \rightarrow .S \\S \rightarrow .L*L \\S \rightarrow .L \\L \rightarrow .LB \\L \rightarrow .B \\B \rightarrow .0 \\B \rightarrow .1\end{cases}State 1 =\begin{cases}S' \rightarrow S.\end{cases}State 2 =\begin{cases}S \rightarrow L.*L \\S \rightarrow L. \\L \rightarrow L.B \\B \rightarrow .0 \\B \rightarrow .1\end{cases}State 3 =\begin{cases}L \rightarrow B.\end{cases}State 4 =\begin{cases}B \rightarrow 0.\end{cases}State 5 =\begin{cases}B \rightarrow 1.\end{cases}State 6 =\begin{cases}S \rightarrow L*.L \\L \rightarrow .LB \\L \rightarrow .B \\B \rightarrow .0 \\B \rightarrow .1\end{cases}State 7 =\begin{cases}L \rightarrow LB.\end{cases}State 8 =\begin{cases}S \rightarrow L*L. \\L \rightarrow L.B \\B \rightarrow .0 \\B \rightarrow .1\end{cases} graph LRA[State 0] -- S --> B[State 1]A[State 0] -- L --> C[State 2]A[State 0] -- B --> D[State 3]A[State 0] -- 0 --> E[State 4]A[State 0] -- 1 --> F[State 5]C[State 2] -- * --> G[State 6]C[State 2] -- B --> H[State 7]C[State 2] -- 0 --> E[State 4]C[State 2] -- 1 --> F[State 5]G[State 6] -- L --> I[State 8]G[State 6] -- B --> D[State 3]G[State 6] -- 0 --> E[State 4]G[State 6] -- 1 --> F[State 5]I[State 8] -- B --> H[State 7]I[State 8] -- 0 --> E[State 4]I[State 8] -- 1 --> F[State 5]B[State 1] --> Accept([Accept]) Accept通常状态不需要画出 创建LR分析表 由此图和上面的集合可以画出表格 状态 ACTION GOTO 0 1 * $ S L B 0 s4 s5 1 2 3 1 acc 2 s4 s5 s6 r2 7 3 r4 r4 r4 r4 4 r5 r5 r5 r5 5 r6 r6 r6 r6 6 s4 s5 8 3 7 r3 r3 r3 r3 8 s4 s5 r1 7 表的构建原则: 首先,表头分为两列,ACTION 和 GOTO,在 ACTION 下均为终结符,而在 GOTO 下均为非终结符,其中在 ACTION 下还需要加上 $ 状态标识匹配结束符(在LL(1)文法中,使用了 # 作为结束符,实际上两种方法均可作为结束符,只需要在题目中注明即可) 对于每一个状态,如果它在图中存在任何转移的方向,则将此转移方向填入表格中。如果转移状态为非终结符,则直接填入对应状态的序号,如果为终结符,则格式为 sn 其中 n 为转移的目标状态 以图中 State 0 为例,其可以通过 $B$ 转移至 State 3,而 $B$ 是一个非终结符,所以在状态为 0 的那一行的第 $B$ 类填上 3 即可 以图中 State 0 为例,其可以通过 $0$ 转移至 State 4,所以在状态为 0 的那一行的第 $0$ 列填上 s4 若此状态中存在任何一个产生式满足 . 在此产生式的最后 假定此状态内的等式有两类,则进行如下操作(此时可以确定,此文法不可能为 LR(0),但是可以是SLR(1)) 既有满足条件的,又有不满足条件的时候 \begin{cases}A_1 \rightarrow \alpha_1 . a_1 \beta_1 \\A_2 \rightarrow \alpha_2 . a_2 \beta_2 \\\dots \\B_1 \rightarrow \gamma_1 . \\B_2 \rightarrow \gamma_2 . \\\dots\end{cases} 显然,前面的产生式为不满足条件的产生式,后面的产生式均为满足条件的产生式 若均满足下列条件,则认为可以通过 SLR 分析法处理,否则认为不可解 \begin{cases}\forall FOLLOW(B_i) \cap \{a_1, a_2 \dots \} = \varnothing \\\forall FOLLOW(B_i) \cap \forall FOLLOW(B_j) = \varnothing\end{cases} 若满足上述条件,则对于 ACTION 列中的每一项 $a$ 若 $a \in {a_1, a_2 \dots }$,则采用 sn 的标识方式,即 若 $a \in FOLLOW(B_i)$,则在所在列标注上 rn,其中 n 指代第几号产生式,这条产生式为 $B_i \rightarrow \gamma_i$ 若此状态内的等式只有一类,即只有满足条件的,则在其所有的 ACTION 列中用 rn 标注(若只有此条件的状态,则此时可以称文法为 LR(0) 文法) 以 State 2 为例,有四个式子不满足条件,仅一个式子满足条件。其中 $S \rightarrow L.$ 为满足条件的式子,剩下四个均不满足条件。所以我们先求出 $S$ 的 FOLLOW(S) = \{\$\},满足等式 FOLLOW(S) \cap \{*, B, 0, 1\} = \varnothing 接着遍历所有 ACTION 内的符号,对于 $0$ 而言,其属于 \{*, B, 0, 1\} 所以,写入 s4 对于 $\$$ 而言,其属于 FOLLOW(S),所以写上 $S \rightarrow L$ 这个产生对应的序号,即 r2 展望符。其表示 $A$ 后面必须紧跟的终结符,其通常是 FOLLOW(A) 的真子集。 - LR(1) 中的 1 表示的即为此展望符的长度 - 当 $\beta \neq \varepsilon$ 时,此展望符没有任何作用 - 当 $\beta = \varepsilon$ 时,当且仅当下一个符号属于 $a$ 时,才可以用此产生式进行规约 - 若存在 $B \rightarrow \gamma$ 则其展望符为 $FIRST(\beta a)$,当 $\beta \Rightarrow^* \varepsilon$ 时,此时展望符为 $a$此时,再进行类似 LL(0) 文法的分析操作,以下面的文法为例$$\begin{cases}S \rightarrow L=R | R \\L \rightarrow *R | id \\R \rightarrow L\end{cases}$$此处省略过程,直接得到答案### 处理后的产生式为$$\begin{cases} 0) S' \rightarrow S \\ 1) S \rightarrow L=R \\ 2) S \rightarrow R \\ 3) L \rightarrow *R \\ 4) L \rightarrow id \\ 5) R \rightarrow L\end{cases}$$### 每个状态的产生式为$$State 0\begin{cases}S' \rightarrow .S, \$ \\S \rightarrow .L=R, \$ \\S \rightarrow .R, \$ \\L \rightarrow .*R, =/\$ \\L \rightarrow .id, =/\$ \\R \rightarrow .L, =/\$\end{cases}$$$$State 1\begin{cases}S' \rightarrow S.,\$\end{cases}$$$$State 2\begin{cases}S \rightarrow L.=R,\$ \\R \rightarrow L.,\$\end{cases}$$$$State 3\begin{cases}S \rightarrow R.,\$\end{cases}$$$$State 4\begin{cases}L \rightarrow *.R,=/\$ \\R \rightarrow .L, =/\$ \\L \rightarrow .*R, =/\$ \\L \rightarrow .id, =/\$\end{cases}$$$$State 4\begin{cases}S \rightarrow L=.R,\$ \\R \rightarrow .L, \$ \\L \rightarrow .*R, \$ \\L \rightarrow .id, \$\end{cases}$$ --> LR(1) 和 LALR(1) 略 语法制导翻译 即将上面的那些产生式带入实际的使用中 例如可以通过下面的文法来描述 C 语言的定义一个变量的过程 产生式语义规则 $S \rightarrow TL$L的类型为T $T \rightarrow int$T为int $T \rightarrow double$T为double $L \rightarrow L, id$创建一个新的 L_{右},将其类型设置为 L_{左} 相同的类型。并创建一个变量,其类型为 L_{左} 的类型,名字为 id $L \rightarrow id$创建一个变量,其类型为 L_{左} 的类型,名字为 id 改为用属性来描述,则可以得到如下表格 产生式语义规则 $S \rightarrow TL$L.type = T.type $T \rightarrow int$T.type = int $T \rightarrow double$T.type = double $L \rightarrow L, id$L(右).type = L.type CreateVar(type = L.type, name = id.name) $L \rightarrow id$CreateVar(type = L.type, name = id.name) 语法制导定义(SDD) 语法制导翻译方案(SDT) SDD(语法制导定义) 综合属性和继承属性 对于一个产生式产生的语义规则中,如果产生式左部的属性是仅通过右部的属性得到的,则称此属性为综合属性。而如果产生式右部的属性是通过右部的属性或者左部的属性得到的,则称为继承属性 S-属性定义 与 L-属性定义 S-属性文法 仅仅使用综合属性的SDD称为S属性的SDD,或S-属性定义、S-SDD L-属性文法 一个SDD是L-属性定义(L-SDD),当且仅当它的每个属性要么是一个综合属性,要么是满足如下条件的继承属性:假如存在一个产生式 $A \rightarrow X_1 X_2 X_3 \dots$,若存在一个 $X_i$,它的一个属性值与下面的有关 $A$ 的继承属性 来自 X_1, X_2, X_3, \dots , X_{i-1} 的属性 $X_i$ 自身的属性,但不能形成死循环 例如上面的图片即为 L-SDD SDT(语法制导翻译方案) 语法制导翻译方案(SDT)是在产生式右部中嵌入了程序片段(称为语义动作)的CFG(上下文无关文法) 例如 \begin{cases}D \rightarrow T \{L.type = T.type \} L \\T \rightarrow int \{T.type = int \} \\T \rightarrow double \{T.type = double \} \\L \rightarrow \{ L_1.type = L.type \} L_1, id\end{cases} 嵌入规则如下 将计算某个非终结符号A的继承属性的动作插入到产生式右部中紧靠在A的本次出现之前的位置上 将计算一个产生式左部符号的综合属性的动作放置在这个产生式右部的最右端 使用时,当在进行规约操作时,需要同时执行此程序片段 中间代码生成 中间代码举例 AST,抽象语法树 TAC,三地址码,四元式 P-code,特别用于 Pasal 语言实现 Bytecode,Java 编译器的输出 SSA,静态单赋值形式 典型语句的翻译(四元式) 赋值语句 1 x = b * (c + d) + a (+, c, d, t1) (*, b, t1, t2) (+, t2, a, t3) (=, t3, , x) 布尔表达式 1 (a > b) && (c < d) || (e < f) && (!g) (j>, a, b, 3) (j, , ,5) (j<, c, d, true) (j, , , 5) (j<, e, f, 7) (j, , , false) (jnz, g, , true) (j, , , false) 条件语句 1 if (a > 0) x = x + 1 else x = 4 * (x - 1) (j>, a, 0, 3) (j, , , 6) (+, x, 1, t1) (=, t1, , x) (j, , , 9) (-, x, 1, t2) (*, 4, t2, t3) (=, t3, , x) 9. 运行存储分配 对于那些在编译时刻就可以确定大小的数据对象,可以在编译时刻就为它们分配存储空间,这样的分配策略称为静态存储分配 反之,如果不能在编译时完全确定数据对象的大小,就要采用动态存储分配的策略。即在编译时仅产生各种必要的信息,而在运行时刻,再动态地分配数据对象的存储空间 使用过程(或函数、方法)作为用户自定义动作的单元的语言,其编译器通常以过程为单位分配存储空间 过程体的每次执行称为该过程的一个活动(activation) 过程每执行一次,就为它分配一块连续存储区,用来管理过程一次执行所需的信息,这块连续存储区称为活动记录(activation record) 静态存储分配 静态存储分配中,由编译器决定其在程序中的位置,每次运行时,其位置都相同 限制条件 数组上下界均为常数 不允许过程的递归调用 不允许动态建立数据实体 顺序分配法 为每个静态过程都逐段分配存储空间,每个过程的内存空间都相互独立且不相交 优点:处理上简单 缺点:对内存空间的使用不够经济合理 层次分配法 通过对过程间的调用关系进行分析,凡属无相互调用关系的并列过程,尽量使其局部数据共享存储空间 栈式存储分配 将内存认为是一个栈空间 当一个过程被调用时,向栈中推入一个活动记录,当此过程结束时,该记录被弹出栈 活动树 用来描述程序运行期间控制进入和离开各个活动的情况的树称为活动树。在表示过程 p 的某个活动的结点上,其子结点对应于被 p的这次活动调用的各个过程的活动。按照这些活动被调用的顺序,自左向右地显示它们。一个子结点必须在其右兄弟结点的活动开始之前结束 调用序列和返回序列 暂略 非局部数据的访问 暂略 堆式存储分配 暂略 符号表 暂略 代码优化 流图 基本块 控制流只能从基本块的第一个指令进入该块。也就是说,没有跳转到基本块中间或末尾指令的转移指令 除了基本块的最后一个指令,控制流在离开基本块之前不会跳转或者停机 划分方法: 确定首指令 第一个指令是首指令 任何一个条件或无条件转移的指令的目标指令是一个首指令 紧跟在一个条件或无条件转移指令之后的指令是一个首指令 任意两个相邻的首指令之间的记为一个基本块 例如对于代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 i = m - 1; j = n; v = a[n]; while (1) { do i = i + 1; while(a[i] < v); do j = j - 1; while(a[j] > v); if (i >= j) break; x = a[i]; a[i] = a[j]; a[j] = x; } x = a[i]; a[i] = a[n]; a[n] = x; 可以划分出 6 个基本块 B1: 1 2 3 i = m - 1; j = n; v = a[n]; B2: 1 2 i = i + 1; if a[i] < v goto B2 B3: 1 2 j = j - 1; if a[j] > v goto B3 B4: 1 if i >= j goto B6 B5: 1 2 3 4 x = a[i]; a[i] = a[j]; a[j] = x; goto B2 B6: 1 2 3 x = a[i]; a[i] = a[n]; a[n] = x; 流图 根据上面的基本块,再根据其转跳关系,可以绘制流图 graph LRB1 --> B2B2 --> B2B2 --> B3B3 --> B3B3 --> B4B4 --> B5B4 --> B6B5 --> B2 常用的代码优化方法 暂略 基本块的优化 将基本块通过 DAG(有向无环图) 表示 例如对于代码 1 2 3 4 a = b + c; b = b - d; c = c + d; e = b + c; 可以绘制出如下的 DAG 图(通常在图上标注运算符号而不是字母,这里为了更容易理解标注了字母) graph LRb0((b)) --> a0((a))c0((c)) --> a0((a))b0((b)) --> b1((b))d0((d)) --> b1((b))c0((c)) --> c1((c))d0((d)) --> c1((c))c1((c)) --> e0((e))b1((b)) --> e0((e)) 若结果 e 是需要返回的值,即其他基本块需要使用的值,则通过 DAG 图可知,变量 a 是无用的,可以删除 得到新的图为 graph LRb0((b)) --> b1((b))d0((d)) --> b1((b))c0((c)) --> c1((c))d0((d)) --> c1((c))c1((c)) --> e0((e))b1((b)) --> e0((e)) 所以可以得到优化后的代码为 1 2 3 b = b - d; c = c + d; e = b + c; 数据流分析 略 到达定值分析 暂略 代码优化技术 依优化范围划分 窥孔优化:局部的几条指令范围内的优化 局部优化:基本块范围内的优化 全局优化:流图范围内的优化 过程间优化:整个程序范围内的优化 依优化对象划分 目标代码优化:面向代码优化 中间代码优化:面向程序的中间表示 源级优化:面向源程序 依优化侧面划分 指令调度 寄存器分配 存储层次优化 存储布局优化 循环优化 控制流优化 过程优化

2021/1/7
articleCard.readMore

计算机网络复习

概述 计算机网络 21世纪的重要特征是:数字化、网络化、信息化,以网络为核心的信息时代 三类网络:电信网络、有线电视网络、计算机网络 互联网的特点:连通性和共享 连通性:互联网使得上网用户之间,不管相距多远,都可以非常便捷、非常经济地交换信息 共享:信息共享、软件共享、硬件共享 互联网 计算机网络由若干结点(node)和连接这些结点的链路(link)组成 internet 是一个通用名词,它泛指由多个计算机网络互连而成的计算机网络 Internet 是一个专有名词,它指当前全球最大的、开放的、由众多网络相互连接而成的特定互连网,它采用 TCP/IP 协议族作为通信的规则,且其前身是美国的 ARPANET 互联网的三个阶段 从单个网络 ARPANET 向互连网发展的过程,TCP/IP 协议成为 ARPANET 上的标准协议 建成了三级结构的互联网,分为主干网、地区网、校园网(企业网) 形成了多层次 ISP 结构的互联网 ISP:网络服务提供商,例如中国电信 ISP分为不同的层次:主干 ISP、地区 ISP、本地 ISP 主干 ISP 服务面积大,拥有高速主干网 地区 ISP 通过一个或多个主干 ISP 连接起来,部分大公司直接接入地区 ISP 本地 ISP 负责给用户提供直接的服务,用户数据经由本地 ISP、地区 ISP、主干 ISP、地区 ISP、本地 ISP 发送至目标用户 IXP:互联网交换点,将两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。使得互联网上的数据流量分布更加合理,减少了分组转发的迟延时间,降低了分组转发的费用 万维网(WWW)被广泛使用在互联网上,大大方便广大非网络专业人员对网络的使用 互联网的标准化工作由互联网协会(ISOC)进行管理 互联网体系结构委员会(IAB):负责管理互联网有关协议的开发 互联网工程部(IETF) 互联网研究部(IRTF) 互联网的组成 边缘部分:由所有连接在互联网上的主机(端系统 end system)组成。这部分是用户直接使用的,用来进行通信和资源共享 核心部分:由大量网络和连接这些网络的路由器组成。这部分是为边缘部分提供服务的 边缘部分 互联网的通信方式: 客户(Client)-服务器(Server)(C/S方式):主机A运行客户程序而主机B运行服务器程序,在这种情况下,A是客户而B是服务器。客户A向服务器发出服务请求,而服务器B向客户A提供服务。客户是服务请求方,而服务器是服务提供方。 对等方式(P2P方式 peer-to-peer):两台主机都运行了对等连接软件(P2P软件),他们可以进行平等的、对等连接通信 互联网的核心部分(通信交换方式) 在互联网核心部分起特殊作用的是路由器。路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能 电路交换 使用电路交换通话之前,必须先拨号请求连接。当被叫用户听到交换机振铃音并摘机后,从主叫端到被叫端就建立了一条连接,也就是一条专用的物理通路。这条连接保证了双方通话时所需的通信资源,而这些资源在双方通信时不会被其他用户占用。在通话的全部时间内,通话的两个用户始终占用端到端的通信资源。以下三个步骤称为电路交换 建立连接(占用通信资源) 通话(一直占用通信资源) 释放资源(归还通信资源) 如果用户在拨号呼叫时电信网的资源已不足以支持此次呼叫,则主叫用户会听到忙音,表示电信网不接受用户的呼叫(例如对方正在通话中) 但使用电路交换来传送计算机数据时,其线路的的传输效率往往很低。这是因为计算机的数据是突发式地出现在传输线路上的,因此线路上真正用来传送的数据时间往往不到10%,甚至1%。浪费通信线路资源 分组交换 分组交换采用存储转发技术,我们把要发送的整块数据称为一个报文(message),在发送端,先把较长的报文划分成较短的、固定长度的数据段,每一个数据段前面添加上首部(header)构成分组(packet)。分组又称为包,分组的首部也可以称为包头。以下三个步骤 路由器/交换机接受数据包(存储) 路由器/交换机查看数据段的首部,决定数据包接下来应该发送给哪个路由器 路由器/交换机发送数据包(转发) 优点: 高效:在分组传输的过程中动态分配传输带宽,对通信链路是逐段占用 灵活:为每一个分组独立地选择最合适的转发路由 迅速:以分组作为传送单位,可以不先建立连接就能向其他主机发送分组 可靠:保证可靠的网络协议,分布式多路由的分组交换网,使网络有很好的生存性 缺点: 分组在各路由器存储转发时需要排队,会造成一定的时延 分组交换需要携带控制信息 注:图中的报文交换即为将报文不拆分为较短的、固定长度的数据段的报文交换方式 计算机网络类别 按照作用范围分 广域网(WAN):广域网的作用范围通常为几十到几千公里,是互联网的核心部分,其任务是通过长距离运送主机发送给的数据,速度高、通信量大 城域网(MAN):作用范围通常为一个城市,可以跨越几个街区甚至整个城市。目前城域网很多采用以太网技术,因此有时也常常并入局域网的范围 局域网(LAN):一般工作范在1km左右 个人区域网(PAN):个人网络,通常只有10m左右 按照使用者分 公用网(public network):指电信公司出资建造的大型网络,这种网络通常是提供给所有愿意缴纳费用的人 专用网(private network):某个部门为了满足本单位的需要而建造的网络,这种网络不向本单位以外的人提供服务 用来把用户接入到互联网的网络 这种网络称为接入网(AN),即与宽带接入技术有关的网络 网络性能 速率 速率的单位为 bit/s,有时写作bps,含义为:每秒传输的比特位个数。注意,这与通常提及的网速单位不同,通常网速的单位为 B/s 而不是 b/s $$1Gbps = 1000Mbps = 10^6 Kbps = 10^9 bps$$$$1 B/s = 8 bps$$ 带宽 通常带宽指代某个信号具有的频带宽度,单位为Hz 在计算机网络中,带宽和速率同义,单位为bit/s 吞吐量 在单位时间内,通过某个网络的实际数据量 时延 指数据从网络的一段传送到另一端所需要的时间 发送时延:主机或路由器发送数据帧需要的时间。从发送数据帧的第一个比特开始到最后一个比特发送完毕所需要的时间 $发送时延 = \frac{数据帧长度(bit)}{速率(bit/s)}$ 传播时延:电磁波在信道中传播一定的距离需要花费的时间 $传播时延 = \frac{链路长度(m)}{数据在链路上的传播速度(m/s)}$ 处理时延:主机或路由器收到分组后需要花费时间进行处理。包括分析数据首部、提取数据部分、差错检验、寻找下一个路由 排队时延:分组经过网络传输时,在进入路由器后需要在输入队列中等待处理,待路由器确定了转发接口后,还要在输出队列中等待转发 总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延 提高数据的发送速率只能减小数据的发送时延 时延带宽积 表示通信管道链路中总共可以容纳多少个比特 时延带宽积 = 传播时延 \times 带宽 往返时间 RTT 有效数据率 = \frac{数据长度}{发送时间 + RTT} 利用率 利用率有信道利用率和网络利用率两种 信道利用率:某信道有百分之几的时间是被利用的(有数据通过)。信道利用率并非越高越好,信道利用率越大,则该信道的时延就会增加。令 $D_0$ 表示网络空闲时的时延,$D$ 表示网络当前的时延,信道利用率用$U$表示,则D = \frac{D_0}{1-U}通常较大的主干网的 ISP 的信道利用率不超过 50% 网络利用率:全网络的信道利用率的加权平均值 其他非性能特征 费用 质量 标准化 可靠性 可拓展性和可升级性 易于管理和维护 计算机网络体系结构 网络协议 规定了所交换数据的格式以及有关的同步问题规则称为网络协议,主要由下面三个要素构成 语法:数据与控制信息的结构或格式 语义:需要发出何种控制信息,完成何种动作以及做出何种响应 同步:事件实现顺序的详细说明 网络层次 分层的好处: 各层之间是独立的 灵活性好 结构上可分割开 易于实现和维护 能促进标准化工作 差错控制 流量控制 分段和重装 复用和分用 连接建立和释放 五层协议结构 应用层(application layer) 通过应用进程间的交互来完成特定网络应用。对于不同的网络应用需要有不同的应用层协议。例如 DNS(域名系统),HTTP(万维网应用),SMTP(电子邮件协议) 运输层(transport layer) 负责向两台主机中进程通信提供通用的数据传输服务。主要有两种协议: TCP(传输控制协议):面向连接的、可靠的数据传输服务。其数据传输的单位是报文段 UDP(用户数据报协议):提供无连接的、尽最大努力的数据传输服务,不保证数据传输的可靠性。其数据传输的单位是用户数据报 网络层(network layer) 负责为分组交换网上的不同主机提供通信服务,将运输层产生的报文段或用户数据封装成分组或包进行传送。IP 协议 数据链路层(data link layer) 将IP数据报组装成帧(framing),添加控制信息(同步信息、地址信息、差错控制)。检查所收到的帧中是否有差错。与 mac 地址有关 物理层(physical layer) 传输比特流 实体、协议、服务 和服务访问点: 实体(entity)——表示任何可发送或接收信息的硬件或软件进程。 协议——控制两个对等实体进行通信的规则的集合。 在协议的控制下,两个对等实体间的通信使得本层能够向上一层提供服务。 要实现本层协议,还需要使用下层所提供的服务。 下面的协议对上面的服务用户是透明的。 协议是“水平的”,即协议是控制对等实体之间通信的规则。 服务是“垂直的”,即服务是由下层向上层通 过层间接口提供的。 同一系统相邻两层的实体进行交互的地方,称为服务访问点 SAP (Service Access Point) 协议必须把所有不利的条件事先都估计到,而不能假定一切都是正常的和非常理想的,必须非常仔细地检查这个协议能否应付各种异常情况。 物理层 物理层的主要任务是确定与传输媒体的接口有关的一些特性 机械特性:指明接口所用接线器的形状和尺寸、引脚数目和排列、固定和锁定装置等 电器特性:指明在接口电缆的各条线上出现的电压的范围 功能特性:指明某条线上出现的某一电平的电压的意义 过程特性:指明对于不同功能的各种可能事件出现的顺序 数据通信的基础知识 数据通信模型 一个数据通信系统可以划分为三大部分 源系统 源点:源点设备产生要传输的数据 发送器:调制器(调制解调器) 传输系统 目标系统 接收器:解调器(调制解调器) 终点 通信系统中的术语 消息(message):运送消息的实体 信号(signal):数据的电气或电磁的表现 模拟信号(连续信号):消息的参数取值是连续的 数字信号(离散信号):消息的参数取值是离散的 单向通信(单工通信):只能有一个方向的通信而没有反方向的交互 双向交替通信(半双工通信/单工):双方都可以发送信息,但是双方不能同时发送信息 双向同时通信(全双工通信):双方可以同时发送给和接受信息 基带信号:由源点产生的信号。通常包含有低频甚至直流成分,需要通过调制器进行调制(modulation) 调制:对基带信号进行的变换 基带调制(编码):仅仅对基带信号的波形进行变换,使它能够与信道特性相适应,变换后仍然是基带信号 不归零制:正电平表示1,负电平表示0 归零制:正脉冲表示1,负脉冲表示0 曼切斯特编码:周期中心向上跳为0,周期中心向下跳为1 差分曼切斯特编码:位开始边界有跳位0,没有跳变为1。每个位中间必须进行一次跳变 带通调制:使用载波(carrier)进行调制,将基带的信号频率范围搬移到高频率段,并转换为模拟信号(为了达到更高的信息传输速率,通常采用复杂的多元振幅相位混合调制方法QAM) 调幅(AM):载波的振幅随着基带数字信号变化而变化 调频(FM):载波的频率随着基带数字信号变化而变化 调相(PM):载波的初始相位随基带数字信号变化而变化 信道的极限容量 信道能够通过的频率范围:在任何信道中,,码元传输的速率是有上线的,传输速率超过此上线,就会出现严重的码间串扰的问题,使接收端对码元的识别称为不可能 信噪比:所有电子设备和通信信道中,会随机产生噪音。如果信号相对较强,则噪音的影响就相对较小。信噪比(dB) = 10 \times log_{10}(\frac{信号的平均功率(S)}{噪音的平均功率(N)}) (dB) 信道极限信息传输速率$C$为C = W \times log_2(1 + \frac{S}{N}) (bit/s)其中,$W$ 为信道的带宽(Hz),$S$ 为信道内所传信号的平均功率,$N$ 信道内部的(高斯)噪音的功率 物理层下面的传输媒体 导引型传输媒体: 双绞线:屏蔽双绞线,无屏蔽双绞线 同轴电缆 光缆 单模光纤:纤芯细,光线不会经过多次反射,成本高,传输距离远 多模光纤:可以同时传输多个光路,容易失真,只适合近距离传输 非导引型传输媒体 短波 微波 卫星 信道复用技术 频分复用:所有用户在同样的时间占用不同的带宽(频率带宽)资源 时分复用:将时间划分为一段段等长的时分复用帧(TDM 帧)。每一个时分复用的用户在每一个 TDM 帧中占用固定序号的时隙。每一个用户所占用的时隙是周期性地出现(其周期就是 TDM 帧的长度)。TDM 信号也称为等时(isochronous)信号。时分复用的所有用户是在不同的时间占用同样的频带宽度。 统计时分复用(提供时分复用的利用率) 波分复用:光的频分复用。 码分复用CDM:常用的名词是码分多址 CDMA。各用户使用经过特殊挑选的不同码型,因此彼此不会造成干扰。这种系统发送的信号有很强的抗干扰能力,其频谱类似于白噪声,不易被敌人发现。每一个比特时间划分为 m 个短的间隔,称为码片(chip)。当码片序列长度为$m$ bit发送的信息的速率为$b$ bit/s,则实际的发送速率要达到$mb$ bit/s。CDMA 的重要特点:每个站分配的码片序列不仅必须各不相同,并且还必须互相正交(orthogonal)。在实用的系统中是使用伪随机码序列。 数字传输系统 略 宽带接入技术 略 数据链路层 链路层使用的信道分为两种类型: 点对点信道:一对一 广播信道:一对多 使用点对点信道的数据链路层 数据链路和帧 链路(link):是一条无源的点到点(一个节点到相邻节点)的物理线路段,中间没有任何其他的交换结点。一条链路只是一条通路的一个组成部分。 数据链路(data link):除了物理线路外,还必须有通信协议来控制这些数据的传输。若把实现这些协议的硬件和软件加到链路上,就构成了数据链路。最常用的就是网络适配器(即网卡)。 帧:数据链路层的协议数据单元。数据链路层把网络层交下来的数据打包成帧发送到链路上,以及把接收到的帧中的数据取出并交给网络层 数据链路层协议 封装成帧 在一段数据的前后分别添加首部和尾部,构成一个帧。 首部和尾部的一个重要作用就是进行帧定界(确定一个帧的界限)。 每一种链路层协议都规定了所能传送的帧的数据部分长度的上限——最大传送单元(MTU) 控制字符SOH(Start Of Header)放在一帧的最前面,EOT(End Of Transmission)放在一帧的结束。如果只有SOH没有EOT则丢弃。(SOH = 0x01,EOT = 0x04) 透明传输 由于数据部分也有可能出现 EOT 的字符导致数据链路层会错误的找到帧的边界。若发送端的数据链路层在数据中出现控制字符“SOH”或“EOT”,则在前面插入一个转义字符“ESC”(其十六进制编码是 0x1B),如果出现了转义字符“ESC”,则再插入一个“ESC”。这种办法称为字节填充 差错检验 CRC(循环冗余检验) 在发送端,先把数据划分为组,每组 $k$ 个比特,将每组的数据用二进制的模2运算进行$2^n$乘的运算,然后在每组数据后添加 $n$ 位冗余码。 假定本组数据为 $M = 101001$,$n = 3$。假定除数$P = 1101(n + 1位)$ 首先在 $M$ 后添加 $n$ 位 $0$,得到 $2^nM = 101001000$ 将新得到的值与 $P$ 进行模2除法(进行异或运算,直到最后一位) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 110101 (商) +--------- 1101|101001000 (被除数) 1101 ---------- 1110 (每次运算使用的是异或运算,而不是减法) 1101 ---------- 0111 (每次运算时,如果首位是0,则商为0,如果首位是1,则商为1) 0000 ---------- 1110 1101 ---------- 0110 0000 ---------- 1100 1101 ---------- 001 (余数R) 将余数$R = 001$添加至 $M$ 后,得到CPC后的结果为 $101001001$ CPC校验只能判断这个帧是否有差错,不能判断哪一位或者哪几位除了差错 仅用循环冗余检验 CRC 差错检测技术只能做到无差错接受(accept)。 “无差错接受”是指:“凡是接受的帧(即不包括丢弃的帧),我们都能以非常接近于 1 的概率认为这些帧在传输过程中没有产生差错”。也就是说:“凡是接收端数据链路层接受的帧都没有传输差错”(有差错的帧就丢弃而不接受)。要做到“可靠传输”(即发送什么就收到什么)就必须再加上确认和重传机制 在数据后面添加上的冗余码称为帧检验序列 FCS 点对点协议PPP PPP协议的功能 简单:这是首要的要求,互操作性提高了 封装成帧:必须规定特殊字符作为帧定界符 透明性 多种网络层协议:同一条物理链路上同时支持多种网络层协议 多种类型链路 差错检测 检测连接状态 最大传输单元 网络层地址协商 数据压缩协商 PPP协议不需要的功能 纠错 流量控制 序号 多点线路 半双工或单工链路 PPP协议的组成 一个将IP数据报封装到串行链路的方法 链路控制协议 LCP(Link Control Protocol) 网络控制协议 NCP(Network Control Portocol) PPP协议的帧格式 各字段的含义 其中,首部的第一个字段和尾部的最后一个字段 F(flag) 都是 0x7E,表示一个帧的开始或结束。 首部中的地址字段 A 规定为 0xFF,控制字段 C 规定为 0x03 这两个字段并没有携带 PPP 帧的信息 首部中第四个字段为协议字段,当协议字段为 0x0021 时,PPP 帧的信息字段就是 IP 数据报。若为 0xC021, 则信息字段是 PPP 链路控制数据。若为 0x8021,则表示这是网络控制数据。 信息字段的长度是可变的,不超过 1500 字节 尾部中的第一个字段是用于CRC的帧校验序列FCS 字节填充 将信息字段中出现的每一个 0x7E 字节转变成为 2 字节序列(0x7D, 0x5E)。 若信息字段中出现一个 0x7D 的字节,则将其转变成为 2 字节序列(0x7D, 0x5D)。 若信息字段中出现 ASCII 码的控制字符 (即数值小于 0x20 的字符),则在该字 符前面要加入一个 0x7D 字节,同时将该字符的编码加以改变。例如出现 0x03,则要改为 0x7D,0x23 零比特填充:PPP 协议用在 SONET/SDH 链路时,是使用同步传输,这时 PPP 协议采用零比特填充方法来实现透明传输。信息字段每出现5个连续的1,则添加一个0,这样不会产生控制字符F相同的信息部分。 PPP协议的工作状态:略 使用广播信道的数据链路层 局域网的数据链路层 共享信道技术: 静态划分信道 动态媒体接入控制 随机接入 受控接入 CSMA/CD 协议(Carrier Sense Multiple Access with Collision Detection 载波侦听多路访问/碰撞检测) 以太网 所有计算机连接到一根总线上 采用无连接的工作方式 差错帧是否需要重传由高层决定 使用曼切斯特编码 CSMA/CD 协议 多点接入:许多计算机以多点接入的方式连接在一根总线上 载波监听:用电子技术监听总线上有没有其他计算机在发送数据 碰撞检测:边发送边监听 截断二进制指数退避 当发送的数据包发生碰撞冲突时,以太网使用截断二进制指数退避法 确定一个争用期时间:以太网把争用期定为 $51.2 \mu s$。 早期的以太网的网速为 $10Mbps$,所以在此时间内,计算机总共能够发送 64 字节的数据(512bit)。所以这个时间通常也被叫做512比特时间 当数据包开始发送的 $51.2 \mu s$ 内,如果接收到了其他计算机发送的数据包,则认为此次数据包发生了碰撞,并立即停止数据包的传输。 发生冲突的双方从 $[0, (2^k - 1)]$ 范围中随机取出一个整数,记为 $r$k = min\{重传次数(第一次为1), 10\} 下一次重传的将在 $r$ 倍的争用期 当重传达到 $16$ 次仍不能成功时,则放弃,并向高层报告 例如,在第1次重传时,k=1,随机数 $r$ 从整数 \{0,1\} 中选一个数。因此重传推迟的时间是0或争用期,在这两个时间中随机选择一个。整数范围的选择为2的k次方个数 若再发生碰撞,则重传时,k=2,随机数 $r$ 就从整数 \{0,1,2,3\} 中选一个数。因此重传推迟的时间是在$0, 1, 2, 3$倍的争用期这4个时间中随机抽取一个 同样,若在发生碰撞,则重传时k=3,随机数 $r$ 就从整数 \{0,1,2,3,4,5,6,7\} 中选一个数。以此类推 同时,以太网规定了最短的帧长为 64 字节,如果要发送的数据小于此数值,则必须要填入一些字节使得帧长不小于 64 字节。同样,如果发送方发送的帧数据在争用期(64比特)内没有发送碰撞,则后续发送的数据一定不会发生碰撞 对于接收方,如果接收到的帧小于 64 字节,则可以认为这是由于冲突而异常中止的无效帧 同时为了使刚刚收到数据帧的站的接收缓存来得及清理,做好接收下一帧的准备,以太网还规定了帧间最小间隔 $9.6 \mu s$ CSMA/CD 协议要点归纳 准备发送 检测信道 在发送过程中仍不停检测信道 发送成功:在争用期内没有检测到碰撞 发送失败:在争用期内检测到碰撞,立即停止发送数据 以太网每发送一帧,一定要把已发送是帧保留一下,如果在争用期内检测到了碰撞,则此帧需要重传 使用集线器的星形拓扑 略 以太网的信道利用率 S_{max} = \frac{T_0}{T_0 + \tau} = \frac{1}{1+\frac{\tau}{T_0}}T_0 = \frac{帧长(bit)}{发送速率(bit/s)} mac地址 mac地址是计算机中固化在适配器ROM中的地址,简单来说mac地址由电脑硬件直接决定,长度为 48 位(6 个字节)。制作适配器的厂商通常需要向 IEEE 的 RA 购买 OUI(组织唯一标识符,也可以理解为是mac地址段) 适配器过滤 适配器在接收到 mac 帧后,先用硬件检查 mac 帧中的目的地址,如果是发往本站的帧则收下,否则丢弃掉。 这里“发往本机的帧”包括以下三种: 单播(unicast):一对一 广播(broadcast):一对全体 多播(multicast):一对多 一些适配器可以设置为混杂模式,工作在混杂方式的适配器会将以太网上传输的所有帧都接受下来。这样实际上是窃听其他站点的通信 mac帧格式 前两个字段为目的地址和源地址 第三个字段为类型字段,用来标志上一层使用的是说明协议。例如为 0x0800 时为 IP 数据报,为 0x8137 为 Novell IPX 第四个字段为数据段,长度在 46 到 1500 字节(46 = 最短长度 64 字节 - 第一个字段 6 字节 - 第二个字段 6 字节 - 第三个字段 2 字节 - 第五个字段 4 字节) 第五个字段是帧校验序列 CRC 将 mac 帧向物理层传输时,还需要在开头加上 7 个字节的前同步码,同步码是由 10 串组成,使得接收端的适配器能够根据同步码的频率来调制时钟频率。和一个字节的帧开始界定符 无效的 mac 帧 帧的长度不是整数个字节 用收到的帧检验序列 FCS 由差错 收到的帧的 mac 数据字段的长度不在 46-1500 字节之间 扩展的以太网 略 网络层 网络层提供的两种服务 虚电路服务:通过网络层来实现可靠的传输网络协议,面向连接,类似打电话时的连接建立过程 数据报服务:不提供服务质量的保证,不需要建立连接 方面虚电路服务数据报服务(采用的) 思路可靠的通信应当由网络来保证可靠的通信应当由用户主机来保证 连接的建立必须有不需要 终点的地址仅在建立连接阶段使用,每个分组使用短的虚电路号每个分组都有终点的完整地址 分组转发属于同一条虚电路的分组均按照同一路由进行转发每个分组独立选择路由进行转发 当结点出现故障时所有通过出故障的结点的虚电路均不能工作出故障的结点可能会丢失分组,一些路由可能会发生变化 分组的顺序总是按照顺序到达终点到达终点的顺序不一定按照发送顺序 端到端的差错处理和流量控制可以由网络负责,也可以由用户主机负责由用户主机负责 网络协议 IP 与 IP 协议配套使用的还有三个协议 ARP(地址解析协议) ICMP(网络控制报文协议) IGMP(网际组管理协议) 虚拟互连网络 网络上使用的中间设备 物理层:转发器 数据链路层:网桥 网络层:路由器 网络层以上:网关 分类的 IP 地址 IP地址有两个固定长度的字段组成,第一个字段为网络号,第二个为主机号。一个 IP 地址在整个互联网范围内是唯一的, A类、B类、C类地址的网络号字段分别为1个、2个和3个字节长,而在网络号字段的最前面有 1-3 位的类别位,其数值分别规定位 0、10、110 A类、B类、C类地址的主机号的主机号字段分别为3个、2个、1个字节长 A类、B类、C类的地址都是单播见适配器过滤地址 D类地址(前4位是1110)用于多播 E类地址(前4位是1111)保留为以后用 IP 地址的指派范围 网络类别最大可以指派的网络数第一个可以指派的网络号最后可以指派的网络号每个网络中的最大主机数 A126($2^{7} - 2$)112616777214($2^{24} - 2$) B16383($2^{14} - 1$)128.1191.25565534($2^{14} - 1$) C2097151($2^{21} - 1$)192.0.1223.255.255254 特殊的 IP 地址 网络号主机号源地址使用目的地址使用代表的含义 00可以不可以本网络上的本主机 0非全0且非全1可以不可以在本网络上的某台主机 全1全1不可以可以在本网络上进行广播 非全0且非全1全1不可以可以对某个网络上的所有主机进行广播 127非全0且非全1可以可以本机 IP 地址的一些特点 每一个 IP 地址都由网络号和主机号两部分组成 实际上 IP 地址标志的是一台主机(或路由器)和一条链路的接口 一个网络指的是具有相同网络号的主机的集合 在 IP 地址中,所有分配到网络号的网络都是平等的 同一个局域网上的主机或者路由器的 IP 地址中的网络号必须是一样的 用网桥(它工作在链路层)互连的网段仍然是一个局域网,网络号必须相同 路由器总是拥有两个或更多的 IP 地址,即路由器的每一个接口都有一个不同网络号的 IP 地址 当两个路由器相连时,在接口两端处,可以分配也可以不分配 IP IP 地址与硬件地址(mac地址) 物理地址/硬件地址/mac 地址是数据链路层和物理层使用的地址,是固定的 IP 地址是网络层和以上各层使用的地址,是一种逻辑地址 在 IP 层抽象的互联网上只能看到 IP 数据报。IP 数据报不论转发多少次,其数据报中的 IP 地址始终是源地址和目的地址。 虽然 IP 数据报首部有源 IP 地址,但是路由器只根据目的站的 IP 地址进行转发 在链路层,只能看到 mac 帧,IP。数据报倍封装在 mac 帧中,mac 地址在不同网络中时,mac 帧内的源地址和目标地址会发生变化,具体表现为,对于每一次转发,其 mac 帧内的源地址和目标地址都会发生变化 考虑网络层时,可以是哟个统一的、抽象的 IP 地址来研究主机和主机或者路由器之间的通信 ARP(地址解析协议) 将网络层得到的 IP 地址转为下一跳的 mac 地址的过程(将 IP 地址映射到 mac 地址) 当主机 A 需要向 主机 B 发送 IP 数据报时,首先根据主机 B 的 IP 的地址,在本机的 ARP 缓存中寻找是否有主机 B 的 IP 地址,如果有,则可以获取主机 B 的 mac 地址 如果没有主机 B 的 IP,则向本局域网上广播一个 ARP 请求 在本局域网上的所有主机运行的 ARP 进程都收到此 ARP 请求 ARP 进程查询本机的 IP 是否与请求的 IP 相同,如果相同,则向主机 A 发送 ARP 响应 主机 A 收到主机 B 的 ARP 响应后,将此 mac 地址和 IP 写入 ARP 缓存 为什么不直接使用 mac 地址进行通信 全世界存在各种各样的异构网络,它们使用不同的硬件地址(网络的异构性体现),要使这些异构网络能够进行通信,就需要非常复杂的硬件地址转换工作,因此由用户主机来完成这件事几乎是不可能的,使用统一的IP地址,就使得所有的主机都在同一个IP网络内进行通信。 mac地址不具备归属地特征,不能作为地址 为什么不直接使用 IP 地址进行通信 因为并非每个主机都一个公网IP,很多主机都是使用的内网IP,依据NAT对外访问 IP地址是动态变化的 IP 数据报的格式 版本:占 4 位,指 IP 协议的版本。目前广泛使用的协议为 4 (即 IPv4) 首部长度:占 4 位,可表示最大十进制数值是 15,单位是 4 字节(32位)。首部长度必须是 4 字节的倍数。由于首部至少为 20 字节,所以首部长度至少为 5 区分服务:占 8 位,用来获得更好的服务。一般情况下不使用这个字段 总长度:占 16 位,指首部和数据之和的长度,单位是字节。所以数据报的最长长度位 65535 字节。通常是极少遇到的 标识:占 16 位,一个存在于 IP 软件中的计数器,每产生一个数据报,计数器就加一,并把此值赋给标识字段。当数据报因为过长而分片时,多个分片的标识符相同,便于接收方进行组装 标志:占 3 位,但是目前只有两位有意义 MF(More Fragment),最低位:MF = 1表示后面还有分片 DF(Don`t Fragment),中间位:DF = 1表示不能分片 片偏移:占13位,较长的数据报分片后,此片在原分组中的相对位置(不是序号),以 8 个字节位单位。每个分片的长度也一定是 8 个字节的整数倍 例如,一个数据报长度为 3820 字节,数据部分为 3800 字节,20字节的首部。分为 1400、1400、1000 四个片段,则每个片段的首部的偏移分别为 0、175($1400/8$)、350($2800/8$) 生存时间(TTL):占 8 位,当此数据报经过一次路由器时,TTL 会被减去1,当 TTL 等于 0 时,此数据报就会被丢弃 协议:表示数据报的协议类型。 常见的协议ICMPIGMPIP(一种特殊的数据报)TCPUDPIPv6 协议字段值12461741 首部检验和:占 16 位。仅检验数据报首部的数据是否正确 源地址:占 32 位 目的地址:占 32 位 可变字段:很少被用到 IP 层的转发分组的流程 从数据报的首部提取目标主机的 IP 地址 D,得出目的网络地址位 N 若 N 就是与此路由器直接相连的某个网络地址,则直接交付 若路由表中有目的地址位 D 的特定主机路由,则将数据报交给下一跳的路由器 若路由表中有到达网络 N 的路由,则将数据报交给下一跳的路由器 若路由表中有默认的路由,则将数据报交给默认路由 报告分组出错 划分子网 由于两级 IP 地址不够灵活,于是 IP 地址从原来的两级结构改为了通过子网掩码划分网络的 CIDR (无分类域间路由选择)模式,即 IP + 子网掩码的方式,不再有 A类、B类、C类的划分 即 IP = 网络前缀 + 主机号网络前缀 = IP \space \& 子网掩码 通常可以使用“斜线记法”来表示 IP,即在 IP 地址后加上斜线,并写上网络前缀所占的位数 例如 IP = 128.14.35.7/20 表示 128.14.35.7 的前 20 位作为网络前缀,剩下的作为主机号。即网络前缀为:128.14.32.0 最长前缀匹配 由于使用了变长的子网掩码,导致路由表中会出现多个匹配的结果,此时应当选择匹配结果中具有最长网络前缀的路由 子网的划分方法 将需要的划分网络出的网络分割为最小的单元 不断合并最小单元,直到满足最大的网络 重复上述操作,直到所有网络都满足 例如,一个自治系统内有 5 个局域网,该自治系统分配到的 IP 地址块为 218.75.230.0/24 将此网络划分为 5 个子网,每个子网的设备数如下:9、28、15、13、4 首先将所有的子网的设备数加2后,向上取整至2的幂次倍数,然后再将所有值分割为最小值的 $k$ 次倍,假定最小值为 $2^n$,这里取 $n = 3$ ID设备数加2后取整后分割后 1911162个8 22830324个8 31517324个8 41315162个8 54681个8 总计需要 13 个 长度为 8 的子网 由于 IP 地址块为 218.75.230.0/24,即可以分配的位数为 $t = 32 - 24 = 8$ 位的字符,相当于可以划分出 $2^{t - n} = 32$ 个子网,每个子网可以容纳 $6$ 台设备,此时掩码为 255.255.255.248 248 = \begin{matrix}\underbrace{11111} \\ t个1 \end{matrix} \space \begin{matrix}\underbrace{000} \\ n个0 \end{matrix} = 11111000 = 248 可以分割整个 IP 地址块得到 ID网络号掩码 1218.75.230.0255.255.255.248 2218.75.230.8255.255.255.248 3218.75.230.16255.255.255.248 4218.75.230.24255.255.255.248 5218.75.230.32255.255.255.248 6218.75.230.40255.255.255.248 7218.75.230.48255.255.255.248 8218.75.230.56255.255.255.248 9218.75.230.64255.255.255.248 10218.75.230.72255.255.255.248 11218.75.230.80255.255.255.248 12218.75.230.88255.255.255.248 13218.75.230.96255.255.255.248 由于只需要 13 个子网,这里只罗列出前 13 个,实际上可以罗列出 32 个 按照设备数排序后划分子网得到(注意,合并后,掩码也要合并) ID设备数加2后取整后分割后子网号子网掩码 22830324个8218.75.230.0255.255.255.224 31517324个8218.75.230.32255.255.255.224 41315162个8218.75.230.64255.255.255.240 1911162个8218.75.230.80255.255.255.240 54681个8218.75.230.96255.255.255.248 ICMP(网际控制报文协议/互联网控制消息协议) 它用于网际协议(IP)中发送控制消息,提供可能发生在通信环境中的各种问题反馈。通过这些信息,使管理者可以对所发生的问题作出诊断,然后采取适当的措施解决 ICMP报文类型 报文类型类型值种类 终点不可达3差错报告报文 时间超过11差错报告报文 参数问题12差错报告报文 改变路由5差错报告报文 回送请求和回答8或0询问报文 时间戳请求和回答13或14询问报文 不应发送 ICMP 差错报文的类型 对 ICMP 差错报告报文,不再发送 ICMP 差错报告报文 对非第一个分片的数据报片,不发送 ICMP 差错报告报文 对具有多播地址的数据报,不发送 ICMP 差错报告报文 对具有特殊地址的数据报(127.0.0.0或0.0.0.0),不发送 ICMP 差错报告报文 ICMP 的应用 PING:ping 使用了 ICMP 回送请求与回送回答报文,ping 没有通过 TCP 或者 UDP traceroute:路由跟踪,利用了 ICMP 时间超过,通过设置 TTL 来发送一系列报文,获取源主机和目标主机之间的路由线路 互联网的路由选择协议 路由表中的路由是如何动态更新获取的 理想的路由算法 算法必须是正确的完整的 算法在计算上应简单 算法应能适应通信量和网络拓扑的变化 算法应具有稳定性 算法应是公平的 算法应是最佳的 分层次的路由选择协议 内部网关协议IGP:在一个小的网络系统(AS)内的路由协议,常见的有 RIP和OSPF协议 外部网关协议EGP:不同网络系统内的路由协议,目前使用的最多的是 BGP 的版本4(BGP-4) RIP 一个基于距离向量的路由选择协议 距离指的是跳数,每经过一次路由器则跳数加一,相邻的路由器的跳数为1,跳数最多为 15,超过 15 表示不可达。 RIP 协议会按照固定的时间间隔和相邻的路由器交换自己的路由表,经过若干次交换后,所有路由器都会得到当前网络中任意一个网络的最优的下一跳路由器地址 算法逻辑: 对于来自地址为 X 的相邻路由器发送来的 RIP 报文,先将报文中的所有项目中的下一跳地址都改为 X,然后将所有的距离字段都加一 对于新的报文中的每一项,假定其目的网络为 N,距离为 d 如果自己的路由表中没有目标网络为 N 的,则直接加入到自己的路由表中(从没有数据到有数据) 如果有目标网络为 N 的,且下一跳地址为 X 的,则直接替换此项(更新原来旧的数据) 如果有目标网络为 N 的,且下一跳地址不为 X 的,则用 d 与其进行比较,选择距离小的写入路由表(选择最优的) 若一段时间内(3分钟)没有收到相邻的路由器的 RIP 包,则视为此路由器不可达,把其距离设定为 16 RIP的报文由首部和路由部分组成 首部:占 4 个字节,其中的命令字段指出报文的意义,1 表示请求路由信息,2 对请求的路由信息的响应或未被请求而发出的路由更新报文。首部后面的全 0 用于凑足 4 个字节,对齐 路由部分:路由部分由若干个路由信息组成,每个路由信息需要 20 个字节,最多一次可以传输 25 个路由信息 协议标记(地址族标识符、地址类别)表示所使用的地址协议,如果采用 IP 地址则此字段为 2 路由标记填入自治系统号 ASN(此字段考虑的是如果非 RIP 路由器发送的信息时,可以进行区别) 网络地址 子网掩码 下一跳的路由器地址 度量值:到此网络的距离 RIP 协议的优点:实现简单,开销小,使用 UDP RIP 协议的问题:但是 RIP 能使用的最大距离为 15,限制了网络的规模。当网络出现故障时,要经过比较长的时间才能将此信息传递到所有的路由器(好消息传播得快,坏消息传播的慢) OSPF 开放最短路径优先协议 使用分布式的链路状态协议,所有路由器都拥有整个网络的拓扑结构图 使用洪泛法向本网络中的所有路由器发送信息 发送的信息是本路由器相邻的所有路由器的链路状态,以及该链路的度量(费用、距离、时延、带宽等) 只有链路状态发生变化时,路由器才向所有路由器发送此信息 特点 所有路由器最终都能获得本网络中的整个路由器拓扑图 链路状态路由器更新速度快,收敛快 有时为了能够解决大型网络拓扑图太大的问题,OSPF 可以将网络划分为多个不同的区域,每个路由器仅知道本区域内的拓扑情况,区域之间通过区域边界路由器进行交互 OSPF 不使用 UDP 而是直接用 IP 数据报传送,减少信息的通信量 版本:当前的版本号为 2 类型 问候(Hello):用来发现和维持邻站的可达性,每隔 10 秒钟交换一次,如果 40 秒内没有发生交换,则认为此路由器不可达,更新数据库并且向其他路由器进行报告 数据库描述(Database Description):向邻站给出自己的链路状态数据库中的所有链路状态项目的摘要信息 链路状态请求(Link State Request):向对方请求发送某些链路状态项目的详细信息 链路状态更新(Link State Update):用洪泛法对全网更新 链路状态确认(Link State Acknowledgment):对链路更新的确认 分组长度:包括 OSPF 首部在内的分组长度,以字节为单位 路由器标识符:标志发送该分组的路由器的接口的 IP 地址 区域标识符:分组属于的区域的标识符 检验和:用来检测分组中的出错 鉴别类型:目前只有两种,0 和 1 鉴别:鉴别类型为 0 时就填入 0,鉴别类型为 1 则填入 8 个字节的口令 OSPF的特点 允许管理员给每条线路指派不同的代价,使得 OSPF 相对灵活 如果到达同一个网络有多条相同代价的路径,那么可以将通信量分配给这几条路径,使得负载平衡 所有在 OSPF 路由器之间交换的分组都具有鉴别的功能,保证了仅在可信赖的路由器之间交换链路状态信息 OSPF 支持可变长度的子网划分和无分类的编址 CIDR 由于网络中的链路状态经常发生变化,所以链路状态会带上一个 32 位的序号,序号越大状态越新 BGP 边界网关协议(BGP)采用路径向量(path vector)路由选择协议。协议交换路由信息节点数量级是自治系统个数的数量级。刚开始运行时,BGP的邻站是交换整个的BGP路由表。但以后只需要在发生变化时更新有变化的部分。 标记:16字节长,用来鉴别收到的 BGP 报文。若不适用鉴别时,应当全为 1 长度:包括通用首部在内的整个 BGP 报文的长度,以字节为单位 类型 OPEN:打开,用来和另一个 BGP 路由器建立关系 UPDATE:更新,用来通知某一路由的信息,以及列出要撤销的多条路由 KEEPALIVE:保活,用来周期性验证邻站的连通性 NOTIFICATION:通知,用来发送检测到的错误 报文: OPEN 版本:1字节,现在为 4 本自治系统号:2字节,全球唯一 保持时间:2字节 BGP 标识符:4个字节,通常为此路由器的 IP 地址 可选参数长度:1字节 可选参数 UPDATE 不可行路由长度:2字节 撤销的路由 路径属性总长度:2字节 路径属性 网络层可达性信息(NLRI) KEEPALIVE:无报文部分 NOTIFICATION 差错代码:1字节 差错子代码:1字节 差错数据 路由器 路由选择和分组转发 IPv6 IPv6 的长度为 128 位,而 IPv4 仅 32 位 IPv4至IPv6的变化: 更大的地址空间 扩展的地址层次结构 灵活的首部格式 改进的选项 允许的协议继续扩充 支持即插即用,不需要 DHCP 进行 IP 分配 支持资源的预分配 IPv6 首部改为 8 字节对齐 IP 多播 实现一对多通信,主机只需要发送一个数据报,路由器负责复制数据报 在目的地址中使用多播的标识符(D类地址),来标识一个多播组 尽最大努力交付,不保证一定能交付 首部中的协议字段值为 2,表示使用 IGMP(网际组管理协议) 不产生 ICMP 差错报文 无法 Ping 通 IGMP 略 虚拟专用网络(VPN)和网络地址转换(NAT) IP 地址中有一部分被指定为专有地址。当使用的是专有地址时,说明此地址并非是互联网上的地址,而是本地局域网(专用网)内的地址 10.0.0.0 至 10.255.255.255 172.16.0.0 至 172.31.255.255 192.168.0.0 至 192.168.255.255 VPN 由于这些地址是专有地址,无法直接在互联网上访问到,所以可以使用 VPN 技术来实现内网的访问。VPN的数据在互联网段是加密数据,所以效果上和真正的专用网一样。 内联网:两个地区的专用网进行连接 远程接入VPN:在互联网上的主机访问在专用网内的主机 NAT 每个专用网拥有至少一个互联网的 IP 地址,当专用网内的主机需要和互联网通信时,通过此专用网的 IP 地址和不同端口来临时当作主机的 IP,实现对互联网的访问 多协议标志交换(MPLS) 略 运输层 运输层的协议概述 进程之间的通信 运输层使得两台主机之间的交互细化到应用程序的进程 运输层的一个很重要的功能:复用和分用 复用:发送方不同的应用进程都可以使用同一个运输层协议传输数据 分用:接收方的运输层在剥去报文的首部后,能够把这些数据正确交付目的应用程序 两个主要协议 UDP(用户数据报协议):不需要连接,不提供可靠交互。但在某些情况下却是一种最有效的工作方式 TCP(传输控制协议):在传输数据之前必须先建立连接,数据传送结束后,要释放连接,不提供广播或多播服务。提供可靠的、面向连接的运输服务,增加了许多开销 应用应用层协议运输层协议 名字转换DNS(域名服务器)UDP 文件传送TFTP(简单文件传送协议)UDP 路由选择协议RIP(路由信息协议)UDP IP 地址配置DHCP(动态主机配置协议)UDP 网络管理SNMP(简单网络管理协议)UDP 远程文件服务器NFS(网络文件系统)UDP IP 电话专用协议UDP 流式多媒体通话专用协议UDP 多播IGMP(网际组管理协议)UDP 电子邮件SMTP(简单邮件传送协议)TCP 远程终端接入TELNET(远程终端协议)TCP 万维网HTTP(超文本传送协议)TCP 文件传送FTP(文件传送协议)TCP 运输层的端口 将不同的应用程序绑定至不同的端口,通过端口来区别不同的应用程序。系统同时拥有 65535 个不同的端口 服务器端使用的端口号 熟知端口号:数值范围为 0-1023 的端口 登记端口号:数值范围在 1024-49151 的端口 应用程序端口号 FTP21 TELNET23 SMTP25 DNS53 TFTP69 HTTP80 SNMP161 SNMP(trap)162 HTTPS443 客户端使用的端口号:数值范围为 49152-65535 临时的端口号 UDP UDP 概述 UDP 是无连接的 UDP 尽最大的努力交付 UDP 是面向报文的 UDP 没有拥塞控制 UDP 支持一对一、一对多、多对一、多对多的交互通信 UDP 的首部开销小 UDP 的首部格式 源端口:源端口号。需要对方回信时使用,不需要时可全为 0 目的端口:目的端口号。这在终点交付报文时必须使用 长度:UDP 用户数据报的长度,其最小值为 8(仅有首部) 检验和:检验 UDP 用户数据报在传输中是否有错 TCP(传输控制协议) TCP特点 面向连接的运输层协议 每条 TCP 只能连接两个端点,即{(IP1, 端口1), (IP2,端口2)} 提供可靠交付的服务 提供全双工通信 面向字节流 可靠传输的工作原理 超时重传:当发送端发送的数据后,需要等待接收端发送确认收到报告,如果在一段时间内没有收到确认收到报告,则重新发送数据 暂存已发送的副本:发送端发送了一个分组后,需要暂时保留已发送的分组的副本,同时对所有分组和确认分组进行编号,只有在收到了对应的编号的确认,才可以删除此副本 确认迟到:当收到重复的分组时,需要丢弃,但仍然需要向发送端发送确认收到 滑动窗口协议:可以连续发送多个分组,每收到一个请求就可以将滑动窗口向前滑动。而接收方采用累积确认,对按序到达的最后一个分组进行确认,不再对所有有分组进行确认 以上的逻辑称为自动重传请求(ARQ) TCP 报文段的首部格式 TCP报文段首部的前20个字节是固定的 源端口和目的端口:各占2个字节,表示源端口号和目的端口号,与 UDP 相似 序号:占4个字节,表示当前数据部分中,第一个字节在整个数据流中的位置 确认号:占4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则序号 N - 1 为止的所有数据都已经收到了 数据偏移:占4位,表示TCP首部的长度,单位为 32 位(4字节),最大值为 15,所以TCP首部最大为 60 字节 保留:占6位,保留为今后使用,但目前为0 Flag标记:占6位 URG(紧急):表示此数据应当紧急发送 ACK(确认):在建立连接后的所有报文段都必须把 ACK 置为 1 PSH(推送):很少使用,希望对方的应用程序尽快处理 RST(复位):释放连接,拒绝打开连接 SYN(同步):和 ACK 一起配合来实现连接的建立和释放 FIN(终止):释放连接 窗口:占2个字节,表示发送本报文的主机的接受数据缓存空间,指明了现在允许对方发送的数据量 检验和:占2个字节,检验和字段检验的范围包括首部和数据 紧急指针:占2个字节,仅 URG 为 1 时有意义,指出本报文段中紧急的数据字节数 选项:长度可变,最长为 40 字节,且必须是 4 字节的整数倍 MSS:数据字段长度,默认为 536 字节 TCP 可靠传输的实现 以字节为单位的滑动窗口 TCP 的滑动窗口是以字节为单位的,滑动窗口内的所有数据包会一次性发送出去,窗口内的数据表示已经发送但是未被确认。假定主机 A 收到的来自主机 B 的确认包的 ACK 为 36,那么滑动窗口将会前移至 36 超时重传时间的选择 基于 RTT,定义一个值 RTT_S,此值基于新的 RTT 不断更新,更新公式如下: RTT_S = (1 - \alpha) \times (RTT_S) + \alpha \times RTT 通常 $\alpha = 0.125$ 所以定义超时重传时间 RTO = RTT_S + 4 \times RTT_D 其中 $RTT_D$ 为 $RTT$ 的偏差的加权平均值,其公式如下 \begin{cases}RTT_D = RTT / 2,第一次 \\RTT_D = (1 - \beta) \times RTT_D + \beta \times |RTT_S - RTT|, 后续的情况\end{cases} 通常 $\beta = 0.25$ 选择确认 SACK 略 TCP 流量控制 TCP 流量控制让发送方发送速率不要太快,要让接收方有足够的缓存来接受数据 如图中所示,通过 TCP 数据结构中的窗口,实现双方的流量的控制。接收方(R)在发送确认包的同时,将当前的缓存空间发送给发送方(T),使得双方能够控制传输的速度。而当程序将收到的数据读出后,再将新的空间剩余发送给发送方 TCP 零窗口死锁:当图片中第五个数据包因为意外丢失时,发送方(T) 一直在等待接收方(R) 发送的新的窗口信息,而接收方(R)也一直在等待发送方(T)发送新的数据,导致了死锁 解决办法:为每个连接设有一个持续计时器。当连接的一方收到对方的零窗口通知时,就启动计时器,在计时器到达一定的时间内,就发送一个零窗口探测报文段,而对方如果再次回复零窗口,则重置计时器,如果回复的不是零窗口,则死锁解开 TCP 的传输效率 略 TCP 拥塞控制 拥塞控制就是防止过多的数据注入到网络中,这样可以使得网络中的路由器或链路不致过载。 主要是两类控制: 开环控制:在设计网络时就将有关拥塞的因素考虑周全,力求网络在工作时不会产生拥塞 闭环控制 基于反馈环路,监测网络系统以便在检测到拥塞在何时何处发生 把拥塞的信息传送到可采取行动的地方 调整网络的运行以解决出现的问题 实现拥塞控制的四个方法 慢开始 发送方设定一个发送窗口(cwnd),即每次发送的数据不能大于发送窗口的数据(可以认为是 TCP 的滑动窗口) 开始时,发送窗口的大小设定为一个很小的值 每当发送方收到一个来自接收方的确认包时,就增大此窗口的大小 每当出现拥塞(超时)时,就将此窗口再次调整为很小的初始值 拥塞避免 设定一个窗口的极大值(ssthresh),当窗口的大小超过此极大值时,无论接受到多少个确认包,每次缓慢的增加窗口大小 出现拥塞时,将此极大值设置为当前窗口的一半 快重传 要求接收方不论收到什么数据,都要发送确认包,确认包只确认连续的数据包的最后一项 发送方遇到连续三次相同的确认,但是没有出现确认超时的情况(收到了确认但是并非是正确的确认信息),则认为在网络中出现了丢失数据包的情况,则立刻进行重传丢失的数据包 快恢复 在出现只丢失部分数据包的情况下,即出现快重传的情况时,不会将窗口设置为初始值,而是仅仅将窗口的大小减半,同时极大值设置为当前窗口的一半 图中,0-1的过程为慢开始,1-2的过程即为拥塞避免,2-3的过程中出现了数据包确认超时,4-5的过程中发生了快重传和快恢复 TCP 的运输连接管理 连接的三个阶段 连接建立 数据传送 连接释放 三个问题 要使每一方能够确知对方的存在 要允许双方协商一些参数 能够对运输实体资源进行分配 TCP 的连接建立 客户端(A) 和 服务器(B) 都处于 CLOSED(关闭) 状态 B 创建传输控制模块,进入 LISTEN(收听) 状态 A 创建传输控制模块,向 B 发送TCP连接请求,此报文中 SYN=1,且 seq=J,A 进入 SYN-SENT(同步已发送) B 接收到请求,如果同意建立连接,则向 A 发送数据包,此报文中 SYN=1,ACK=1,且 ack=J+1,seq=K,进入 SYN-RCVD(同步收到) A 收到 B 的确认后,再次给 B 发送确认数据包,此报文中 ACK=1,且ack=K+1,进入 ESTABLISHED(已建立连接) B 收到 A 的确认后,也进入 ESTABLISHED(已建立连接) TCP 的连接释放 略 应用层 DNS(域名服务器) 功能:将域名转换为 IP 地址 域名结构:从域名的最后一个单词开始,通过 . 来分割,表示了“顶级域名”、“二级域名”、“三级域名”。例如对于域名:mail.google.com,顶级域名为:com,二级域名:google,三级域名:mail。无论域名内容是什么,一定是按照此逻辑进行分配。例如域名:mail.zjgsu.edu.cn的顶级域名为:cn,二级域名为:edu,三级域名为:zjgsu,四级域名:mail DNS 的递归查询过程见计算机网络实验复习 递归是用户只向本地DNS服务器发出请求,然后等待肯定或否定答案。而迭代是本地服务器向根DNS服务器发出请求,而根DNS服务器只是给出下一级DNS服务器的地址,然后本地DNS服务器再向下一级DNS发送查询请求直至得到最终答案 FTP(文件传送协议) 提供文件的交互式访问 语序客户指明文件的类型与格式 允许文件具有存储权限 输入有效的口令 屏蔽了各计算机系统的细节 TFTP(简单文件传送协议) 略 TELNET(远程终端协议) 明文的远程终端协议,目前通常使用 ssh(Secure Shell 加密的网络传输协议) 来代替 TELNET WWW(万维网) HTTP 明文传输的超文本传输协议,由于是明文传输,很容易受到中间人攻击,植入广告 HTTPS 加密传输的超文本传输协议,不会被中间人攻击 电子邮件 SMTP(简单邮件传送协议) IMAP和POP3(邮件接受协议) 操作位置操作内容IMAPPOP3 收件箱阅读、标记、移动、删除等操作客户端与邮件更新同步仅在客户端内 发件箱保存到已发送客户端与邮件更新同步仅在客户端内 创建文件夹新建自定义文件夹客户端与邮件更新同步仅在客户端内 草稿保存草稿客户端与邮件更新同步仅在客户端内 垃圾文件夹接受并移入垃圾文件夹的邮件客户端与邮件更新同步仅在客户端内 广告邮件接受并移入广告邮件夹的邮件客户端与邮件更新同步仅在客户端内 DHCP(动态主机配置协议) 见计算机网络实验复习 重点总结 IPv6长度为128位 CSMACD载波侦听多路访问/碰撞检测 比 TELNET 更好的远程终端协议——SSH 发送邮件的协议为SMTP,接受为IMAP和POP3 光纤分为单模光纤和多模光纤 最短帧长 给出域名问主机名 最大传输单元MTU=1500字节 曼切斯特编码 截断二进制指数退避算法 ARP ICMP 零比特填充 浏览器访问植入广告原理 DNS解析过程 路由表的构建 为什么不使用mac地址作为通信地址 网络划分子网 IP数据报结构 TCP可靠的实现 TCP三次握手 安全的 web 协议——HTTPS 不同 vlan 之间通讯需要第三层交换机

2021/1/7
articleCard.readMore

记一次 Navicat 连接 MySQL 一直报认证错误(Access denied)

今天一时兴起,想在 WSL2 里下个 MySQL。方法也很简单,直接 sudo apt install mysql-server 本来以为顺风顺水,结果却在 Navicat 连接 MySQL 的操作上出事了 问题 Navicat 无法连接上 MySQL 配置情况 Navicat Premium 15.0.19 MySQL 8.0.22 WSL2(Ubuntu 20) 现状 终端可以通过sudo mysql连上 MySQL 终端不可以通过mysql -u root -p的方式连接,显示密码错误(Access denied for user 'root'@'localhost') 终端可以通过默认用户连接(默认用户为 /etc/mysql/debian.cnf 文件中的 debian-sys-maint,密码为安装MySQL时随机生成得到的) Navicat不可以通过直接连接或者通过 ssh 的方式连接,显示密码错误(Access denied for user 'root'@'localhost') Navicat可以通过默认用户连接 经过 尝试1 首先是尝试了百度的结果,重置 MySQL 的 root 账户的密码 因为可以通过sudo mysql直接进入数据库,也就不需要那么多百度出来的奇奇怪怪的操作了 直接进入数据库,然后尝试了下面几行代码 1 2 3 use mysql; alter user 'root'@'localhost' identified by 'newPassword'; exit 然后,测试mysql -u root -p连接——失败 尝试2 后来在MySQL官网找到了重置root密码的方法,然后赶紧拿来测试 官网链接 其中的一点提到 B.3.3.2.2 Resetting the Root Password: Unix and Unix-Like Systems 大致操作就是先终止 MySQL,然后使用 MySQL 的附加参数来设置一个初始化文件,然后使得 MySQL 去运行此文件。 然后,测试mysql -u root -p连接——失败 其实觉得挺奇怪的,既然都能重启 MySQL 了,说明你已经拿到这个设备的 root 权限了,为什么不直接用 sudo mysql 进入直接run这条命令呢? 尝试3 最终我在一份不起眼的博客上找到了解决方案 博客连接 其中提到了一个很重要的命令 1 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'insert_password'; This command changes the password for the user root and sets the authentication method to mysql_native_password. This is a traditional method for authentication, and it is not as secure as auth_plugin. 其中的mysql_native_password是所谓的传统验证方案,也就是 Navicat 连接 MySQL 的解决方案 然后将方案1的命令稍作改正得到 1 2 3 use mysql; alter user 'root'@'localhost' identified with mysql_native_password by 'newPassword'; exit 然后,测试mysql -u root -p连接——成功! 后续 mysql5.8开始将caching_sha2_password作为默认的身份验证插件,该caching_sha2_password和 sha256_password认证插件提供比mysql_native_password插件更安全的密码加密 ,并 caching_sha2_password提供了比更好的性能sha256_password。由于这些优越的安全性和性能特性 caching_sha2_password它是MySQL 8.0首选的身份验证插件,而且也是默认的身份验证插件而不是 mysql_native_password。此更改会影响服务器和libmysqlclient 客户端库;目前来说和经常使用的客户端软件兼容性不好。 这也是导致目前 Navicat 无法连接到 MySQL 5.8及以后版本的原因。当然如此操作后的影响便是无法直接使用sudo mysql的方式连接到数据库,只能通过 mysql -u root -p的传统密码验证的方式来登陆

2021/1/2
articleCard.readMore

计算机网络实验复习

传输介质 颜色 A类线(T568A)颜色:白绿/绿/白橙/蓝/白蓝/橙/白棕/棕 B类线(T568B)颜色:白橙/橙/白绿/蓝/白蓝/绿/白棕/棕 线 分为两种线:直连线和交叉线 直连线:线的两端使用的是相同类的线,即同时使用A类或者B类 交叉线:线的两端使用的是不同的线,一段为A类,一段为B类 为什么有两种不同的线 输入口和输出口的区别 如果使用的是直连线,则一段的输入端和另一端输入端的位置相同 而使用的是交叉线,则一段的输入端和另一端输入端的位置不同 使用时间 当一段为交换机,另一端不为交换机时,使用直连线 其他情况均使用交叉线 动态主机配置协议(DHCP) 用户利用有线或无线方式随机接入局域网,获得由DHCP服务器分配的临时IP地址 分配过程 借助UDP协议、广播方式向局域网中所有DHCP服务器67端口发送DHCP搜索信息(DHCPDISCOVER) 每个DHCP服务器收到广播请求后回应一个有效IP地址,并对该IP地址进行锁定(DHCPOFFER) 客户机接受第一个回应的IP地址,并广播通知所有DHCP服务器确认接受。除分配该IP地址的服务器外,其他服务器解除对准备分配的IP地址的锁定,放回地址池(DHCPREQUEST) 被选中的DHCP服务器收到确认信息后,以广播方式答复确认信息(DHCPACK) 终止DHCP租借 超过服务器配置中所设置的时间,DHCP租借自动过期 未超过服务器配置中所设置的时间,客户机的TCP/IP配置中可进行手动终止。 域名解析服务(DNS) 域名解析系统,以符号名字代替纯数字(IP地址)对计算机进行标识。例如,将www.baidu.com解析为36.152.44.95 域名分级 例如:www.baidu.com 一级域名为:com 二级域名为:baidu 三级域名为:www 每一级域名的解析服务器(DNS)都知道其下一级域名的服务器的IP,同时也知道根服务器的IP 域名解析过程 客户机(PC)向首选DNS服务器发起请求:“你知道www.baidu.com的IP吗?”如果首选DNS服务器知道(一般如果首选DNS服务器曾经解析过,那么会进行一段时间内的缓存,默认三天,如果正好在缓存时间内,那么首选服务器就会知道这个域名的IP)那么首选DNS 服务器就会直接给客户机返回域名的IP 地址 若首选DNS 服务器上没有相关信息,就不能直接返回域名的IP 地址,这时候,首选DNS 服务器就会去询问根DNS服务器(所有的 DNS 服务器都知道全球的 13 台DNS根服务器在哪里),根服务器可能不知道这个具体的 www.baidu.com 的IP地址,但是它知道一级域 com 的DNS服务器的IP(也就是说根服务器只负责维护所有的一级域,所以也就几百条数据在这里,虽然数据量少,但是它接受来自全球的请求,所以负载也很大) 根服务器将 com 的DNS服务器的IP地址返回给首选 DNS 服务器 首选DNS服务器再去请求 “com” DNS服务器:“你知道 www.baidu.com 的IP吗”,但是com DNS服务器也不知道 www.baidu.com 的IP,但是com 的DNS服务器知道 baidu.com 的IP “com” 的DNS服务器将这个信息返回给首选 DNS 服务器 首选DNS服务器再去请求 “baidu.com” DNS服务器,这时候 baidu.com 服务器当然就会知道 www.baidu.com的IP地址 “baidu.com”DNS服务器将这个信息返回给首选DNS 服务器 首选DNS服务器将获取到的 www.baidu.com 的IP返回给客户机 客户机根据获取到的 www.baidu.com 的IP地址来访问WEB服务器 WEB服务器返回相关的数据 序号请求发起者请求接受者询问内容询问结果 1PC默认DNS服务器www.baidu.com暂时不回答 2默认DNS服务器根服务器www.baidu.comcom的DNS服务器的IP 3默认DNS服务器com 的DNS服务器www.baidu.combaidu.com 的DNS服务器的IP 4默认DNS服务器baidu.com 的DNS服务器www.baidu.comwww.baidu.com的IP 5默认DNS服务器PC回答序号1的询问,即返回www.baidu.com的IP 性能优化 缓存:将查找到的新域名解析结果置于本地缓存,以提高域名解析响应速度 复制:根服务器存在多个副本,为客户机请求提供最快速的响应 Internet服务管理器(IIS) Web访问过程 输入想要访问的网站的域名或者IP DNS 解析网站的域名得到 IP 访问对方的 IP 的80端口找到对方的 web 服务器上的对应的网页 将网页下载到本地 浏览器渲染页面并显示出来 FTP访问过程 输入想要访问的FTP的域名或者IP DNS 解析网站的域名得到 IP 访问对方的 IP 的21端口找到对方的 FTP 服务器上的对应的文件夹 打开FTP站点目录 交换机(Switch) 工作在OSI参考模型的第二层,即数据链路层 序号名称举例 7应用层HTTP 6表示层JPEG 5会话层 4传输层TCP 3网络层IP 2数据链路层mac 1物理层 通常交换机只能看到数据包的mac地址,并不知道数据包所要发往的IP地址 基本概念 mac地址 mac 地址与电脑硬件(网卡)有关 是网卡的硬件地址,全球唯一 mac地址表 存放物理地址与交换机端口映射关系的数据库 交换机工作原理 数据转发 数据包信息到达交换机 交换机根据数据包中封装的目的主机的MAC地址信息查找MAC地址表,同时根据源主机MAC地址信息更新自己的MAC地址表 如果表中存在该目的主机的MAC地址,则从其对应的端口将数据包发送出去 如果表中不存在该目的主机的MAC地址,则将该数据包被泛洪到所有端口 目的主机PC2接收到数据包后,回复响应数据包给PC1,该过程与PC1发送数据包给PC2类似,但此时,PC2是源主机,PC1是目的主机 当PC2发送的响应数据包到达交换机时,交换机在转发数据包的同时,根据源主机MAC地址更新MAC地址表(在2.2的情况下,即在MAC地址表中添加一条PC2的MAC地址信息——MAC地址自动学习) 自动老化功能 存在于MAC地址表中的MAC地址,如果长时间没有从该MAC地址收到包,则该MAC地址将被删除 当再次收到该MAC地址发送的包时,把该包作为广播包处理,重新学习 转发 交换机向MAC地址X转发数据包 过滤 交换机收到一个数据包,查表后发现该数据包的来源地址与目的地址属于同一网段。交换机将不处理该数据包 如果交换机的每个端口都只连接一台 PC,那么交换机会正常进行转发而不会进行过滤 使用 所有绿色内容都为需要根据实际情况填写 原状态新状态命令 用户模式特权模式enable 特权模式全局配置模式configure terminal 全局配置模式接口配置模式interface fa0/1 全局配置模式多个接口配置模式interface range fa0/1 - 10 全局配置模式Vlan配置模式interface vlan 1 (多个)接口配置模式/Vlan配置模式全局配置模式exit 全局配置模式特权模式exit 特权模式用户模式exit 模式用途命令行开头最后显示标志 用户模式实验课上无用> 特权模式查看设备信息时使用# 全局配置模式设置设备信息时使用(config)# 交换机的命令列表 特权模式下 命令功能 show mac-address-table查看mac地址表 show aging-time查看自动老化时间 show vlan brief查看 vlan 列表 全局配置模式 命令功能 hostname 新的名字修改交换机名称 mac-address-table static mac vlan 1 interface fa0/1新增一条静态路由绑定 no mac-address-table static mac vlan 1 interface fa0/1删除一条静态路由绑定 vlan 1新建/配置一个Vlan no vlan 1删除一个Vlan ip routing启用路由功能(仅三层交换机) 配置模式 命令功能 switchport mode access设置端口为普通端口 switchport mode trunk设置端口为 trunk 口 switchport access vlan 1将端口设定为 vlan 1 ip address IP 掩码设置当前Vlan的IP(仅在Vlan配置模式下使用,仅三层交换机可用) 路由器(Router) 网络段计算公式 1 IP & 掩码 例如,IP为192.168.1.1,掩码为255.255.255.0 则其网络段为 11000000.10101000.00000001.00000001 & 11111111.11111111.11111111.00000000 = 11000000.10101000.00000001.00000000 即,网络段为:192.168.1.0 交换机只能交换同一个网络段的数据包,不能交换不同网络段的数据包 使用 原状态新状态命令 用户模式特权模式enable 特权模式全局配置模式configure terminal 全局配置模式接口配置模式interface fa0/0 全局配置模式子接口配置模式interface fa0/0.1 子接口配置模式全局配置模式exit 接口配置模式全局配置模式exit 全局配置模式特权模式exit 特权模式用户模式exit 模式用途命令行开头最后显示标志 用户模式实验课上无用> 特权模式查看设备信息时使用# 全局配置模式设置设备信息时使用(config)# 接口配置模式设置单个具体的端口使用(config-if)# 路由器命令列表 特权模式下 命令功能 show interface查看路由器端口信息 show ip route查看路由信息 全局配置模式 命令功能 hostname 新的名字修改路由器名称 ip route 目标网段 掩码 下一个跳转的IP地址设置静态路由转发 配置模式 命令功能 ip address IP 掩码设置路由器的此端口的IP和掩码 no shutdown启用此端口 encapsulation dot1Q Vlan号封装802.1Q(进入子端口的时候,封装此协议可以为此子端口设置IP) 注意,别忘记给PC设置网关 Ping 的返回结果含义 Requesttimed out 超时 对方已关机 对方和我不在同一个网段 对方防火墙设置了ICMP数据包过滤 错误设置IP地址 Destinationhost Unreachable(无法到达) 对方与自己不在同一网段内,而自己又未设置默认的路由(网关) 网线出了故障 BadIP address(错误的IP) DNS服务器未设置 IP地址不存在 Sourcequench received 对方或中途的服务器繁忙无法回应 Unknownhost(不知名主机) 该远程主机的名字不能被域名服务器(DNS)转换成IP地址 域名服务器有故障 名字不正确 网络管理员的系统与远程主机之间的通信线路有故障 Noanswer 中心主机没有工作 本地或中心主机网络配置不正确 本地或中心的路由器没有工作 通信线路有故障 网络协议分析 ARP(地址解析协议) 工作在数据链路层 将IP地址转化成物理地址(mac) 在以太网协议中规定,同一局域网中的一台主机要和另一台主机进行直接通信,必须要知道目标主机的MAC地址。而在TCP/IP协议中,网络层和传输层只关心目标主机的IP地址。这就导致在以太网中使用IP协议时,数据链路层的以太网协议接到上层IP协议提供的数据中,只包含目的主机的IP地址。于是需要一种方法,根据目的主机的IP地址,获得其MAC地址。这就是ARP协议要做的事情。所谓地址解析(address resolution)就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。 另外,当发送主机和目的主机不在同一个局域网中时,即便知道对方的MAC地址,两者也不能直接通信,必须经过路由转发才可以。所以此时,发送主机通过ARP协议获得的将不是目的主机的真实MAC地址,而是一台可以通往局域网外的路由器的MAC地址。于是此后发送主机发往目的主机的所有帧,都将发往该路由器,通过它向外发送。这种情况称为委托ARP或ARP代理(ARP Proxy)。 工作原理 有目的主机IP地址对应的MAC地址,直接转发 没有目的主机IP地址对应的MAC地址,在本网段发起ARP请求广播包进行查询 根据源主机的MAC地址信息,更新ARP列表 数据包 长度(位)4848161616881648324832 数据类型目标以太网地址源以太网地址帧类型硬件类型协议类型硬件地址长度协议地址长度操作码源硬件地址源协议地址目标硬件地址目标协议地址 英文名DEST ADDRSRC ADDRTYPEHARDWARE TYPEPROTOCOL TYPEHLENPLENOPCODESOURCE MACSOURCE IPTARGET MACTARGET IP ARP(请求)FF:FF:FF:FF:FF:FF0x00010x08000x060x040x00010000.0000.0000 ARP(回复)0x00010x08000x060x040x0002 硬件类型:如以太网(0x0001)、分组无线网 协议类型:如网际协议(IP)(0x0800)、IPv6(0x86DD) 硬件地址长度:每种硬件地址的字节长度,一般为6(以太网) 协议地址长度:每种协议地址的字节长度,一般为4(IPv4) 操作码:1为ARP请求,2为ARP应答,3为RARP请求,4为RARP应答 源硬件地址:n个字节,n由硬件地址长度得到,一般为发送方MAC地址 源协议地址:m个字节,m由协议地址长度得到,一般为发送方IP地址 目标硬件地址:n个字节,n由硬件地址长度得到,一般为目标MAC地址 目标协议地址:m个字节,m由协议地址长度得到,一般为目标IP地址 前14字节为以太网首部,后28字节为ARP请求/应答 TCP(传输控制协议) 工作在传输层 实现进程到进程的可靠的数据流传送服务 标识主机位置:地址(IP) 标识进程:端口 三次握手 客户端(通过执行connect函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数X作为消息序列号(seq=X) 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为X+1,SYN/ACK包本身携带一个随机产生的序号Y(seq=Y,ack=X+1) 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为X+1,而ACK的确认码则为Y+1。然后客户端的connect函数成功返回(seq=X+1 ack=Y+1) 四次挥手 貌似不考 数据包 长度(位)16163232466161616 数据类型来源连接端口目的连接端口序列号码确认号码资料偏移保留标志位窗口大小校验和紧急指针 英文名SOURCE PORTDESTINATION PORTSEQUENCE NUMBERACKNOWLEDGEMENT NUMBEROFFSETRESERVEDFLAGSWINDOWCHECKSUMURGENT POINTER TCP第一次握手ABX0000b000010 TCP第二次握手BAYX+1000b010010 TCP第三次握手ABX+1Y+1000b010000 实验操作 交换机配置静态路由 PC 1 C:\>ipconfig /all 获取FastEthernet0的物理地址(Physical Address)为00E0.A3BA.8021 交换机 1 Switch(config)# mac-address-table static 00E0.A3BA.8021 vlan 1 interface fa0/1 即完成了将mac地址为00E0.A3BA.8021的PC与fa0/1端口绑定 路由器设置端口 路由器 1 2 Router(config)# interface fa0/0 Router(config-if)# ip address 192.168.1.1 255.255.255.0 完成将fa0/0端口的IP设置为192.168.1.1,掩码为255.255.255.0 三层交换机实现Vlan间通讯 设备属性 PC1IP:192.168.10.10,掩码:255.255.255.0,网关:192.168.10.1 PC2IP:192.168.20.10,掩码:255.255.255.0,网关:192.168.20.1 PC3IP:192.168.10.20,掩码:255.255.255.0,网关:192.168.10.1 PC4IP:192.168.20.20,掩码:255.255.255.0,网关:192.168.20.1 交换机1与PC1和PC2连接,分别连在fa0/1 fa0/2口,fa0/3与三层交换机连接 交换机2与PC3和PC4连接,分别连在fa0/1 fa0/2口,fa0/3与三层交换机连接 三层交换机与交换机1和交换机2连接,分别连在fa0/1 fa0/2口 交换机1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enable configure terminal vlan 10 exit vlan 20 exit interface fa0/1 switchport access vlan 10 exit interface fa0/2 switchport access vlan 20 exit interface fa0/3 switchport mode trunk exit 交换机2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enable configure terminal vlan 10 exit vlan 20 exit interface fa0/1 switchport access vlan 10 exit interface fa0/2 switchport access vlan 20 exit interface fa0/3 switchport mode trunk exit 交换机3 1 2 3 4 5 6 7 8 9 10 11 12 13 enable configure terminal vlan 10 exit vlan 20 exit interface vlan 10 ip address 192.168.10.1 255.255.255.0 exit interface vlan 20 ip address 192.168.20.1 255.255.255.0 exit ip routing 单臂路由 设备属性 PC1IP:192.168.10.10,掩码:255.255.255.0,网关:192.168.10.1 PC2IP:192.168.20.10,掩码:255.255.255.0,网关:192.168.20.1 交换机与PC1和PC2连接,分别连在fa0/1 fa0/2口,fa0/3与路由器连接 路由器与交换机连接,连在fa0/0口 交换机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enable configure terminal vlan 10 exit vlan 20 exit interface fa0/1 switchport access vlan 10 exit interface fa0/2 switchport access vlan 20 exit interface fa0/3 switchport mode trunk exit 路由器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 enable configure terminal interface fa0/0 no shutdown interface fa0/0.1 encapsulation dot1Q 10 ip address 192.168.10.1 255.255.255.0 no shutdown exit interface fa0/0.2 encapsulation dot1Q 20 ip address 192.168.20.1 255.255.255.0 no shutdown exit

2020/12/26
articleCard.readMore

WSL1 使用 Docker 一直无法启动

问题 WSL1 无法正常启动 Docker,Docker一直处于 not running 状态 解决办法 WSL1 是伪 Linux,实际上仍然是 Windows 底层,而 Docker 是基于系统底层实现的,这就导致了无法在 Windows(WSL1) 上运行 Linux 版本的 Docker 使用 WSL2 则可以正常使用 Docker,目前上述问题在不使用 WSL2 的情况下,暂时无法解决

2020/12/24
articleCard.readMore

我的ACM脚印

2020年12月20日,南京区域赛结束,同时结束的,还有我的两年多的ACM生涯 接下来的寒假重心会向着找实习的方向努力,当然明年还有线下的区域赛、EC-finial以及明年的省赛等等,我都会去认真准备。 这篇文章会写什么 关于我 我的ACM简单的回顾 我的ACM成绩 写给新人 ACM到底和数学建模、挑战杯等等的其他竞赛有什么区别 ACM到底带给我什么了 为什么要打ACM 什么样的人适合去打ACM,什么样的人不适合去打ACM 写给已经进入了ACM的人 我在ACM的训练计划 除了ACM之外的计划 关于ACM写题 ACM算法的学习规划 我的一些经验之谈 这篇文章更多的是想来自我总结一下历史,如果与你的理解有出入也请见谅 关于我 我的ACM简单的回顾 进入大学之前 我是2018年进入的大学,在这之前,我压根没有听说过ACM,也完全不知道这类竞赛,高中也是没有打过OI,也就是真正的纯粹的小白。当然,我的高中压根就不知道有什么叫OI的比赛,可能这就是所谓的省B类学校吧 但是我有优势,我从高一开始自学了程序,我当时想自己写游戏,然后学起来Unity了,也就顺便学了C#。至于优势,大概就是对程序有了自己的理解吧。如果让我对代码理解这个事情上进行一个分级的话,我会这样分: 完全不会程序(基本上就是那些完全没有学过代码的人) 学会了顺序、选择、循环语句(一般是刚刚开始学程序的人,对程序是万能的这条表示怀疑的人) 能够灵活的运用上述三种语句(突然发现仅使用这三种语句居然可以实现一切逻辑,相信代码是万能的,只是需要写代码的。通常这类人同样相信代码是高效的,认为所有的事情基本上都可以在电脑上花费一小段的时间就能得出结果) 知道了代码是非常局限的,计算机能计算的速度是非常有限的,在解决一个问题前会思考这个问题的逻辑,对这个问题进行优化以适合计算机去运行,这类人也就是一个ACMer的入门点 而那时候的我,大概就是第三类的人,比起同时期的同学,只能说我拥有着非常好的起点 但是,实际上,通过一个学期的学习,基本上所有的学生都能到达这个水平 大一 大学的第一个学期,课程安排是学C语言,但是我其实并不需要,因为这些东西只需要我把我学C#的知识转成C就行了 而这个学期,校集训队也联系上了我,只不过因为我有提前的知识了,虽然我在那个时候还完全不知道对于代码还有第四层理解 当然,慢慢的我也接触到了很多算法,例如dfs、bfs之类的,只能说我在那个时候对ACM的理解还存在于ACM是提供更多的解决问题的办法而已 后来,学到了在ACM中最重要的东西:复杂度 也慢慢的开始学习到各种基础的算法:gcd、最短路、背包问题、KMP等等之类的 后来,我在大一快结束的时候,和另外两位大一参加了西安邀请赛,然后成功打铁…… 紧接着是校赛,但是那次校赛的难度太高,导致全场只有20个人过题,我有幸过了两题。但是我和之前组队的两个大一的同学分开了队伍。 然后是浙江省省赛,和两个大二的人组队,然后继续打铁 再接下来是南昌邀请赛,我终于拿到了人生以来的第一个奖牌:铜 然后就是整整一个暑假的集训,杭电的多校、牛客的多校,题目的难度对于当时我而言,未免是过高了一些。那两个月,可谓是绝对的自闭 大二 大二开始,大概是因为经历了暑假的自闭式训练,拿下了一个ICPC的区域赛银牌,虽然是银川偷鸡,但基本上是我一个人完成的比赛,而且其实本来很有希望冲击金牌 大二下半学期,因为疫情的原因,荒废了很久,没有出去打比赛,只能说是不断的学习吧。 也趁着疫情,顺便把CodeForces上把我的两个账号都刷到了紫名 当然,因为写的题多了,代码写的多了,感觉自己写题目的习惯开始发生了改变,特别是经常打CodeForces后,感觉自己对很多思维的理解在不断的加深。大一选择了图论方向,大二开始学数据结构,然后再学了字符串,稍微了解了dp,队内也把构造题的任务分配给了我 大三 这个学期难得有了好多场比赛,而我们原来参加了西安邀请赛的三个人,我们重新组成了完整的队伍,也夺下了省赛银牌、CCPC威海铜牌、ICPC南京银牌这样三个牌。 我的ACM成绩 到目前为止,总共拿下了两个ICPC区域赛银牌,一个CCPC的铜牌,一个浙江省省赛银牌,一个ICPC邀请赛铜牌,Codeforces两个账号都是紫名,准备寒假冲击橙名。接下来会参加一场线下的ICPC比赛以及EC-finial。未来可能在能够拿到offer的情况下,继续回来参加ACM竞赛 写给新人 ACM到底和数学建模、挑战杯等等的其他竞赛有什么区别 如果你是计算机学院的,那么你需要追求的、考虑的唯一的竞赛就是ACM ACM是一个非常全面的竞赛,如果你说你只是喜欢数学,那么ACM比数学建模之类的数学竞赛更加具有挑战性,同样,难度更高。对的,在我的认知中,ACM对数学的要求甚至远远高于数学竞赛。因为ACM和其他比赛不一样的一点,便是ACM不设置任何的知识点上界,越新的知识点,越高级的知识点,ACM都可以考。甚至任何一道数学竞赛的题,如果你在ACM中见到,都是合情合理的。在打ACM的时候,这个知识点不会的情况是很正常的,是所有参加了ACM竞赛的人可以深切感受到的。而如果你只是去打数学之类的竞赛,如果你不能到达一个很高的层次,你可能很难体会到那种,自己完全不会,完全是毫无能力的那种绝望感。而在ACM,你可以在任何一场比赛中见到,甚至是随便在点开的一场比赛。 其次,ACM是一个非常公平、公正同时也是非常严格、残酷的比赛,甚至因为它的机制,你可以认为它是你整个大学生涯中见到过,最公平,也是唯一一个能够让参赛选手心服口服的比赛。因为ACM几乎不存在任何的主观因素,你只有准确完整的解决这个问题,你才能拿到那么一点成绩分。而且ACM系列竞赛结束,如果这场比赛有任何一道题有一些问题,通常出题人都会出面道歉。这也是我第一次知道,原来负责出题的人也是要道歉的。从那样的高考制度过来,我们甚至都不会去关心出题人是谁,即使他出了岔子,也会有专门的公关来解决。ACM却没有这些无聊的内容。 数学建模也好,挑战杯也罢,评委老师评分制意味着主观可以胜过客观,甚至,到最后可能变成了PPT大赛。如果说这类竞赛的好处,我觉得除了给你提高了拿到奖学金的可能性,对于自身能力的提升以及后续的工作而言甚至意义并不大。而奖学金,能比得上你找到一个好工作后在一个月内赚的钱多吗?你难道能一辈子拿奖学金过日子吗?当然这样的人是存在的,但是我相信大多数读者也和我一样,觉得这是一种奢望。 ACM到底带给我什么了 知识 我在ACM集训队的第一周,我所得到的知识,是我的室友们在大学四年内都可能会学不清楚、理不通的知识,而那些知识点,在我经历了两年多的ACM训练后觉得,这些只不过应该是理所当然会的、最基本的知识。这些知识点带给我的财富,是我在经历了四五个项目后,才意识到的我们与其他人的差距。ACM的知识点,只要你未来是做计算机行业的,那么它一定会在每一个角落里发挥着它的作用。 思维 ACM的题,对一个人的思维能力的提升有着非常恐怖的作用。特别是当你频繁的打Codeforces比赛时,你会深切的体会到自己的思维能力在以非常恐怖的速度进步。而与思维能力直接挂钩的,便是逻辑思路以及问题的敏感性。如果有人去看过杜老师的Codeforces录播视频,看过tourist的比赛视频,你会发现他们基本上并不会去论证一个方法的可行性,他们通常在读到这道题的时候,会反应过来这题“好像这样搞搞”、“在随便暴力一下”、“应该就对了”。这样的思路也正是我现在打Codeforces的一种感觉,当然我还不能到达杜老师这样对于这么难的题也可以如此轻松的想出解法,但是我仍然觉得我思考问题的思路和方向变得非常的开阔,而且思考问题的逻辑自然变得严谨合理。 代码实现能力 ACM的题,难度较高的题,有些需要各种数据结构的嵌套,需要各种开辟各种奇奇怪怪的数组。而你需要在短时间内完成代码的编写和调试,这无疑是对代码能力的直接挑战。例如Codeforces,5道题目只有两个小时,除掉读题(纯英文题面)和思考问题的时间,你又有多少时间可以来写代码调试代码呢?当室友还在为编译出错烦恼时,我们基本就不需要调试百行代码以下的程序,因为只需要简单的测试证明其确实没有错误即可。 ACM朋友圈 ACM届有一个最高级别的竞赛,被称为WF(world finial)——世界总决赛。这场比赛的含金量有多高?也许有人会说,最多也就是拿到金牌可以全球500强随意挑。但是,实际上只需要你碰到了这个比赛的边界,你只要有幸被邀请参加了这场比赛,不论你的名次,这个星球上的企业你已经可以随意选了,而且本科毕业就可以去工作的那种。而我们平时聊天水群,里面有的是因为ACM成绩优异而进入Google中国、微软亚洲研究院的人。而对于正常学业而言,各位也应该知道你需要读多少年的书才有胆量往这些企业中投递一份简历 学长学姐 通常能坚持下来打ACM的都是能够在思维、能力、勤奋或者智商上略胜一筹的人,那么和这样的学长学姐在同一个实验室的屋檐下打比赛,你能得到的帮助和支持,远远超过参加社团带来的收益。 直面清北复交 ACM竞赛是所有队伍在相同地点使用相同设备在相同的时间内解决相同的题目。 而你的对手则是来自全国的大学,对,北大清华每年都会来,而且非常重视。 ACM从来就没有院赛、校赛、省赛等等一大堆乱七八糟的东西,虽然他们确实存在但是他们并不是被官方承认的。ACM只有区域赛,(比如Asia-East东亚地区),区域总决赛(比如EC-finial,东亚地区总决赛),和世界总决赛(WF)。无论在哪个比赛,你都可能会遇到任何一个学校的队伍。所以在这样的比赛中,你可以很清楚的知道自己的水平在全地区范围内的位置,对自己的能力有一个更好的评估,能够看到外面更加广阔的天空。而不是拘泥于那么小的一个地区,争夺那么毫无意义的第一名 为什么要打ACM 因为我要证明我自己 在ACM比赛中,你会真实的,亲身和北大清华等等高校在同一个体育馆里,使用相同的设备相同的时间来解决相同的问题。那么你为什么不去证明自己比他们强?我知道这通常不可能,因为他们也会派出他们最强的队伍来与你们竞技。但是我们需要的就是在这么多的强队中,证明我们自己的能力。在计算机领域内最有影响力的比赛中,证明自己而已 什么样的人适合去打ACM,什么样的人不适合去打ACM ACM竞赛是一个需要大量的时间去投入,但是到很久之后才会有结果的产出。这和其他竞赛不同,数学建模通常你只需要很短的时间训练就能拿出成绩,而一个ACMer,在大三之前甚至可能都没有一点点成果。但是你在大一大二的投入终将会给你在大二下至大三上的时候带来丰富的回报。 这样的回报,需要愿意投资的人耐心投资才有可能赚得盆满钵满,一旦出现懈怠都有可能颗粒无收。耐心、专注、勤奋、自觉这些是一个ACMer必须要具备的因素。 写给已经进入了ACM的人 我在ACM的训练计划 保持在Codeforces的个人刷题,最好是现场比赛,其次是复现比赛。Codeforces对训练一个人的思维能力有者极大的帮助作用,而且其出题非常的新颖,是我认为最适合ACMer进行个人训练的平台。Codeforces的思维题数量非常庞大,而且非常的有趣。正式的区域赛等比赛,通常思维题也会占据很大一部分比重。 队伍内保持至少一周一场往年比赛的复现赛,我们队在长达几个月的集训时间内保持了一周两场比赛且不耽误正常课程。 当你决定要写ACM题时,请不要断开,也尽可能避免使用碎片化的时间学习,这对学习的进度没有任何帮助,除非你只是突然需要回忆一个知识点。 在实验室内写题而不是在寝室或者图书馆或者其他任何地方写题。 除了ACM之外的计划 ACM毕竟只是我们大学的一个工具,我们希望它能够服务于我们找工作、服务于我们在其他领域的发展。不可以把ACM当成是自己在大学里唯一执着的对象,甚至把它树立为人生目标,这不合实际也没有意义,反而会影响你的正常社交与生活,这不应该是一个人的目标。 关于ACM写题 ACM算法的学习规划 在经历了两年多的学习之后我发现,其实很多的算法并没有太多的学习意义,或者说不必要为其投入过多的经历去学习。 我是负责队伍内的图论+字符串,以及构造题思维题,会数据结构,了解dp和树上问题。 另外一位队友负责计算几何和博弈论,以及数论,会dp,了解图论和树上问题 还有一位队友负责了数据结构和dp,以及树上问题,会数论,了解图论 基本上可以说是覆盖了所有的知识面,而且大部分知识面都是有多个人会。 我以我熟悉的图论为例,诸如“最大流”这些个算法,通常对于一个银牌队伍而言,其实学习的意义并不大。因为我至今未见到过最大流题的难度低于金牌题的(按照实际区域赛出题情况)相反,灵活的结合思维和拓扑排序,你会发现图论问题变得非常简单。很多区域赛的图论的铜牌题在你眼里变成了暴力傻逼题。这是对于一个图论选手在频繁使用图论相关的知识点的时候自然而然形成的。 我认为把学习那些过高的知识点重要性低于去熟练掌握最基本的算法的内容。 对于字符串也一样,上一次看到“回文树”是在复现赛上看到的,是一道金牌题,虽然对于会“回文树”的队伍而言相对简单很多,但是作为一道金牌题,很多时候在比赛现场可能根本没有时间去看这样一道题。 当然你的队伍是为了冲金牌的,这些知识点当然也应该成为你的必须知识点之一。 我的一些经验之谈 看题一定要看数据,通过数据大小猜测算法的复杂度,再去考虑可能的算法逻辑。通常为了卡掉错误的算法,正确算法的复杂度应该在$1e6-1e8$之间,前者考虑可能有很大的常数的复杂度,后者则是最差的不可能发生的情况下的复杂度。 队伍内除了在最后冲刺的时候,其他时间内务必保证多开,无论何时也不要三个人讨论同一个问题,即使你们现在被榜丢下了。甚至很多时候可以尝试三开 队伍中每个人都应该具备非常良好的代码能力,除非你们队伍中专门有数学专业的人帮忙 Shiroha @2020.12.21凌晨1点30分

2020/12/21
articleCard.readMore

【2020HDU多校】第二场1005(HDU6767)New Equipments——费用流

题目链接 题目大意 给出 $n$ 个工人和 $m$ 件装备,装备的编号为 $1, 2, 3 … m$。 对于工人 $i$ ,他有三个参数 $a_i, b_i, c_i$,当为这个工人装备了第 $j$ 个装备时,需要花费 $a_i j ^ 2+ b_i j + c_i$ 的费用。 当为 $k$ 个工人装备上装备时,最小花费是多少。 对所有的 $k$ 的情况均需要输出 分析 费用流 将每个员工与源点链接,取每个员工的二次函数曲线的最小值附近的 $n$ 个点,与员工相连,所有的在二次函数曲线上的点与汇点相连,员工连接到二次函数的线需要设定费用,其他线费用均为 $0$,所有线的流量均为 $1$ 输出每次 spfa 过程中得到的费用即可 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 #include <bits/stdc++.h> using namespace std; #define ll long long const ll maxn = 3000; const ll INF = 0x3f3f3f3f3f3f3f3f; bool flag; struct Edge { ll from, to, cap, flow, cost; Edge(ll u, ll v, ll c, ll f, ll cc) : from(u), to(v), cap(c), flow(f), cost(cc) {} }; struct MCMF { ll n, m; vector<Edge> edges; vector<ll> G[maxn]; ll inq[maxn]; //是否在队列中 ll d[maxn]; //bellmanford ll p[maxn]; //上一条弧 ll a[maxn]; //可改进量 void init(ll nn) { this->n = nn; for (ll i = 0; i <= n; ++i) G[i].clear(); edges.clear(); } void addEdge(ll from, ll to, ll cap, ll cost) { edges.emplace_back(Edge(from, to, cap, 0, cost)); edges.emplace_back(Edge(to, from, 0, 0, -cost)); m = (ll) (edges.size()); G[from].emplace_back(m - 2); G[to].emplace_back(m - 1); } bool spfa(ll s, ll t, ll &flow, ll &cost) { for (ll i = 1; i <= n; ++i) d[i] = INF; memset(inq, 0, sizeof(inq)); d[s] = 0; inq[s] = 1; p[s] = 0; queue<ll> q; a[s] = INF; q.push(s); while (!q.empty()) { ll u = q.front(); q.pop(); inq[u] = 0; for (ll i = 0; i < (ll) (G[u].size()); ++i) { Edge &e = edges[G[u][i]]; if (e.cap > e.flow && d[e.to] > d[u] + e.cost) { d[e.to] = d[u] + e.cost; p[e.to] = G[u][i]; a[e.to] = min(a[u], e.cap - e.flow); if (!inq[e.to]) { q.push(e.to); inq[e.to] = 1; } } } } if (d[t] == INF) return false; flow += a[t]; cost += (ll) d[t] * (ll) a[t]; cout << (flag ? " " : "") << cost; flag = true; for (ll u = t; u != s; u = edges[p[u]].from) { edges[p[u]].flow += a[t]; edges[p[u] ^ 1].flow -= a[t]; } return true; } ll MincostMaxflow(ll s, ll t, ll &cost) { ll flow = 0; cost = 0; while (spfa(s, t, flow, cost)); return flow; } } mcmf; struct Node { ll a, b, c; ll l, r; ll cal(ll x) { return a * x * x + b * x + c; } void make(ll n, ll m) { ll mid = -b / 2 / a; l = mid - n / 2 - 1; r = mid + n / 2 + 1; l = max(1ll, l); r = min(m, r); if (r == m) { l = r - n - 2; } else if (l == 1) { r = l + n + 2; } } } node[60]; void solve() { ll T; cin >> T; for (ll ts = 0; ts < T; ++ts) { flag = false; unordered_map<ll, ll> trans; ll n, m; cin >> n >> m; ll ind = 51; for (ll i = 1; i <= n; ++i) { cin >> node[i].a >> node[i].b >> node[i].c; node[i].make(n, m); ll l = node[i].l; ll r = node[i].r; assert(r - l < 60); for (ll j = l; j <= r; ++j) { if (trans.count(j)) continue; trans.insert({j, ind++}); } } ll source = ind + 10, target = ind + 11; mcmf.init(target + 10); #ifdef ACM_LOCAL cerr << target + 10 << endl; #endif for (auto &item : trans) mcmf.addEdge(item.second, target, 1, 0); for (ll i = 1; i <= n; ++i) { mcmf.addEdge(source, i, 1, 0); for (ll j = node[i].l; j <= node[i].r; ++j) { mcmf.addEdge(i, trans[j], 1, node[i].cal(j)); } } ll cost; mcmf.MincostMaxflow(source, target, cost); cout << endl; } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); signed test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/7/24
articleCard.readMore

从后缀自动机到广义后缀自动机

广义后缀自动机 前置知识 广义后缀自动机基于下面的知识点 字典树(Trie树) 后缀自动机 请务必对上述两个知识点非常熟悉之后,再来阅读本文,特别是对于后缀自动机中的后缀链接能够有一定的理解 起源 广义后缀自动机是由刘研绎在其2015 国家队论文《后缀自动机在字典树上的拓展》上提出的一种结构,即将后缀自动机直接建立在字典树上。 大部分可以用后缀自动机处理的字符串的问题均可扩展到 Trie 树上。——刘研绎 约定 参考字符串约定 字符串个数为 $k$ 个,即 $S_1, S_2, S_3 … S_k$ 约定字典树和广义后缀自动机的根节点为 $0$ 号节点 概述 后缀自动机 (suffix automaton, SAM) 是用于处理单个字符串的子串问题的强力工具。 而广义后缀自动机 (General Suffix Automaton) 则是将后缀自动机整合到字典树中来解决对于多个字符串的子串问题 常见的伪广义后缀自动机 通过用特殊符号将多个串直接连接后,再建立 SAM 对每个串,重复在同一个 SAM 上进行建立,每次建立前,将 last 指针置零 方法1和方法2的实现方式简单,而且在面对题目时通常可以达到和广义后缀自动机一样的正确性。所以在网络上很多人会选择此类写法,例如在后缀自动机一文中最后一个应用,便使用了方法1(原文链接) 但是无论方法1还是方法2,其时间复杂度较为危险 构造广义后缀自动机 根据原论文的描述,应当在多个字符串上先建立字典树,然后在字典树的基础上建立广义后缀自动机。 字典树的使用 首先应对多个串创建一棵字典树,这不是什么难事,如果你已经掌握了前置知识的前提下,可以很快的建立完毕。这里为了统一上下文的代码,给出一个可能的字典树代码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define MAXN 2000000 #define CHAR_NUM 30 struct Trie{ int next[MAXN][CHAR_NUM]; // 转移 int tot; // 节点总数:[0, tot) void init() { tot = 1; } int insertTrie(int cur, int c) { if (next[cur][c]) return next[cur][c]; return next[cur][c] = tot++; } void insert(const string &s) { int root = 0; for (auto ch : s) root = insertTrie(root, ch - 'a'); } }; 这里我们得到了一棵依赖于 next 数组建立的一棵字典树。 后缀自动机的建立 如果我们把这样一棵树直接认为是一个后缀自动机,则我们可以得到如下结论 对于节点 i ,其 len[i] 和它在字典树中的深度相同 如果我们对字典树进行拓扑排序,我们可以得到一串根据 len 不递减的序列。$BFS$ 的结果相同 而后缀自动机在建立的过程中,可以视为不断的插入 len 严格递增的值,且插值为 $1$。所以我们可以将对字典树进行拓扑排序后的结果做为一个队列,然后按照这个队列的顺序不断地插入到后缀自动机中。 由于在普通后缀自动机上,其前一个节点的 len 值为固定值,即为 last 节点的 len。但是在广义后缀自动机中,插入的队列是一个不严格递增的数列。所以对于每一个值,对于它的 last 应该是已知而且固定的,在字典树上,即为其父亲节点。 由于在字典树中,已经建立了一个近似的后缀自动机,所以只需要对整个字典树的结构进行一定的处理即可转化为广义后缀自动机。我们可以按照前面提出的队列顺序来对整个字典树上的每一个节点进行更新操作。最终我们可以得到广义后缀自动机。 对于每个点的更新操作,我们可以稍微修改一下SAM中的插入操作来得到。 对于整个插入的过程,需要注意的是,由于插入是按照 len 不递减的顺序插入,在进行 $clone$ 后的数据复制过程中,不可以复制其 len 小于当前 len 的数据。 算法 根据上述的逻辑,可以将整个构建过程描述为如下操作 将所有字符串插入到字典树中 从字典树的根节点开始进行 $BFS$,记录下顺序以及每个节点的父亲节点 将得到的 $BFS$ 序列按照顺序,对每个节点在原字典树上进行构建,注意不能将 len 小于当前 len 的数据进行操作 对操作次数为线性的证明 由于仅处理 $BFS$ 得到的序列,可以保证字典树上所有节点仅经过一次。 对于最坏情况,考虑字典树本身节点个数最多的情况,即任意两个字符串没有相同的前缀,则节点个数为 $\sum_{i=1}^{k}|S_i|$,即所有的字符串长度之和。 而在后缀自动机的更新操作的复杂度已经在后缀自动机中证明 所以可以证明其最坏复杂度为线性 而通常伪广义后缀自动机的平均复杂度等同于广义后缀自动机的最差复杂度,面对对于大量的字符串时,伪广义后缀自动机的效率远不如标准的广义后缀自动机 实现 对插入函数进行少量必要的修改即可得到所需要的函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 struct GSA{ int len[MAXN]; // 节点长度 int link[MAXN]; // 后缀链接,link int next[MAXN][CHAR_NUM]; // 转移 int tot; // 节点总数:[0, tot) int insertSAM(int last, int c) { int cur = next[last][c]; len[cur] = len[last] + 1; int p = link[last]; while (p != -1) { if (!next[p][c]) next[p][c] = cur; else break; p = link[p]; } if (p == -1) { link[cur] = 0; return cur; } int q = next[p][c]; if (len[p] + 1 == len[q]) { link[cur] = q; return cur; } int clone = tot++; for (int i = 0; i < CHAR_NUM; ++i) next[clone][i] = len[next[q][i]] != 0 ? next[q][i] : 0; len[clone] = len[p] + 1; while (p != -1 && next[p][c] == q) { next[p][c] = clone; p = link[p]; } link[clone] = link[q]; link[cur] = clone; link[q] = clone; return cur; } void build() { queue<pair<int, int>> q; for (int i = 0; i < 26; ++i) if (next[0][i]) q.push({i, 0}); while (!q.empty()) { auto item = q.front(); q.pop(); auto last = insertSAM(item.second, item.first); for (int i = 0; i < 26; ++i) if (next[last][i]) q.push({i, last}); } } } 由于整个 $BFS$ 的过程得到的顺序,其父节点始终在变化,所以并不需要保存 last 指针。 插入操作中,int cur = next[last][c]; 与正常后缀自动机的 int cur = tot++; 有差异,因为我们插入的节点已经在树型结构中完成了,所以只需要直接获取即可 在 $clone$ 后的数据拷贝中,有这样的判断 next[clone][i] = len[next[q][i]] != 0 ? next[q][i] : 0; 这与正常的后缀自动机的直接赋值 next[clone][i] = next[q][i]; 有一定差异,此次是为了避免更新了 len 大于当前节点的值。由于数组中 len 当且仅当这个值被 $BFS$ 遍历并插入到后缀自动机后才会被赋值 性质 广义后缀自动机与后缀自动机的结构一致,在后缀自动机上的性质绝大部分均可在广义后缀自动机上生效(后缀自动机的性质) 当广义后缀自动机建立后,通常字典树结构将会被破坏,即通常不可以用广义后缀自动机来解决字典树问题。当然也可以选择准备双倍的空间,将后缀自动机建立在另外一个空间上。 应用 所有字符中不同子串个数 可以根据后缀自动机的性质得到,以点 $i$ 为结束节点的子串个数等于 $len[i] - len[link[i]]$ 所以可以遍历所有的节点求和得到 例题:【模板】广义后缀自动机(广义 SAM) ??? note “参考代码” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <bits/stdc++.h> using namespace std; #define MAXN 2000000 // 双倍字符串长度 #define CHAR_NUM 30 // 字符集个数,注意修改下方的 (-'a') struct exSAM { int len[MAXN]; // 节点长度 int link[MAXN]; // 后缀链接,link int next[MAXN][CHAR_NUM]; // 转移 int tot; // 节点总数:[0, tot) void init() { tot = 1; link[0] = -1; } int insertSAM(int last, int c) { int cur = next[last][c]; if (len[cur]) return cur; len[cur] = len[last] + 1; int p = link[last]; while (p != -1) { if (!next[p][c]) next[p][c] = cur; else break; p = link[p]; } if (p == -1) { link[cur] = 0; return cur; } int q = next[p][c]; if (len[p] + 1 == len[q]) { link[cur] = q; return cur; } int clone = tot++; for (int i = 0; i < CHAR_NUM; ++i) next[clone][i] = len[next[q][i]] != 0 ? next[q][i] : 0; len[clone] = len[p] + 1; while (p != -1 && next[p][c] == q) { next[p][c] = clone; p = link[p]; } link[clone] = link[q]; link[cur] = clone; link[q] = clone; return cur; } int insertTrie(int cur, int c) { if (next[cur][c]) return next[cur][c]; return next[cur][c] = tot++; } void insert(const string &s) { int root = 0; for (auto ch : s) root = insertTrie(root, ch - 'a'); } void insert(const char *s, int n) { int root = 0; for (int i = 0; i < n; ++i) root = insertTrie(root, s[i] - 'a'); } void build() { queue<pair<int, int>> q; for (int i = 0; i < 26; ++i) if (next[0][i]) q.push({i, 0}); while (!q.empty()) { auto item = q.front(); q.pop(); auto last = insertSAM(item.second, item.first); for (int i = 0; i < 26; ++i) if (next[last][i]) q.push({i, last}); } } } exSam; char s[1000100]; int main() { int n; cin >> n; exSam.init(); for (int i = 0; i < n; ++i) { cin >> s; int len = strlen(s); exSam.insert(s, len); } exSam.build(); long long ans = 0; for (int i = 1; i < exSam.tot; ++i) { ans += exSam.len[i] - exSam.len[exSam.link[i]]; } cout << ans << endl; } 多个字符串间的最长公共子串 我们需要对每个节点建立一个长度为 $k$ 的数组 flag(对于本题而言,可以仅为标记数组,若需要求出此子串的个数,则需要改成计数数组) 在字典树插入字符串时,对所有节点进行计数,保存在当前字符串所在的数组 然后按照 len 递减的顺序遍历,通过后缀链接将当前节点的 flag 与其他节点的合并 遍历所有的节点,找到一个 len 最大且满足对于所有的 k ,其 flag 的值均为非 $0$ 的节点,此节点的 $len$ 即为解 例题:SPOJ Longest Common Substring II ??? note “参考代码” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 #include <bits/stdc++.h> using namespace std; #define MAXN 2000000 // 双倍字符串长度 #define CHAR_NUM 30 // 字符集个数,注意修改下方的 (-'a') #define NUM 15 // 字符串个数 struct exSAM { int len[MAXN]; // 节点长度 int link[MAXN]; // 后缀链接,link int next[MAXN][CHAR_NUM]; // 转移 int tot; // 节点总数:[0, tot) int lenSorted[MAXN]; // 按照 len 排序后的数组,仅排序 [1, tot) 部分,最终下标范围 [0, tot - 1) int sizeC[MAXN][NUM]; // 表示某个字符串的子串个数 int curString; // 字符串实际个数 /** * 计数排序使用的辅助空间数组 */ int lc[MAXN]; // 统计个数 void init() { tot = 1; link[0] = -1; } int insertSAM(int last, int c) { int cur = next[last][c]; len[cur] = len[last] + 1; int p = link[last]; while (p != -1) { if (!next[p][c]) next[p][c] = cur; else break; p = link[p]; } if (p == -1) { link[cur] = 0; return cur; } int q = next[p][c]; if (len[p] + 1 == len[q]) { link[cur] = q; return cur; } int clone = tot++; for (int i = 0; i < CHAR_NUM; ++i) next[clone][i] = len[next[q][i]] != 0 ? next[q][i] : 0; len[clone] = len[p] + 1; while (p != -1 && next[p][c] == q) { next[p][c] = clone; p = link[p]; } link[clone] = link[q]; link[cur] = clone; link[q] = clone; return cur; } int insertTrie(int cur, int c) { if (!next[cur][c]) next[cur][c] = tot++; sizeC[next[cur][c]][curString]++; return next[cur][c]; } void insert(const string &s) { int root = 0; for (auto ch : s) root = insertTrie(root, ch - 'a'); curString++; } void insert(const char *s, int n) { int root = 0; for (int i = 0; i < n; ++i) root = insertTrie(root, s[i] - 'a'); curString++; } void build() { queue<pair<int, int>> q; for (int i = 0; i < 26; ++i) if (next[0][i]) q.push({i, 0}); while (!q.empty()) { auto item = q.front(); q.pop(); auto last = insertSAM(item.second, item.first); for (int i = 0; i < 26; ++i) if (next[last][i]) q.push({i, last}); } } void sortLen() { for (int i = 1; i < tot; ++i) lc[i] = 0; for (int i = 1; i < tot; ++i) lc[len[i]]++; for (int i = 2; i < tot; ++i) lc[i] += lc[i - 1]; for (int i = 1; i < tot; ++i) lenSorted[--lc[len[i]]] = i; } void getSizeLen() { for (int i = tot - 2; i >= 0; --i) for (int j = 0; j < curString; ++j) sizeC[link[lenSorted[i]]][j] += sizeC[lenSorted[i]][j]; } void debug() { cout << " i len link "; for (int i = 0; i < 26; ++i) cout << " " << (char) ('a' + i); cout << endl; for (int i = 0; i < tot; ++i) { cout << "i: " << setw(3) << i << " len: " << setw(3) << len[i] << " link: " << setw(3) << link[i] << " Next: "; for (int j = 0; j < CHAR_NUM; ++j) { cout << setw(3) << next[i][j]; } cout << endl; } } } exSam; int main() { exSam.init(); string s; while (cin >> s) { exSam.insert(s); } exSam.build(); exSam.sortLen(); exSam.getSizeLen(); int ans = 0; for (int i = 0; i < exSam.tot; ++i) { bool flag = true; for (int j = 0; j < exSam.curString; ++j) { if (!exSam.sizeC[i][j]) { flag = false; break; } } if (flag) ans = max(ans, exSam.len[i]); } cout << ans << endl; }

2020/7/23
articleCard.readMore

2020牛客暑期多校训练营(第三场)D-Points Construction Problem——构造

D-Points Construction Problem 思路 第一点:千万不要考虑矩阵,千万不要考虑矩阵,千万不要考虑矩阵。因为完全可以是两个三个矩阵和几条链组成,这实在过于难考虑 这道题最难以考虑的地方就是矩阵的构造。这里给出一个思路去解决这个问题。 当然可能这个方法不是最正确的,但是结果是最优(毕竟AC了) 计算缺失边数 这个应该相对简单,即公式 (n * 4 - m) / 2 的结果 类矩阵结构 这里我们仅考虑非单链的结构,即可以出现矩阵的结构,即 $缺失边数 \geq 4$的情况 我们首先给出一个矩阵的核心部分,暂时称其为“核” 这个核有一个特性:4个点能够增加4条边,记作: $4 \rightarrow 4$ 这是一个矩阵的基础,而且一个矩阵仅需要一个核。 接下来最贪心的方法就是放下这样两个蓝色的点 这个结构能够实现用2个点增加3条边,记作: $2 \rightarrow 3$ 同样,我们也可以在上面放下这样的结构 同样被记作: $2 \rightarrow 3$ 值得注意的是:核结构 $4 \rightarrow 4$ 是所有类矩阵结构的前提,但是由于其产生的连边数量非常少,所以尽可能的减少其使用,即整个图结构仅使用一次 $4 \rightarrow 4$。而 $2 \rightarrow 3$ 则没有次数限制,可以向上也可以向右 在上图的基础上,我们还可以提出一个结构: 这个橙色的点非常的巧妙,其实现了一个点新增了两条边,记作 $1 \rightarrow 2$ 很明显,$1 \rightarrow 2$ 结构是最优的,结构越多则越能用较少的点来实现缺失的边的需求。所以我们需要尽可能的增加 $1 \rightarrow 2$ 的结构 但是,此结构有数量限制,其数量受到 $2 \rightarrow 3$ 的数量限制。 再考虑到矩阵的结构能够带来更多的 $1 \rightarrow 2$ 结构,所以我们选择采用如下的贪心策略 先放一个$2*2$的矩阵 向上/右扩展 用 $1 \rightarrow 2$ 结构填充矩阵 向右/上扩展 用 $1 \rightarrow 2$ 结构填充矩阵 重复 $2-6$ 直到缺失边全部被满足 如果使用的点数超出提供的,则无解,否则将多余的点数放在遥远的天边,然后输出 剩余不满足结构 由于上述策略可能会出现遗留下 $1至2$ 条缺失边,则我们可以把点放在矩阵的左下角,即图中的× 则可以满足一条缺失边或者两条缺失边的要求 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 #include <bits/stdc++.h> using namespace std; bool flag[60][60]; void print(int n) { if (n < 0) { cout << "No" << endl; return; } cout << "Yes" << endl; while (n--) cout << n * 100 << " " << n * 100 << endl; for (int i = 0; i < 60; ++i) for (int j = 0; j < 60; ++j) if (flag[i][j]) cout << i + 1 << " " << j + 1 << endl; } void solve() { int T; cin >> T; for (int ts = 0; ts < T; ++ts) { int n, m; cin >> n >> m; int target = (n * 4 - m) / 2; if ((n * 4 - m) & 1) { n = -1; print(n); continue; } memset(flag, 0, sizeof(flag)); if (target < 4) { int x = 2; flag[1][1] = true; n--; while (target && n >= 0) { flag[x][1] = true; x++; target--; n--; } print(n); continue; } flag[1][1] = flag[1][2] = flag[2][1] = flag[2][2] = true; n -= 4; target -= 4; int l = 3, r = 3; while (target > 2) { // 右扩展 flag[1][l] = true; flag[2][l] = true; l++; target -= 3; n -= 2; int len = 3; while (len < r && target > 1) { flag[len][l - 1] = true; target -= 2; n--; len++; } if (target > 2) { // 上扩展 flag[r][1] = true; flag[r][2] = true; r++; target -= 3; n -= 2; len = 3; while (len < l && target > 1) { flag[r - 1][len] = true; target -= 2; n--; len++; } } } if (target == 2) { n -= 2; flag[0][1] = true; flag[1][0] = true; } else if (target == 1) { n -= 1; flag[0][1] = true; } print(n); } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); int test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/7/18
articleCard.readMore

2020牛客暑期多校训练营(第三场)E-Two Matchings——复杂思维与简单dp

E-Two Matchings 比赛期间写博文,队友我家挖祖坟 数论只会g c d,队友AC我挂机 题目连接 注意本文中的部分字母和原文稍有不同,请注意! 题意 定义序列 $a$ ,满足如下要求 长度为 $n$ 的序列 $a$ 由 $1, 2, 3… n$ 组成 $a_{a_i} = i$ $a_i \neq i$ 定义一个字符串的费用为$\sum_{i=1}^{n}w_i - w_{a_i}/2$ , $w$ 为给出的权值数组 求两个满足上述对序列 $a$ 的描述的序列 $p, q$,同时还要满足 $p_i \neq q_i$ 对于每一个 $i$ 都成立 则这两个序列的费用和的最小值是多少 分析 根据条件 长度为 $n$ 的序列 $a$ 由 $1, 2, 3… n$ 组成 $a_{a_i} = i$ $a_i \neq i$ 可以得到序列是由基础序列 $1, 2, 3…n$ 通过进行两两对调得到,且每个值进行且只进行一次对调。(这里就不仔细证明了,应该……在打这个比赛的人应该都能理解吧) 而我们需要得到的就是两个费用最小的串,即最小串和次小串 注意,接下来的讨论仅讨论排序后的下标,即如果写着 $1$ 则指代 sort 后的数组 $w$ 中最小的值 最小值 首先是最小的值,那很明显,把 w 数组排序后,间隔着相减就可以得到,例如下面已经排序后的下标序列: $$1, 2, 3, 4, 5, 6$$ 我们可以得到其最小的解为 $$(2 - 1) + (4 - 3) + (6 - 5)$$ 我们暂时不去处理这个解,保留原样 次小值 接下来是次优解,首先应当保证其每一位的值不相同 由于我们已经将最小值的组合取完了,则次优解就有了非常多的限制 我们可以“旋转”这个数列得到 $$2, 3, 4, 5, 6, 1\rightarrow (3 - 2) + (5 - 4) + (6 - 1)$$ 把这个“旋转”暂时称为 $6-rotation$,指代 $6$ 个元素的旋转 而此时即为次优的解。 证明 我们以六个数字的数列来证明上述操作 首先用 $-$ 表示这个值作为其所在的交换中的较小值, $+$ 表示这值作为其所在的交换中的较大值 例如最小值可以表示为 123456 -+-+-+ 我们并不需要具体考虑哪个值与哪个值交换,因为最终的求和结果是一样的,即上面的值与下面的符号结合后相加就是最终结果。 除去最小解后,我们只有以下两种组合方法 index123456 min-+-+-+ plan 1--+-++ plan 2---+++ error--++-+ 这里举例一个错误的方案,虽然看起来此方案是与最小值方案不同,但是注意一下最后两个值,无论这个错误方案怎么组合,$5-6$ 必然要发生组合并发生交换,则与最小值的方案出现重复,则不行。 那么我们比较一下这两个方案哪个更优 $$\frac{方案1}{方案2} = \frac{-1-2+3-4+5+6}{-1-2-3+4+5+6} = \frac{-1}{1} $$ (使用分数线仅用于视觉上更好的体现上下的对比效果,并无除法运算思想,下同) 注意,这里不能取 $abs$ 因为在配对的时候我们已经保证了右边的加号匹配左边的减号,即必定为正数 很明显,方案1更优,即上方的次优解 (感谢 @yyymmmi 和 @hnust_zhangpeng 指出错误,现已更正) 合并最小值和次小值 我们将两个解相加发现最终结果为 $$[(2 - 1) + (4 - 3) + (6 - 5)] + [(3 - 2) + (5 - 4) + (6 - 1)] =2 * (6 - 1)$$ 长度不及 $6$ 的时候 而对于长度仅为 $4$ 的串,只能 $4-rotation$ ,即 $$1, 2, 3, 4 \rightarrow (4-rotation)\rightarrow 2, 3, 4, 1 \rightarrow (3 - 2) + (4 - 1)$$ 此时的最终结果为(过程忽略) $$2 * (4 - 1)$$ 长度为$8$的时候 那么我们再往长度增长的方向考虑,当 $n = 8$ 时,我们有两个方案, 两个 $4-rotation$ ( $1234$ 和 $5678$ )来旋转它 两个 $4-rotation$ ( $1278$ 和 $3456$ )来旋转它 一个 $8-rotation$ 来旋转它 注意,此题是不存在 $2-rotation$ 的,因为这毫无意义,所以 $n = 8$ 时,没有一个 $6-rotation$ 和一个 $2-rotation$ 这样的组合。 先比较一下两个 $4-rotation$: $$\frac{方案1}{方案2} = \frac{2 * [(4 - 1) + (8 - 5)]}{2 * [(8 - 1) + (6 - 3)]} = \frac{12}{20}$$ 我们选择使用方案 $1$ 接下来是方案 $1$ 和方案 $3$ 的比较 $$\frac{方案1}{方案3} = \frac{2 * [(4 - 1) + (8 - 5)]}{2 * [(8 - 1)]} = \frac{12}{14}$$ 此时证明得到方案 $1$ 在三个方案内最优,此时 $n =8$ 时的答案为: $$2 * [(4 - 1) + (8 - 5)]$$ 同时我们得到了一个结论:仅存在 $4-rotation$ 和 $6-rotation$ 两种旋转,如果存在 $8-rotation$ 则可以将此 $8-rotation$ 分解为两个 $4-rotation$ 可以更优。 长度更长的时候 当 $n \geq 10$ 时,即可以将整个串分解成多个 $4-rotation$ 和多个 $6-rotation$ 组成。 那么得到了 $dp$ 的递推公式:dp[i] = min(dp[i - 4] + v[i - 1] - v[i - 4], dp[i - 6] + v[i - 1] - v[i - 6]) 注意 $dp$ 的初始值有三个:$n = 4, n = 6, n = 8 \space (防止n = 8的时候出现2-rotation)$ AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <bits/stdc++.h> using namespace std; long long dp[200100]; void solve() { int T; cin >> T; for (int ts = 0; ts < T; ++ts) { int n; cin >> n; vector<long long> v; for (int i = 0; i < n; ++i) { long long tmp; cin >> tmp; v.push_back(tmp); } sort(v.begin(), v.end()); dp[0] = 0; dp[4] = v[3] - v[0]; dp[6] = v[5] - v[0]; dp[8] = v[7] - v[4] + dp[4]; for (int i = 10; i <= n; i += 2) dp[i] = min(dp[i - 4] + v[i - 1] - v[i - 4], dp[i - 6] + v[i - 1] - v[i - 6]); cout << dp[n] * 2 << endl; } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); int test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; } 事后发现其实代码有越界的问题……但是它AC了

2020/7/18
articleCard.readMore

2020牛客暑期多校训练营(第二场)I-Interval——最大流转对偶图求最短路

题目链接 题意 给出一个区间 $[l ,r]$ ,允许进行如下操作: 将 $[l, r]$ 转为 $[l - 1, r]$ 或者 $[l + 1, r]$ 将 $[l, r]$ 转为 $[l, r - 1]$ 或者 $[l, r + 1]$ 且保证 $l \leq r \space and \space l > 0 \space r \leq n$ 但是给出了一系列的限制 $l, r, dir, c$ ,表示当前区间为 $[l, r]$ 时,限制当前的区间不能进行操作 $1$(dir = L)或者操作 $2$ (dir = R),而启用这个限制则需要 $c$ 的费用,你可以选择是否启用这个限制 询问最少需要花费多少来实现不能将区间 $[1, n]$ 转变为 $l = r$ 的区间。 分析 从 $1, n$ 能否转变为 $l = r$ 可以通过最短路来求算。但是无法求知当最短路无法到达时(即题目要求的不能转变)最少需要多少的限制条件,而这些条件又是什么。所以采用最大流来解决 最大流 画出网格图 将所有可以转换的两个状态之间用边连接,如果有提供限制的,将流量限制为费用,如果没有限制的,则设置为 $INF$ 对于整个矩阵而言,只需要一半的点用于建图,所以将汇点放在另外一半点中。所有 $l = r$ 的点与汇点连接,而源点为 $[1, n]$ 对于样例可以得到如下图 样例: 3 4 1 3 L 10 1 3 R 3 1 2 L 1 1 2 R 1 补充,图片漏画了$[2, 3] \rightarrow [2, 2]$的连线,其流量为 $INF$ 可以直接通过最大流求出答案 但是会TLE 对偶图 对偶图Wikipedia(https://en.wikipedia.org/wiki/Dual_graph) 通过对偶图,可以快速的将一张网格网络图求最大流转为求最短路 关于对偶图的解释请自行查阅资料 在原图上绘制对偶图得到 将对偶图中有用的元素将其分离出来得到 (图中未注明边权的边均为 $0$) 可以通过最短路快速得到解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <bits/stdc++.h> using namespace std; #define ll long long const int maxn = 510; int n, m; ll dis[maxn * maxn]; char si; vector<pair<ll, int>> G[maxn * maxn]; void addedge(int u, int v, int cost) { G[u].push_back({cost, v}); } ll dijkstra(int s, int t) { memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> q; q.push({0ll, s}); while (!q.empty()) { ll u = q.top().second, c = q.top().first; q.pop(); if (dis[u] < c)continue; for (auto i : G[u]) { ll cc = i.first, v = i.second; if (dis[v] > dis[u] + cc) { dis[v] = dis[u] + cc; q.push({dis[v], v}); } } } return dis[t]; } inline int id(int x, int y) { return x * (n + 3) + y; } void solve() { cin >> n >> m; for (int i = 0; i < m; ++i) { int u, v, w; char c; cin >> u >> v >> c >> w; if (c == 'L') { addedge(id(u, v), id(u, v + 1), w); addedge(id(u, v + 1), id(u, v), w); } else { addedge(id(u, v), id(u - 1, v), w); addedge(id(u - 1, v), id(u, v), w); } } for (int i = 1; i <= n; ++i) { addedge(id(0, 0), id(0, i), 0); addedge(id(i, n + 1), id(n + 1, n + 1), 0); } dijkstra(id(0, 0), id(n + 1, n + 1)); if (dis[id(n + 1, n + 1)] >= 0x3f3f3f3f3f3f3f3f) cout << -1 << endl; else cout << dis[id(n + 1, n + 1)] << endl; } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); int test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/7/16
articleCard.readMore

2020牛客暑期多校训练营(第二场)H-Happy Triangle——动态开点线段树+STL+区间化点

在WA了好多发之后,终于找到了我不小心写错的bug……我是SB 我的写法与网络上很多人的差异较大,但是个人觉得比其他人的更容易理解。第一次写动态开点的线段树,直接稍微改动了一下原本自己习惯的线段树板子,所以可能与其他板子不同。同时因为是改了线段树的板子,所以反而更容易看懂。其次就是个人感觉我的写法比题解要简单很多,而且码量很小 题意 对于一个可重复集合,进行Q次操作。集合起始的时候为空,操作类型如下 往集合中加入一个元素 从集合中删除一个元素(保证其存在) 给定一个元素 $x$ ,问集合中是否存在另外两个元素$a, b$(允许值相同但是不允许元素相同),使得$a, b, x$三条边可以组成一个非退化三角形(即满足任意两边之和大于第三边,或任意两边之差小于第三边)。 分析 分析三角形公式 首先根据公式$a + b > c$ 转为 $c - b<a$ (假定$a \leq b \leq c$),那么我们可以得到,下面的结论: 假定存在 $a \leq b$ 满足 $a, b, c$ 三边能够组成三角形,那么对于 $a \leq a’ \leq b$ 必定存在 $a’, b, c$ 可以组成三角形(由 $c - b<a \leq a’$ 证得) 那么我们可以指定如下规定: 对于输入的 $x$ ,我们找到两条长度分别为 $a, b$ 的边,使得 $a, b, x$ 能够组成三角形,且不存在 $a’$ 满足 $a < a’ \leq b$,这里我们暂时称 $a, b$ 相邻(这点非常重要!!!) 即 $a, b$ 在整个集合排序后,在数组中的下标差为 $1$ 接下来考虑如何找到 $a, b$。题解中提到类似分类讨论,但是我觉得没有必要。我们仅考虑通过 $a, b$ 的运算后的结果与 $x$ 来比较,最终得到我们的结果是否符合。根据 $a, b, x$ 的大小关系讨论三种情况:(前提 $a \leq b$) $x$ 为最大值时,我们只需要保证 $a + b > x$ 即可 $a \leq x \leq b$ 时,我们需要保证 $a + x > b$ ,转换后得到 $b - a < x$ $x \leq a \leq b$ 时,我们需要保证 $x + a > b$ ,转换后得到 $b - a < x$ 总结:我们只需要保证我们选出来的$a, b$ 保证 $a + b > x \space and \space b - a < x$。由于 $a \leq b$ 所以$b \geq x / 2$(请先记住这个结论,将会在之后用到) 接下来是解决 $a + b$ 和 $b - a$ 的数据存储和更新问题(由于询问是在线询问,而 $a, b$ 相邻,所以随着插入新的数据,这两个值都会发生变化)。 对于 $a + b$ 的处理: 我们将所有当前在集合中的数据进行排序,可以使用 multiset 来实现,但是我个人不太喜欢 multi 的数据结构,所以我选择了 map ,first 保存数据的值,second 保存了数据重复的个数。从此开始,我们暂时不讨论重复值的情况。对于排序好的数据,我们可以通过二分数值来得到 $x / 2$ 在数组中的哪个位置。由于 $a \leq b$ ,所以只有两种可能 $a < x/2 \space and \space b > x / 2$ $a, b \geq x / 2$ 后者很好解决,只需要取值的时候,数组下标大于 $x/2$ 所在的下标位置即可。而前者因为 $a, b$ 相邻,所以我们可以使用 upper_bound 轻松解决(b = *map.upper_bound(x / 2), a = *(map.upper_bound(x / 2) - 1)) 至此,在保证数据有序的情况下,我们已经第一步缩小了数据范围,得到了一个数组下标范围 [map.upper_bound(x / 2), map.end()]。注意,这里的右区间始终为最大值($INF$) 对于 $b - a$ 的处理 由于求算 $b - a$ 的过程本身需要排序,而上面对 $a + b$ 的处理的时候已经排完序了。所以我们能够较快的得到 $b - a$ 的值( $a, b$ 相邻)但是此时的更新的操作过于复杂,而且我们并不需要知道哪个区间的值能够满足条件(即小于 $x$ ),我们可以只需要知道我们已经缩小后的区间内,是否存在 $a, b$ 满足 $b - a < x$,即 $min(b - a) < x$。 区间最小值,单点更新,此时我们想到了线段树(主要是我不知道有没有动态树状数组这个感觉不太可能存在的东西,于是就写了线段树,实际上需要将线段树的空间动态化,不然空间会爆炸)。 对于整个集合,假如有 $n$ 个不同的元素,则只会产生 $n - 1$ 个不同的插值(由于 $a, b$ 相邻,每个元素只会产生一个,假定最后一个元素不产生) 那么我们建立一棵长度为 $1e9$ 的线段树,对于每个不同的==值(x)==,将其产生的差值保存在节点 $x -x$ 下,其他没有值的节点,则保持 $INF$ 举一个例子,假如我们有如下值在集合中 1, 3, 4, 10, 123, 423 则此时得到的差值为 2, 1, 6, 113, 300 则我们对如下数组a建立线段树 1 2 3 4 5 a[1] = 2; a[3] = 1; a[4] = 6; a[10] = 113; a[123] = 300; 由于输入的 $1 \leq x \leq 1e9$,所以我们开不起这么大的线段树,而实际上最多只会有 $1e5$ 个叶子节点,所以线段树最多的节点个数为 $1e5 \space lg 1e5 < 1e7$,所以只需要准备 $1e7$ 个节点,然后动态开点即可满足整个线段树的需要。 至于这里为什么选择将每个差值产生的较小者(即 $a$ )作为下标的存储位置。由于之后会遇到前面 $a + b$ 得到的区间恰好从 $a, b$ 中间穿过,如果保存的是在 $b$ 下,则会出现 $a + b < x$ 但是仍然被选出来作为 $min(b - a)$。 接下来是线段树的更新操作。 插入 由于值保存在较小值处,所以需要更新较小值的值,和当前新插入的节点的下的值 删除 由于删除操作难以实现,不如直接把被删除的点的值设置为 $INF$,以及,被删掉的点前面一个点的值需要更新 注意一下各种边界情况。 查询的操作 首先从已经排序好的数组中,得到 $x / 2$ 所在数组中的区间,然后拿着这个区间去找线段树,询问区间最小值,将最小值与 $x$ 比较,如果最小值比 $x$ 小,则输出 Yes ,否则输出 No 处理重复的数据 这里就相当简单了,对于相同的数据,只需要保证 $a + a > x$ 即可满足 $a, a, x$ 能够组成三角形。我选择再创建了一个 set 将所有满足个数大于等于 $2$ 的值均保存在数组中,然后去寻找是否存在 set 中是否存在 $a$ 满足 $a > x / 2$,则可以得到解 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 #include <bits/stdc++.h> using namespace std; #define MAXN 8000000 const int maxn = 1e9; struct SegTree { int tot; int sub[MAXN]; // 保存了差值 int lson[MAXN], rson[MAXN]; void init() { for (int i = 0; i < MAXN; ++i) sub[i] = INT_MAX; memset(lson, 0xff, sizeof(int) * MAXN); memset(rson, 0xff, sizeof(int) * MAXN); tot = 1; } inline void up(int cur) { if (lson[cur] == -1 && rson[cur] == -1) sub[cur] = INT_MAX; else if (lson[cur] == -1) sub[cur] = sub[rson[cur]]; else if (rson[cur] == -1) sub[cur] = sub[lson[cur]]; else sub[cur] = min(sub[lson[cur]], sub[rson[cur]]); } inline int getLson(int cur) { assert(tot < MAXN); if (lson[cur] == -1) lson[cur] = tot++; return lson[cur]; } inline int getRson(int cur) { assert(tot < MAXN); if (rson[cur] == -1) rson[cur] = tot++; return rson[cur]; } void update(int x, int value, int cur = 0, int l = 1, int r = maxn) { if (x == l && l == r) { sub[cur] = value; return; } int mid = (l + r) >> 1u; if (x <= mid) { update(x, value, getLson(cur), l, mid); } else { update(x, value, getRson(cur), mid + 1, r); } up(cur); } int query(int x, int y, int cur = 0, int l = 1, int r = maxn) { if (x == l && y == r) return sub[cur]; int mid = (l + r) >> 1u; if (y <= mid) { return query(x, y, getLson(cur), l, mid); } else if (x > mid) { return query(x, y, getRson(cur), mid + 1, r); } else { return min(query(x, mid, getLson(cur), l, mid), query(mid + 1, y, getRson(cur), mid + 1, r)); } } } segTree; void solve() { int q; cin >> q; segTree.init(); map<int, int> pool; // 当前集合中的数据 set<int> multi;// 用于处理重复数据 for (int i = 0; i < q; ++i) { int op, x; cin >> op >> x; switch (op) { case 1: { auto iter = pool.find(x); if (iter != pool.end()) { iter->second++; if (iter->second == 2) multi.insert(x); } else { pool.insert({x, 1}); auto cur = pool.find(x); auto lower = cur, up = cur; up++; if (lower != pool.begin()) { lower--; segTree.update(lower->first, x - lower->first); } if (up != pool.end()) { segTree.update(x, up->first - x); } } } break; case 2: { auto cur = pool.find(x); if (cur == pool.end()) break; cur->second--; if (cur->second == 1) { multi.erase(x); } else if (cur->second == 0) { auto lower = cur, up = cur; up++; segTree.update(x, INT_MAX); if (lower != pool.begin()) { lower--; if (up != pool.end()) segTree.update(lower->first, up->first - lower->first); else segTree.update(lower->first, INT_MAX); } pool.erase(cur); } } break; case 3: { auto iter = pool.upper_bound(x / 2); if (iter == pool.end()) { // 没有值比 x / 2 更大了,则不存在 a + b > x 了 cout << "No" << endl; break; } auto lower = iter; if (lower != pool.begin()) { lower--; if (lower->first + iter->first <= x) { lower++; } } auto mu = multi.lower_bound(iter->first); if (mu != multi.end()) { cout << "Yes" << endl; } else { int res = segTree.query(lower->first, maxn); if (res < x) cout << "Yes" << endl; else cout << "No" << endl; } } break; } } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); int test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/7/15
articleCard.readMore

2020牛客暑期多校训练营(第一场)H-Minimum-cost Flow——网络流

题目链接 大致题意 给出一个费用流图,每条边的流量上限相同且不固定。有$q$个询问,每个询问中给出每条边的流量上限(分数,且保证$\leq 1$)。当图中的流量为 $1$ 个单位的时候,求出此时的费用。 分析 首先是询问个数,有$1e5$次询问,则需要预处理整个图,然后O(1)作答才可以过。然后注意到题目中给出的数据规模,图的边数只有$100$条 首先由于边的流量均为分数($\frac{u}{v}$),而总流量为 $1$ 个单位。我们先扩大$\frac{v}{u}$倍,将每条边的流量固定为 $1$ 个单位,此时流量为 $\frac{v}{u}$ 个单位 考虑在最大流使用 SPFA 查找路径时,当查找到一条路径时,此路径的流量一定为 $1$ (根据上述的设定)。由于采用的本身便是最短路查找路径,得到的路径的费用为当前网络图下的最低费用。 所以可以稍微修改费用流板子中的一部分代码,使得每次得到路径并结算费用时,将每次得到的路径费用记录下来并保存。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #define ll long long const int maxn = 100; //点数 const int INF = 0x3f3f3f3f; struct Edge { int from, to, cap, flow, cost; Edge(int u, int v, int c, int f, int cc) : from(u), to(v), cap(c), flow(f), cost(cc) {} }; vector<ll> res; struct MCMF { int n, m; vector<Edge> edges; vector<int> G[maxn]; int inq[maxn]; //是否在队列中 int d[maxn]; //bellmanford int p[maxn]; //上一条弧 int a[maxn]; //可改进量 void init(int n) { this->n = n; for (int i = 0; i <= n; ++i) G[i].clear(); edges.clear(); } void addEdge(int from, int to, int cap, int cost) { edges.emplace_back(Edge(from, to, cap, 0, cost)); edges.emplace_back(Edge(to, from, 0, 0, -cost)); m = int(edges.size()); G[from].emplace_back(m - 2); G[to].emplace_back(m - 1); } bool spfa(int s, int t, int &flow, ll &cost) { for (int i = 1; i <= n; ++i) d[i] = INF; memset(inq, 0, sizeof(inq)); d[s] = 0; inq[s] = 1; p[s] = 0; queue<int> q; a[s] = INF; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = 0; for (int i = 0; i < int(G[u].size()); ++i) { Edge &e = edges[G[u][i]]; if (e.cap > e.flow && d[e.to] > d[u] + e.cost) { d[e.to] = d[u] + e.cost; p[e.to] = G[u][i]; a[e.to] = min(a[u], e.cap - e.flow); if (!inq[e.to]) { q.push(e.to); inq[e.to] = 1; } } } } if (d[t] == INF) return false; flow += a[t]; cost += (ll) d[t] * (ll) a[t]; res.push_back(d[t]); for (int u = t; u != s; u = edges[p[u]].from) { edges[p[u]].flow += a[t]; edges[p[u] ^ 1].flow -= a[t]; } return true; } int MincostMaxflow(int s, int t, ll &cost) { int flow = 0; cost = 0; while (spfa(s, t, flow, cost)); return flow; } } mcmf; 通过 res 数组记录下所有得到的路径的费用 而后分解流量。对于每一次询问,从 res 数组中从开始取值每次取出一条路径来提供 $1$ 个单位的流量,直到满足流量要求,则停止取值并输出答案。 虽然 $1 \leq u, v \leq 1e9$,可能需要循环遍历 $1e9$ 次才能出结果,但是由于图中每条边的容量都是 $1$,所以对于每一条路径,它只有被完全占用和完全空闲两种状态,而边数只有 $100$ 条,即整个网络的最大流量只能是 $100$ 个单位,即最多遍历 $100$ 次,则复杂度不超过 $100 1e5 = 1e7$,当 $MAX_FLOW u < v$ 时,则无解,输出 $-1$ 注意一下乘法运算只需要考虑分子部分,分母部分不需要参与运算,在最后需要分子分母都除以 $gcd$ 以保证最简 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 #include <bits/stdc++.h> using namespace std; #define ll long long const int maxn = 100; //点数 const int INF = 0x3f3f3f3f; struct Edge { int from, to, cap, flow, cost; Edge(int u, int v, int c, int f, int cc) : from(u), to(v), cap(c), flow(f), cost(cc) {} }; vector<long long> res; struct MCMF { int n, m; vector<Edge> edges; vector<int> G[maxn]; int inq[maxn]; //是否在队列中 int d[maxn]; //bellmanford int p[maxn]; //上一条弧 int a[maxn]; //可改进量 void init(int n) { this->n = n; for (int i = 0; i <= n; ++i) G[i].clear(); edges.clear(); } void addEdge(int from, int to, int cap, int cost) { edges.emplace_back(Edge(from, to, cap, 0, cost)); edges.emplace_back(Edge(to, from, 0, 0, -cost)); m = int(edges.size()); G[from].emplace_back(m - 2); G[to].emplace_back(m - 1); } bool spfa(int s, int t, int &flow, ll &cost) { for (int i = 1; i <= n; ++i) d[i] = INF; memset(inq, 0, sizeof(inq)); d[s] = 0; inq[s] = 1; p[s] = 0; queue<int> q; a[s] = INF; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = 0; for (int i = 0; i < int(G[u].size()); ++i) { Edge &e = edges[G[u][i]]; if (e.cap > e.flow && d[e.to] > d[u] + e.cost) { d[e.to] = d[u] + e.cost; p[e.to] = G[u][i]; a[e.to] = min(a[u], e.cap - e.flow); if (!inq[e.to]) { q.push(e.to); inq[e.to] = 1; } } } } if (d[t] == INF) return false; flow += a[t]; cost += (ll) d[t] * (ll) a[t]; res.push_back(d[t]); for (int u = t; u != s; u = edges[p[u]].from) { edges[p[u]].flow += a[t]; edges[p[u] ^ 1].flow -= a[t]; } return true; } int MincostMaxflow(int s, int t, ll &cost) { int flow = 0; cost = 0; while (spfa(s, t, flow, cost)); return flow; } } mcmf; int n, m; void solve() { ios::sync_with_stdio(false); cin.tie(0); cout.precision(10); cout << fixed; while (cin >> n >> m) { mcmf.init(n + 10); res.clear(); for (int i = 0; i < m; ++i) { int u, v, f; cin >> u >> v >> f; mcmf.addEdge(u, v, 1, f); } ll cost = 0; ll mf = mcmf.MincostMaxflow(1, n, cost); int q; cin >> q; while (q--) { ll u, v; cin >> u >> v; if (mf * u < v) { cout << "NaN" << '\n'; continue; } ll sum = 0; ll ans = 0; for (auto item : res) { if (sum + u <= v) { sum += u; ans += item * u; } else { ans += (v - sum) * item; break; } } ll g = __gcd(ans, v); cout << ans / g << '/' << v / g << '\n'; } } } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); int test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/7/14
articleCard.readMore

Educational Codeforces Round 80 D. Minimax Problem——二分+二进制处理

题目链接 题目大意 有n个维度为m的向量,取其中两个进行合并,合并时每个维度取两者之间的较大者,得到的新的向量中,维度值最小者最大为多少 分析 首先最需要注意的是m的取值,m最大只有8,那么我们可以二分答案,对于每一个二分值,进行下面的操作,将整个矩阵的每一个元素,如果这个元素大于二分值,则变成1,反正则变成0,把每一个向量压缩为单个二进制数,这样我们最多只会得到$2^8 = 256$种不同的二进制数,然后暴力的遍历所有可能的二进制数的组合,得到是否满足当前二分值 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <bits/stdc++.h> using namespace std; const int NUM = 3e5 + 100; int data[NUM][10]; bool check(int value, int n, int m, pair<int, int> &ans) { map<unsigned, int> s; for (int i = 0; i < n; ++i) { unsigned temp = 0; for (int j = 0; j < m; ++j) { temp <<= 1u; temp |= data[i][j] > value; } s.insert({temp, i}); } unsigned tar = -1u >> (sizeof(int) * 8 - m); for (auto iter1 = s.begin(); iter1 != s.end(); ++iter1) { for (auto iter2 = iter1; iter2 != s.end(); ++iter2) { if ((iter1->first | iter2->first) == tar) { ans.first = iter1->second; ans.second = iter2->second; return true; } } } return false; } void solve() { int n, m; cin >> n >> m; int l = INT_MAX, r = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { cin >> data[i][j]; l = min(l, data[i][j]); r = max(r, data[i][j]); } } int mid, cnt = r - l; pair<int, int> ans; while (cnt > 0) { int step = cnt / 2; mid = l + step; if (check(mid, n, m, ans)) { l = mid + 1; cnt -= step + 1; } else cnt /= 2; } cout << ans.first + 1 << " " << ans.second + 1 << endl; } signed main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/2/4
articleCard.readMore

【bzoj2049】[Sdoi2008]Cave 洞穴勘测——线段树上bfs求可撤销并查集

题面 2049: [Sdoi2008]Cave 洞穴勘测 Time Limit: 10 Sec Memory Limit: 259 MB Submit: 12030 Solved: 6024 Description 辉辉热衷于洞穴勘测。某天,他按照地图来到了一片被标记为JSZX的洞穴群地区。经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好两个洞穴。假如两个洞穴可以通过一条或者多条通道按一定顺序连接起来,那么这两个洞穴就是连通的,按顺序连接在一起的这些通道则被称之为这两个洞穴之间的一条路径。洞穴都十分坚固无法破坏,然而通道不太稳定,时常因为外界影响而发生改变,比如,根据有关仪器的监测结果,123号洞穴和127号洞穴之间有时会出现一条通道,有时这条通道又会因为某种稀奇古怪的原因被毁。辉辉有一台监测仪器可以实时将通道的每一次改变状况在辉辉手边的终端机上显示:如果监测到洞穴u和洞穴v之间出现了一条通道,终端机上会显示一条指令 Connect u v 如果监测到洞穴u和洞穴v之间的通道被毁,终端机上会显示一条指令 Destroy u v 经过长期的艰苦卓绝的手工推算,辉辉发现一个奇怪的现象:无论通道怎么改变,任意时刻任意两个洞穴之间至多只有一条路径。因而,辉辉坚信这是由于某种本质规律的支配导致的。因而,辉辉更加夜以继日地坚守在终端机之前,试图通过通道的改变情况来研究这条本质规律。然而,终于有一天,辉辉在堆积成山的演算纸中崩溃了……他把终端机往地面一砸(终端机也足够坚固无法破坏),转而求助于你,说道:“你老兄把这程序写写吧”。辉辉希望能随时通过终端机发出指令 Query u v,向监测仪询问此时洞穴u和洞穴v是否连通。现在你要为他编写程序回答每一次询问。已知在第一条指令显示之前,JSZX洞穴群中没有任何通道存在。 Input 第一行为两个正整数n和m,分别表示洞穴的个数和终端机上出现过的指令的个数。以下m行,依次表示终端机上出现的各条指令。每行开头是一个表示指令种类的字符串s(”Connect”、”Destroy”或者”Query”,区分大小写),之后有两个整数u和v (1≤u, v≤n且u≠v) 分别表示两个洞穴的编号。 Output 对每个Query指令,输出洞穴u和洞穴v是否互相连通:是输出”Yes”,否则输出”No”。(不含双引号) Sample Input 样例输入1 cave.in 1 2 3 4 5 6 2005 Query123127 Connect123127 Query123127 Destroy127123 Query123127 样例输入2 cave.in 1 2 3 4 5 6 3 5 Connect12 Connect31 Query23 Destroy13 Query23 Sample Output 样例输出1 cave.out 1 2 3 No Yes No 样例输出2 cave.out 1 2 Yes No HINT 数据说明 10%的数据满足n≤1000, m≤20000 20%的数据满足n≤2000, m≤40000 30%的数据满足n≤3000, m≤60000 40%的数据满足n≤4000, m≤80000 50%的数据满足n≤5000, m≤100000 60%的数据满足n≤6000, m≤120000 70%的数据满足n≤7000, m≤140000 80%的数据满足n≤8000, m≤160000 90%的数据满足n≤9000, m≤180000 100%的数据满足n≤10000, m≤200000 保证所有Destroy指令将摧毁的是一条存在的通道本题输入、输出规模比较大,建议c\c++选手使用scanf和printf进行I\O操作以免超时 分析 一条边的存在的时间其实就是一段连续的时间(我们这里指定每一行命令就是一个单位的时间),这样我们就可以把问题离线化。 那么我们可以用线段树来保存整个状态,这并不是什么难事,我们在线段树的每一个节点上保存一个边的集合,表示这个节点下所有的子节点都包含了这条边。 那么我们对于任意一个叶子节点,从根节点到叶子节点的全过程遍历到的所有的边组成的集合就是当前的图 由于如果每次询问都是从根节点出发的话效率太低,我们采用直接在线段树上移动的方式来解决效率问题。 通过可撤销并查集的性质,来实现在线段树上移动 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 #include <bits/stdc++.h> using namespace std; const int MAXN = 10100; const int MAXM = 200100; typedef pair<int, int> pii; struct UFS { int f[MAXN]; stack<pii> s; int finds(int x) { while (x ^ f[x]) x = f[x]; return x; } void unite(int x, int y) { x = finds(x); y = finds(y); if (x != y) { s.push({x, f[x]}); f[x] = y; } } void init(int b, int e) { // 初始化函数,范围为 [b, e) for (int i = b; i < e; i++) f[i] = i; } void undo() { f[s.top().first] = s.top().second; s.pop(); } }; struct SegTree { vector<pii> data[MAXM << 2]; static inline int lson(int k) { return k << 1; } static inline int rson(int k) { return (k << 1) | 1; } static inline int fat(int l, int r) { return (l + r) >> 1; } // add 函数对应于正常的线段树的 insert,但是稍微有些不同 void add(int k, int l, int r, int x, int y, const pii &value) { if (l == x && r == y) { data[k].push_back(value); return; } int mid = fat(l, r); if (y <= mid) { add(lson(k), l, mid, x, y, value); } else if (x > mid) { add(rson(k), mid + 1, r, x, y, value); } else { add(lson(k), l, mid, x, mid, value); add(rson(k), mid + 1, r, mid + 1, y, value); } } }; UFS ufs; SegTree segTree; vector<pair<pii, int> > que; int tar; // 通过 dfs 的方式在线段树上移动 bool dfs(int k, int l, int r) { // 当完成一次询问之后,需要跳出当前的叶子,即回溯。通过 goto 来使得回溯的过程会自动进入正确的叶子节点 rejudge: int target = que[tar].second; if (target == r && l == r) { if (ufs.finds(que[tar].first.first) == ufs.finds(que[tar].first.second)) cout << "Yes" << endl; else cout << "No" << endl; tar++; return tar == que.size();// true 表示所有的询问已经结束,退出 dfs } int mid = SegTree::fat(l, r); if (target <= mid) { // for (auto &item: segTree.data[SegTree::lson(k)]) // ufs.unite(item.first, item.second); for (int i = 0; i < segTree.data[SegTree::lson(k)].size(); ++i) ufs.unite(segTree.data[SegTree::lson(k)][i].first, segTree.data[SegTree::lson(k)][i].second); if (dfs(SegTree::lson(k), l, mid)) return true; // for (auto &item: segTree.data[SegTree::lson(k)]) for (int i = 0; i < segTree.data[SegTree::lson(k)].size(); ++i) ufs.undo(); if (que[tar].second > r) return false; goto rejudge; } else { // for (auto &item: segTree.data[SegTree::rson(k)]) // ufs.unite(item.first, item.second); for (int i = 0; i < segTree.data[SegTree::rson(k)].size(); ++i) ufs.unite(segTree.data[SegTree::rson(k)][i].first, segTree.data[SegTree::rson(k)][i].second); if (dfs(SegTree::rson(k), mid + 1, r)) return true; // for (auto &item: segTree.data[SegTree::rson(k)]) for (int i = 0; i < segTree.data[SegTree::rson(k)].size(); ++i) ufs.undo(); if (que[tar].second > r) return false; goto rejudge; } } void solve() { int n, m; cin >> n >> m; map<pii, int> mp; for (int i = 0; i < m; ++i) { string s; int u, v; cin >> s >> u >> v; if (u > v) swap(u, v); switch (s[0]) { case 'Q': // que.push_back({{u, v}, i + 1}); que.push_back(make_pair(make_pair(u, v), i + 1)); break; case 'C': // mp.insert({{u, v}, i + 1}); mp.insert(make_pair(make_pair(u, v), i + 1)); break; case 'D': { // auto iter = mp.find({u, v}); map<pii, int>::iterator iter = mp.find(make_pair(u, v)); // segTree.add(1, 1, m, iter->second, i, {u, v}); segTree.add(1, 1, m, iter->second, i, make_pair(u, v)); mp.erase(iter); break; } } } map<pii, int>::iterator iter = mp.begin(); while (iter != mp.end()) { segTree.add(1, 1, m, iter->second, m, iter->first); iter++; } ufs.init(0, n + 1); tar = 0; // for (auto &item: segTree.data[1]) for (int i = 0; i < segTree.data[1].size(); ++i) ufs.unite(segTree.data[1][i].first, segTree.data[1][i].second); // ufs.unite(item.first, item.second); dfs(1, 1, m); return; } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); // cin.tie(nullptr); // cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { if (acm_local_for_debug == '$') exit(0); cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/1/8
articleCard.readMore

Codeforces Round 606 E. Two Fairs——图论

题目链接 题意 给你一张无向图,求出有多少对点对(x, y)满足从点x到点y的所有路径必同时经过点a和点b 分析 单点 首先考虑假如点a和点b是同一个点的情况 我从任意的一点出发,把所有与点a/b相连的路视为不存在,通过bfs遍历所有可能到达的点。那么这些点之间可以满足不经过点a/b能联通。反之,如果能将其他所有的点均进行bfs,组成类似并查集的数据结构,那么我可以很快得到,所有非同一集合内的点之间必须通过点a/b。 下一个问题:如何保证所有点都完成了遍历(bfs) 我们可以不断的在vis数组中找没有被vis的点,然后不断的bfs,但是这样效率很低 换一种思路 我们可以直接从点a/b出发,设定bfs起点为点a/b,那么就可以一次性的完成整个图的bfs遍历,并且使用类似并查集的结构将他们保存下来。 两点 我们可以这样定义,如果存在点对(x,y),假设与点b联通的路均视为不连通,满足x与a联通,但是不与y联通,同时,假设与点a联通的路均视为不连通,满足y与b联通,但是不与x联通,那么我们可以得到这样点x的集合和点y的集合,那么这两个集合内各取一点即为一组合理的点对 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <bits/stdc++.h> using namespace std; const int MAXN = 201000; const int MAXM = 1001000; // 无权有向图 struct Graph { struct Edge { int to, next; } edge[MAXM]; int head[MAXN]; int tot; void init(int n) { tot = 0; memset(head, -1, sizeof(int) * (n + 1)); } void add_edge(int from, int to) { edge[tot].to = to; edge[tot].next = head[from]; head[from] = tot++; } } graph; void solve() { int T; cin >> T; for (int ts = 0; ts < T; ++ts) { int n, m, a, b; cin >> n >> m >> a >> b; graph.init(n); for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; graph.add_edge(u, v); graph.add_edge(v, u); } bool vis[MAXN]; queue<int> q; int a_cnt = n - 2, b_cnt = n - 2; memset(vis, false, sizeof(bool) * (n + 1)); q.push(a); vis[a] = true; while (!q.empty()) { int cur = q.front(); q.pop(); for (int i = graph.head[cur]; i != -1; i = graph.edge[i].next) { if (!vis[graph.edge[i].to] && graph.edge[i].to != b) { vis[graph.edge[i].to] = true; q.push(graph.edge[i].to); a_cnt--; } } } memset(vis, false, sizeof(bool) * (n + 1)); q.push(b); vis[b] = true; while (!q.empty()) { int cur = q.front(); q.pop(); for (int i = graph.head[cur]; i != -1; i = graph.edge[i].next) { if (!vis[graph.edge[i].to] && graph.edge[i].to != a) { vis[graph.edge[i].to] = true; q.push(graph.edge[i].to); b_cnt--; } } } cout << 1ll * a_cnt * b_cnt << endl; } } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/1/8
articleCard.readMore

Codeforces Round 612 (Div. 2) C. Garland——DP

题目链接 贪心模拟了半天,最后放弃了 题意 给你一串从$1-n$的序列,其中部分未知(表示为0),补全序列使得相邻数值奇偶性相反的数量最少 相邻数值的奇偶性相反:两个相邻的两个数值,其中一个为奇数另外一个为偶数 分析 一开始用了贪心,结果卡在第十二个样例,然后改成dp 定义dp数组如下 1 2 int dp[120][60][2]; // dp[i][j][0/1] 表示第i+1个位置放了偶/奇数,且到第i+1处总共放了j个奇数,有多少个奇偶性相反 得到状态转移方程 1 2 dp[i][j][1] = min(dp[i - 1][j - 1][0] + 1, dp[i - 1][j - 1][1]); dp[i][j][0] = min(dp[i - 1][j][1] + 1, dp[i - 1][j][0]); 当然这得看这个位置本身是不是已经有了数值,如果为0则两个都需要,如果已经有数值了就按照原来的数值进行dp AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <bits/stdc++.h> using namespace std; void solve() { int n; int dp[120][60][2], value[120]; cin >> n; for (int i = 0; i < n; ++i) { cin >> value[i]; } memset(dp, 0x3f, sizeof(dp)); if (value[0] == 0) dp[0][1][1] = dp[0][0][0] = 0; else dp[0][value[0] & 1][value[0] & 1] = 0; for (int i = 1; i < n; ++i) { for (int j = 0; j <= min(i + 1, (n + 1) / 2); ++j) { if ((value[i] & 1 || value[i] == 0) && j > 0) dp[i][j][1] = min(dp[i - 1][j - 1][0] + 1, dp[i - 1][j - 1][1]); if (!(value[i] & 1)) dp[i][j][0] = min(dp[i - 1][j][1] + 1, dp[i - 1][j][0]); } } cout << min(dp[n - 1][(n + 1) / 2][1], dp[n - 1][(n + 1) / 2][0]) << endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/1/6
articleCard.readMore

【洛谷】P2444 [POI2000]病毒——AC自动机

题目链接 题目描述 二进制病毒审查委员会最近发现了如下的规律:某些确定的二进制串是病毒的代码。如果某段代码中不存在任何一段病毒代码,那么我们就称这段代码是安全的。现在委员会已经找出了所有的病毒代码段,试问,是否存在一个无限长的安全的二进制代码。 示例: 例如如果{011, 11, 00000}为病毒代码段,那么一个可能的无限长安全代码就是010101…。如果{01, 11, 000000}为病毒代码段,那么就不存在一个无限长的安全代码。 任务: 请写一个程序: 1.在文本文件WIR.IN中读入病毒代码; 2.判断是否存在一个无限长的安全代码; 3.将结果输出到文件WIR.OUT中。 输入格式 在文本文件WIR.IN的第一行包括一个整数n(n\le 2000)(n≤2000),表示病毒代码段的数目。以下的n行每一行都包括一个非空的01字符串——就是一个病毒代码段。所有病毒代码段的总长度不超过30000。 输出格式 在文本文件WIR.OUT的第一行输出一个单词: TAK——假如存在这样的代码; NIE——如果不存在。 输入输出样例 输入 3 01 11 00000 输出 NIE 分析 想办法让那串无限长的字符串不断的在树上失配,然后不断的走fail指针最后进入一个循环即可 即在树上dfs,保证不经过任何字符串尾节点使得找到一个树上环 AC code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 #include <bits/stdc++.h> using namespace std; /* * AC 自动机:多个模式串去匹配一个串,求有多少个模式串与主串有匹配内容 * * 使用操作: * 1、把每一个模式串插入到树中 insert * 2、build * 3、使用 query 询问有多少个模式串匹配 */ const int CHAR_NUM = 2;//仅小写 const int MAXN = 2100;//模式串个数 const int MAXM = 30100;//模式串最长长度 const int NUM = MAXN * MAXM;//空间=个数*长度,稳 struct Trie { int c[NUM][CHAR_NUM], val[NUM], fail[NUM], cnt; void insert(char *s) { int len = strlen(s); int now = 0; for (int i = 0; i < len; i++) { // int v = s[i] - 'a'; int v = s[i] - '0'; if (!c[now][v])c[now][v] = ++cnt; now = c[now][v]; } val[now]++; } void build() { queue<int> q; for (int i = 0; i < CHAR_NUM; i++)if (c[0][i])fail[c[0][i]] = 0, q.push(c[0][i]); while (!q.empty()) { int u = q.front(); q.pop(); for (int i = 0; i < CHAR_NUM; i++) if (c[u][i])fail[c[u][i]] = c[fail[u]][i], q.push(c[u][i]); else c[u][i] = c[fail[u]][i]; } } int query(char *s) { int len = strlen(s); int now = 0, ans = 0; for (int i = 0; i < len; i++) { // now = c[now][s[i] - 'a']; now = c[now][s[i] - '0']; for (int t = now; t && ~val[t]; t = fail[t])ans += val[t], val[t] = -1; } return ans; } } AC; char s[30100]; bool vis[NUM]; bool dfs(int cur) { for (int i = 0; i < CHAR_NUM; ++i) { int x = AC.c[cur][i]; if (!AC.val[x]) { bool flag = false; int t = x; while (t) { if (AC.val[t]) { flag = true; break; } t = AC.fail[t]; } if (flag) continue; if (vis[x]) return true; vis[x] = true; if (dfs(x)) { return true; } vis[x] = false; } } return false; } void solve() { int n; cin >> n; for (int i = 0; i < n; ++i) { cin >> s; AC.insert(s); } AC.build(); vis[0] = true; bool flag = dfs(0); if (flag) { cout << "TAK" << endl; } else { cout << "NIE" << endl; } } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2020/1/5
articleCard.readMore

【HDU5934】Bomb——有向图强连通分量+重建图

题目大意 二维平面上有 n 个爆炸桶,$i-th$爆炸桶位置为 $(x_i, y_i)$ 爆炸范围为 $r_i$ ,且需要 $c_i$ 的价格引爆,求把所有桶引爆所需的钱。 分析 通过求有向图的强连通分量,求出所有爆炸块(满足引爆一个块内的任意一个爆炸桶就可以摧毁这个块内的爆炸桶),然后把所有爆炸块视为一个爆炸桶,价值为爆炸块内的价值最小值,然后重建有向图,将新建的有向图所有入度为 0 的点的价值相加,就是答案。 AC-Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 #include <bits/stdc++.h> using namespace std; const int MAXN = 1100; // 点数 const int MAXM = 1000100; // 边数 struct Edge { int to, next; } edge[MAXM]; // 只有这里写的是 MAXM int head[MAXN], tot; int Low[MAXN], DFN[MAXN], Stack[MAXN], Belong[MAXN]; //Belong 数组的值是 1 ~ scc int Index, top; int scc; // 强连通分量的个数 bool Instack[MAXN]; int num[MAXN]; // 各个强连通分量包含点的个数,数组编号 1 ~ scc // num 数组不一定需要,结合实际情况 void addedge(int u, int v) { edge[tot].to = v; edge[tot].next = head[u]; head[u] = tot++; } void Tarjan(int u) { int v; Low[u] = DFN[u] = ++Index; Stack[top++] = u; Instack[u] = true; for (int i = head[u]; i != -1; i = edge[i].next) { v = edge[i].to; if (!DFN[v]) { Tarjan(v); if (Low[u] > Low[v]) Low[u] = Low[v]; } else if (Instack[v] && Low[u] > DFN[v]) Low[u] = DFN[v]; } if (Low[u] == DFN[u]) { scc++; do { v = Stack[--top]; Instack[v] = false; Belong[v] = scc; num[scc]++; } while (v != u); } } void solve(int N) { memset(DFN, 0, sizeof(DFN)); memset(Instack, false, sizeof(Instack)); memset(num, 0, sizeof(num)); Index = scc = top = 0; for (int i = 1; i <= N; i++) if (!DFN[i]) Tarjan(i); } void init() { tot = 0; memset(head, -1, sizeof(head)); } struct node { int x, y, r, c; bool in_boom(const node &other) const { return hypot(abs(x - other.x), abs(y - other.y)) <= r; } }; node nodeList[1100]; int n; void init_graph1() { init(); for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { if (i == j) continue; if (nodeList[i].in_boom(nodeList[j])) addedge(i, j); } } } struct Graph { struct Node { int deg; int value; }; Node node[MAXN]; void init() { for (int i = 0; i < n + 5; ++i) { node[i].deg = 0; node[i].value = INT_MAX; } } void add_edge(int from, int to) { if (from != to) node[to].deg++; } }; Graph graph; int ans; void tp_init() { graph.init(); for (int i = 1; i <= n; ++i) { graph.node[Belong[i]].value = min(graph.node[Belong[i]].value, nodeList[i].c); for (int j = 1; j <= n; ++j) { if (i == j) continue; if (nodeList[i].in_boom(nodeList[j])) graph.add_edge(Belong[i], Belong[j]); } } } void tp() { ans = 0; tp_init(); for (int i = 1; i <= scc; ++i) { if (graph.node[i].deg == 0) { ans += graph.node[i].value; } } } void solve() { int t; cin >> t; for (int ts = 0; ts < t; ++ts) { cin >> n; for (int i = 1; i <= n; ++i) { cin >> nodeList[i].x >> nodeList[i].y >> nodeList[i].r >> nodeList[i].c; } init_graph1(); solve(n); tp(); cout << "Case #" << ts + 1 << ": " << ans << endl; } } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2019/10/13
articleCard.readMore

Codeforces Round#589 (Div. 2) D、Complete Tripartite

题目链接 大致题意 把一个图分成三块,要求任意两块之间是完全图,块内部没有连线 分析 首先根据块内没有连线可以直接分成两块 假定点1是属于块1的,那么所有与点1连接的点,都不属于块1;反之则是块1的 然后在所有不属于块1的点内随意找一点k,设定其属于块2,那么所有与点k连接的点且不属于块1,则是块3。 块分完了,然后是判断每个块是否满足条件,我通过下面三条来判断 1、每个块都有点 2、每个块内部没有连线,即没有一条线的两个端点在同一个块内 3、每个块内的点的度等于其他两个块的点个数和也等于n减去当前块内的点数 AC Code (暴力就完事) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <bits/stdc++.h> using namespace std; #define MAXN 101000 int fa[MAXN];// 保存了点属于哪个块 int deg[MAXN];// 保存了点的度 pair<int, int> edge[MAXN * 3]; void solve() { int n, m; cin >> n >> m; int f2 = 2; // f2 用来找块2 for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; deg[u]++; deg[v]++; edge[i] = {u, v}; if (u == 1) { fa[v] = 1; f2 = v; } else if (v == 1) { fa[u] = 1; f2 = u; } } // 找出第三块 for (int i = 0; i < m; ++i) { if (edge[i].first == f2 && fa[edge[i].second] == 1) fa[edge[i].second] = 2; else if (edge[i].second == f2 && fa[edge[i].first] == 1) fa[edge[i].first] = 2; } int cnt[3] = {n, n, n};// 保存了每个块内点的个数 // 需要变成完全图需要多少条边 for (int i = 0; i < n; ++i) cnt[fa[i + 1]]--; // 块内的入度是否符合条件 for (int i = 0; i < n; ++i) { if (deg[i + 1] != cnt[fa[i + 1]]) { cout << -1 << endl; return; } } // 每个块是否为空 if (cnt[0] == n || cnt[1] == n || cnt[2] == n) { cout << -1 << endl; return; } // 内部连线 for (int i = 0; i < m; ++i) { if (fa[edge[i].first] == fa[edge[i].second]) { cout << -1 << endl; return; } } for (int i = 0; i < n - 1; ++i) cout << fa[i + 1] + 1 << " "; cout << fa[n] + 1 << endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; } 总之一句话,暴力就完事了。反正边不多,我已经懒得优化了

2019/9/30
articleCard.readMore

【2019沈阳网络赛】G、Special necklace——自闭的物理题

这道题让我差点怀疑自己高考没考过物理 题意中 he measures the resistance of any two endpoints of it, the resistance values are all $2a$ 指的是在三角形中电阻为 $2a$ 而不是边上的电阻为 $2a$ 实际上每条边的电阻R为 $\frac{1}{R} + \frac{1}{2R} = 2a$ 可以求得$R = 3a$ 所以可以得到递推公式 $a{n+1} = \frac{1}{ \frac{1}{ \frac{1}{ \frac{1}{a{n}} + \frac{4}{3}} + 3} + \frac{1}{3}}$ 通过python打表 1 2 3 4 5 res = 5 / 3 print('%.20f' % res) for i in range(20): res = 1 / ((1 / (1 / (1 / res + 4 / 3) + 3)) + 1 / 3) print('%.20f' % res) 得到 1.66666666666666674068 1.61904761904761906877 1.61805555555555535818 1.61803444782168193150 1.61803399852180329610 1.61803398895790206957 1.61803398875432269399 1.61803398874998927148 1.61803398874989712297 1.61803398874989490253 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 1.61803398874989468048 这是 $a = 1$ 的情况,最后乘上 a 就行 很明显了,直接打表就行,借助一下字符串流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <bits/stdc++.h> using namespace std; vector<double> res; void init() { res.push_back(1.66666666666666674068); res.push_back(1.61904761904761906877); res.push_back(1.61805555555555535818); res.push_back(1.61803444782168193150); res.push_back(1.61803399852180329610); res.push_back(1.61803398895790206957); res.push_back(1.61803398875432269399); res.push_back(1.61803398874998927148); res.push_back(1.61803398874989712297); res.push_back(1.61803398874989468048); res.push_back(1.61803398874989468048); res.push_back(1.61803398874989468048); res.push_back(1.61803398874989468048); res.push_back(1.61803398874989468048); res.push_back(1.61803398874989468048); } void solve() { int t; cin >> t; init(); while (t--) { string str; double a; cin >> str >> a; if (str.length() > 2) { cout << fixed << setprecision(10) << res.back() * a << endl; continue; } stringstream ss(str); int n; ss >> n; if (n > res.size() - 1) { cout << fixed << setprecision(10) << res.back() * a << endl; } else { cout << fixed << setprecision(10) << res[n - 1] * a << endl; } } } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 20) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); solve(); auto end_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << " successful" << endl; cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2019/9/14
articleCard.readMore

【2019南昌网络赛】B-Fire-Fighting Hero

题目链接 分析 英雄方面很简单,跑一遍 Dijkstra 就行了,但是灭火团队就有点麻烦了。 这里可以借助一下最大流的建边来解决这个问题: 我们可以另外找一个点作为起点,然后建立从那个点到每一个团队的起点的边,权值为0,这样就完成了多起点的最短路 恰好我的板子是封装好的 Dijkstra ,我就直接建立两个结构体解决问题,因为点的数量只有 1000 个,空间上已经没有什么顾虑了 AC-Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 #include <bits/stdc++.h> using namespace std; #define MAXN 1100 #define MAXM 1000000 #define INF 0x3fffffff //防止后面溢出,这个不能太大 struct Graph { struct Edge { long long to, next; long long cost; } edge[MAXM]; long long head[MAXN]; long long tot; void init(long long n) { tot = 0; memset(head, -1, sizeof(long long) * (n + 1)); } void add_edge(long long from, long long to, long long value) { edge[tot].to = to; edge[tot].cost = value; edge[tot].next = head[from]; head[from] = tot++; } }; struct Dijkstra { long long low_cost[MAXN]; bool vis[MAXN]; long long pre[MAXN]; void solve(long long b, long long e, long long start, Graph &graph) { for (long long i = b; i < e; i++) { low_cost[i] = INF; vis[i] = false; pre[i] = -1; } low_cost[start] = 0; vis[start] = true; long long cur_edge = graph.head[start]; while (cur_edge != -1) { if (!vis[graph.edge[cur_edge].to] && low_cost[start] + graph.edge[cur_edge].cost < low_cost[graph.edge[cur_edge].to]) { low_cost[graph.edge[cur_edge].to] = low_cost[start] + graph.edge[cur_edge].cost; pre[graph.edge[cur_edge].to] = start; } cur_edge = graph.edge[cur_edge].next; } for (long long j = b; j < e - 1; j++) { long long k = -1; long long Min = INF; for (long long i = b; i < e; i++) { if (!vis[i] && low_cost[i] < Min) { Min = low_cost[i]; k = i; } } if (k == -1) break; vis[k] = true; cur_edge = graph.head[k]; while (cur_edge != -1) { if (!vis[graph.edge[cur_edge].to] && low_cost[k] + graph.edge[cur_edge].cost < low_cost[graph.edge[cur_edge].to]) { low_cost[graph.edge[cur_edge].to] = low_cost[k] + graph.edge[cur_edge].cost; pre[graph.edge[cur_edge].to] = k; } cur_edge = graph.edge[cur_edge].next; } } } }; Graph graph; Dijkstra dijkstra1, dijkstra2; int k_node[MAXN]; void solve() { long long t; cin >> t; long long v, e, s, k, c; for (int ts = 0; ts < t; ++ts) { cin >> v >> e >> s >> k >> c; graph.init(v + 1); for (int i = 0; i < k; ++i) { cin >> k_node[i]; } long long from, to, value; for (long long i = 0; i < e; ++i) { cin >> from >> to >> value; graph.add_edge(from, to, value); graph.add_edge(to, from, value); } dijkstra1.solve(1, v + 1, s, graph);//第一次跑dijkstra for (int i = 0; i < k; ++i) { graph.add_edge(0, k_node[i], 0); // 这里设定超级源点为0,建立从0到每一个团队起点的边,权值为0 } dijkstra2.solve(0, v + 1, 0, graph);//第二次跑dijkstra long long s_min_max = 0; for (long long i = 1; i < v + 1; ++i) s_min_max = max(s_min_max, dijkstra1.low_cost[i]); long long k_min_max = 0; for (long long i = 1; i < v + 1; ++i) k_min_max = max(k_min_max, dijkstra2.low_cost[i]); if (s_min_max <= c * k_min_max)//考虑到精度问题,这里用乘法代替 cout << s_min_max << endl; else cout << k_min_max << endl; } } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); long long test_index_for_debug = 1; char acm_local_for_debug; while (cin >> acm_local_for_debug) { cin.putback(acm_local_for_debug); if (test_index_for_debug > 100) { throw runtime_error("Check the stdin!!!"); } auto start_clock_for_debug = clock(); cout << "Test " << test_index_for_debug << ":" << endl; solve(); auto end_clock_for_debug = clock(); cerr << "Test " << test_index_for_debug++ << " Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; cout << "\n--------------------------------------------------" << endl; } #else solve(); #endif return 0; }

2019/9/9
articleCard.readMore

【2019HDU多校】第九场1006/HDU6685-Rikka with Coin——位运算打表

题目链接 题目大意 使用10、20、50、100元面额的硬币能分别组成题目给出的面额,需要最少的硬币个数 分析 一开始队友想用一堆if-else解决问题,然后WA了无数发…… 我想到了一种比较简单的打表法来解决这个问题,而这个表长度只有==13个int== ==在开始分析之前,我们先不考虑出现 -1 的解。即出现某种情况 mod 10不等于0,因为这个判断非常简单== 定律 开始推这个表之前先确定一个显而易见的定律 若存在两种方案的需要的硬币数一样,且第一种的方案能组成的面额第二种都可以组成,则第一种方案不可取。 证明:如果我使用了第一组方案,则我必定可以使用第二种方案,即使第二种方案不能组成其他更多的面额,这样的选择也是完全没有错误的 推论 根据定律,可以得到下面这些推论 1、不存在一种方案包含两个10元、同理有两个50元也不存在 证明:如果存在一个方案包含两个10元,则我们可以选择用一个10元和一个20元代替这个方案。我们可以确定,两个10元只能组成10、20这两个面额。而10元和20元可以组成10元、20元、30元三个面额。根据定律,则确定此方案不可行。同理,两个50元可以用50+100代替 2、仅使用4个非100元的硬币,只有两种组成方案:10、20、20、50和20、20、20、50 证明:对于4个硬币,方案10、20、20、50可以组成10-100的所有情况,所以任何其他方案如果合理,则必须能组成有超过100元的情况。根据推论1,则50元不可以重复,所以能组成的最大的值就是用20、20、20、50组成110。 3、不存在使用5个非100元的硬币的情况 证明:首先如果使用了5个硬币,根据推论1,可以得到的组合仅两种:10、20、20、20、50即120元,和20、20、20、20、50即130元。那么无论这个组合怎么样,通过推论2的结论,都不如10、20、20、50、100这个组合,因为这个组合能完成10-200以内的所有解。那么根据定律,上述两个组合都是错误的组合。 4、大于110元的面额必须要通过一个或者数个100元的硬币来组成 证明:这个很简单,通过推理3可以直接推出。所以对于大于110元的面额都应不断的-100直到满足上述情况 打表 那么我们知道了最多只有4个非100元的硬币,那么我们就可以得到所有的组合情况(省略0) 12 51 2 1 5 2 2 2 5 1 2 2 1 2 5 2 2 2 2 2 5 1 2 2 5 2 2 2 5 那么我们可以把这些情况能组成的数字打表出来 12 51 2 3 1 5 6 2 4 2 5 7 1 2 3 4 5 1 2 3 5 6 7 8 2 4 6 2 4 5 7 9 1 2 3 4 5 6 7 8 9 10 2 4 6 7 9 11 借助位运算,我们可以这样做: 1 2 3 4 5 6 7 8 9 10 11 12 13 print(1 << 1) print(1 << 2) print(1 << 5) print((1 << 1) + (1 << 2) + (1 << 3)) print((1 << 1) + (1 << 5) + (1 << 6)) print((1 << 2) + (1 << 4)) print((1 << 2) + (1 << 5) + (1 << 7)) print((1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5)) print((1 << 1) + (1 << 2) + (1 << 3) + (1 << 5) + (1 << 6) + (1 << 7) + (1 << 8)) print((1 << 2) + (1 << 4) + (1 << 6)) print((1 << 2) + (1 << 4) + (1 << 5) + (1 << 7) + (1 << 9)) print((1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 6) + (1 << 7) + (1 << 8) + (1 << 9) + (1 << 10)) print((1 << 2) + (1 << 4) + (1 << 5) + (1 << 6) + (1 << 7) + (1 << 9) + (1 << 11)) 可以尝试理解这个表的原理,==即将每一个bite作为表的一个元素== 可以看到最大值只有110元。而注意到面额最大的硬币为100。而110元可以用非100元组成也可以通过10元+100元组成,同理100元也有两种组合方式。所以需要考虑四个情况: 1、100元用100元组成、110元用100元组成 2、100元用100元组成、110元用非100元组成 3、100元用非100元组成、110元用100元组成 4、100元用非100元组成、110元用非100元组成 那么都考虑一下,取较小者 AC-Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 #include<bits/stdc++.h> using namespace std; vector<int> res; void init() { res.push_back(0); res.push_back(2); res.push_back(4); res.push_back(32); res.push_back(14); res.push_back(98); res.push_back(20); res.push_back(164); res.push_back(62); res.push_back(494); res.push_back(84); res.push_back(692); res.push_back(2046); res.push_back(2804); } int get_ans(int as) { int t = -1; for (int i = 0; i < res.size(); ++i) { if ((res[i] ^ as) + (res[i] & as) == res[i]) { t = i; break; } } switch (t) { case 0: return 0; case 1: case 2: case 3: return 1; case 4: case 5: case 6: case 7: return 2; case 8: case 9: case 10: case 11: return 3; case 12: case 13: return 4; } return 1000; } int main() { #ifdef ACM_LOCAL freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); auto start_clock_for_debug = clock(); #endif ios::sync_with_stdio(false); cin.tie(0); cin.tie(0); int t; cin >> t; init(); while (t--) { int n; cin >> n; int hun[4]; int cnt[4][15]; memset(cnt, 0, sizeof(cnt)); memset(hun, 0, sizeof(hun)); bool flag = false; for (int i = 0; i < n; ++i) { int tmp; cin >> tmp; if (flag) continue; if (tmp % 10) { flag = true; continue; } // 100 + 110 int tmp0 = tmp; if (tmp0 % 100 > 10 and tmp0 > 100) { hun[0] = max(tmp0 / 100, hun[0]); tmp0 %= 100; cnt[0][tmp0 / 10]++; } else if (tmp0 > 100) { hun[0] = max((tmp0 - 100) / 100, hun[0]); tmp0 %= 100; cnt[0][tmp0 / 10 + 10]++; } else { cnt[0][tmp0 / 10]++; } // 100 int tmp1 = tmp; if (tmp1 % 100 == 0 and tmp1 > 100) { hun[1] = max((tmp1 - 100) / 100, hun[1]); cnt[1][10]++; } else { hun[1] = max(tmp1 / 100, hun[1]); cnt[1][(tmp1 % 100) / 10]++; } // 110 int tmp2 = tmp; if (tmp2 % 100 == 10 and tmp2 > 100) { hun[2] = max((tmp2 - 100) / 100, hun[2]); cnt[2][11]++; } else { hun[2] = max(tmp2 / 100, hun[2]); cnt[2][(tmp2 % 100) / 10]++; } // None int tmp3 = tmp; hun[3] = max(tmp3 / 100, hun[3]); cnt[3][(tmp3 % 100) / 10]++; } if (flag) { cout << -1 << endl; continue; } int ans; int get_ans[4]; for (int j = 0; j < 4; ++j) { ans = 0; for (int k = 1; k < 13; ++k) { if (cnt[j][k]) { ans += 1 << k; } } get_ans[j] = get_ans(ans) + hun[j]; } sort(get_ans, get_ans + 4); cout << get_ans[0] << endl; } #ifdef ACM_LOCAL auto end_clock_for_debug = clock(); cerr << "Run Time: " << double(end_clock_for_debug - start_clock_for_debug) / CLOCKS_PER_SEC << "s" << endl; #endif return 0; }

2019/8/21
articleCard.readMore

【2019牛客暑期多校第三场】J题LRU management

题目链接 题意 好吧,这道题我其实看都没看过,队友跟我说了说这道题是模拟题,卡时间。然后我就上了…… 大致就是维护一个线性表,然后有两种操作:插入、查询 插入时,如果这个值(string)之前出现过,则把之前那个值(string)放到线性表的表尾(删去原来那个),但是保存的值(int)仍是之前那个值(int)。如果没有出现过,则把它插入到表尾。如果插入后发现线性表长度超过 m ,则弹出表头的元素。 查询时,如果有这个值(string),然后根据要求查询这个值(string)的上一个或者下一个,再返回它的值(int),如果没有(没有上一个或者下一个也是)则输出:Invalid 分析 一开始觉得这个……应该就是拿STL可以暴力过的(当然不能太暴力)我选择了 unordered_map + list 听说用 map 会 T,没试过…… unordered_map 是哈希表,而 map 是红黑树,相对而言, map 的查询、插入、删除的时间比较稳定,都是 O(logN),而 unordered_map 的时间不确定性比较大,运气好就是 O(1) 的查询,运气差就是 O(N) 复杂度 平均为常数,最坏情况与容器大小成线性。 摘自cppreference unordered_map 用 string 作为索引,保存了 list 的迭代器 list 保存了值的顺序情况,包括了 string 和 int 两个变量 但是我第一发居然T了,然后加了快读就AC了,感觉就是被卡常了…… AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 #include <bits/stdc++.h> using namespace std; typedef list<pair < int, string>> ::iterator pl; unordered_map <string, pl> ump; list <pair<int, string>> lists; char catchmessage[100]; struct ioss { #define endl '\n' static const int LEN = 20000000; char obuf[LEN], *oh = obuf; std::streambuf *fb; ioss() { ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); fb = cout.rdbuf(); } inline char gc() { static char buf[LEN], *s, *t, buf2[LEN]; return (s == t) && (t = (s = buf) + fread(buf, 1, LEN, stdin)), s == t ? -1 : *s++; } inline ioss &operator>>(long long &x) { static char ch, sgn, *p; ch = gc(), sgn = 0; for (; !isdigit(ch); ch = gc()) { if (ch == -1) return *this; sgn |= ch == '-'; } for (x = 0; isdigit(ch); ch = gc()) x = x * 10 + (ch ^ '0'); sgn && (x = -x); return *this; } inline ioss &operator>>(int &x) { static char ch, sgn, *p; ch = gc(), sgn = 0; for (; !isdigit(ch); ch = gc()) { if (ch == -1) return *this; sgn |= ch == '-'; } for (x = 0; isdigit(ch); ch = gc()) x = x * 10 + (ch ^ '0'); sgn && (x = -x); return *this; } inline ioss &operator>>(char &x) { static char ch; for (; !isalpha(ch); ch = gc()) { if (ch == -1) return *this; } x = ch; return *this; } inline ioss &operator>>(string &x) { static char ch, *p, buf2[LEN]; for (; !isalpha(ch) && !isdigit(ch); ch = gc()) if (ch == -1) return *this; p = buf2; for (; isalpha(ch) || isdigit(ch); ch = gc()) *p = ch, p++; *p = '\0'; x = buf2; return *this; } inline ioss &operator<<(string &c) { for (auto &p: c) this->operator<<(p); return *this; } inline ioss &operator<<(const char *c) { while (*c != '\0') { this->operator<<(*c); c++; } return *this; } inline ioss &operator<<(const char &c) { oh == obuf + LEN ? (fb->sputn(obuf, LEN), oh = obuf) : 0; *oh++ = c; return *this; } inline ioss &operator<<(int x) { static int buf[30], cnt; if (x < 0) this->operator<<('-'), x = -x; if (x == 0) this->operator<<('0'); for (cnt = 0; x; x /= 10) buf[++cnt] = x % 10 | 48; while (cnt) this->operator<<((char) buf[cnt--]); return *this; } inline ioss &operator<<(long long x) { static int buf[30], cnt; if (x < 0) this->operator<<('-'), x = -x; if (x == 0) this->operator<<('0'); for (cnt = 0; x; x /= 10) buf[++cnt] = x % 10 | 48; while (cnt) this->operator<<((char) buf[cnt--]); return *this; } ~ioss() { fb->sputn(obuf, oh - obuf); } } io; int main() { #ifdef ACM_LOCAL freopen("./in.txt", "r", stdin); freopen("./out.txt", "w", stdout); #endif ios::sync_with_stdio(false); int t; io >> t; while (t--) { ump.clear(); lists.clear(); int q, m; io >> q >> m; string s; int op, val; for (int i = 0; i < q; i++) { pl cur; io >> op >> s >> val; if (op) { if (!ump.count(s)) { cout << "Invalid" << endl; continue; } cur = ump[s]; if (val == 1) { cur++; if (cur == lists.end()) { cout << "Invalid" << endl; continue; } } else if (val == -1) { if (cur == lists.begin()) { cout << "Invalid" << endl; continue; } cur--; } cout << (*cur).first << endl; } else { if (!ump.count(s)) { pair<int, string> newnode = make_pair(val, s); lists.push_back(newnode); pl tmp = lists.end(); tmp--; ump.insert(make_pair(s, tmp)); if (lists.size() > m) { ump.erase(lists.front().second); lists.pop_front(); } cout << val << endl; continue; } cur = ump[s]; pair<int, string> newnode = make_pair((*cur).first, s); lists.push_back(newnode); pl tmp = lists.end(); tmp--; ump[s] = tmp; lists.erase(cur); cout << newnode.first << endl; } } } return 0; }

2019/7/26
articleCard.readMore

【2019多校第一场补题 / HDU6582】2019多校第一场E题1005Path——最短路径+网络流

HDU6582链接 题意 在一张有向图中,有一个起点和一个终点,你需要删去部分路径,使得起点到终点的最短距离增加(并不要求需要使得距离变成最大值),且删除的路径长度最短。求删去的路径总长为多少 分析 一开始理解错题意了,以为是在保证路径变成最长的路径之后,求删去的路径和最小是多少。然后就自闭了很久,还WA了好几发。后来看到题目中是 longer 而不是 longest 。突然醒悟。直接最短路径 +网络流就行,中间重新建图。 大致的过程是先跑最短路径(我用了SPFA算法,因为当数据量较大时,图为稀疏图,所以用邻接表形式),然后求出起点到每一个点的距离(保存在数组 dist 中)。然后删掉所有的边,对满足下面等式的边进行重建(网络流的边,即同时需要搭建反向的边,只不过流量为0),然后跑网络流(我用了ISAP算法,仍然是邻接表) $dist[a] - dist[b] = edge[a to b]$ $a to b$ 指代这条边起点为 $a$ 终点为 $b$,且满足 $edge[b to a] = - edge[a to b]$ AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 #include <bits/stdc++.h> using namespace std; #define MAXN 20100 #define MAXM 20100 bool visited[MAXN]; //标记数组 long long dist[MAXN]; //源点到顶点i的最短距离 long long path[MAXN]; //记录最短路的路径 long long enqueue_num[MAXN]; //记录入队次数 long long vertex_num; //顶点数 long long edge_num; //边数 long long source; //源点 struct Edge { long long to, next, cap, flow; } edge[MAXM]; long long head[MAXN]; long long tot; long long gap[MAXN], dep[MAXN], cur[MAXN]; void init() { tot = 0; memset(head, -1, sizeof(head)); } void addedge(long long u, long long v, long long w) { edge[tot].to = v; edge[tot].cap = w; edge[tot].next = head[u]; edge[tot].flow = 0; head[u] = tot++; } bool SPFA() { memset(visited, 0, sizeof(visited)); memset(enqueue_num, 0, sizeof(enqueue_num)); for (long long i = 0; i < vertex_num; i++) { dist[i] = __LONG_LONG_MAX__; path[i] = source; } queue<long long> Q; Q.push(source); dist[source] = 0; visited[source] = true; enqueue_num[source]++; while (!Q.empty()) { long long u = Q.front(); Q.pop(); visited[u] = 0; for (long long curnode = head[u]; curnode != -1; curnode = edge[curnode].next) { if (dist[u] + edge[curnode].cap < dist[edge[curnode].to]) { dist[edge[curnode].to] = dist[u] + edge[curnode].cap; path[edge[curnode].to] = u; if (!visited[edge[curnode].to]) { Q.push(edge[curnode].to); enqueue_num[edge[curnode].to]++; if (enqueue_num[edge[curnode].to] >= vertex_num) return false; visited[edge[curnode].to] = 1; } } } } return true; } long long Q[MAXN]; void BFS(long long start, long long end) { memset(dep, -1, sizeof(dep)); memset(gap, 0, sizeof(gap)); gap[0] = 1; long long front = 0, rear = 0; dep[end] = 0; Q[rear++] = end; while (front != rear) { long long u = Q[front++]; for (long long i = head[u]; i != -1; i = edge[i].next) { long long v = edge[i].to; if (dep[v] != -1) continue; Q[rear++] = v; dep[v] = dep[u] + 1; gap[dep[v]]++; } } } long long S[MAXN]; long long sap(long long start, long long end, long long N) { BFS(start, end); memcpy(cur, head, sizeof(head)); long long top = 0; long long u = start; long long ans = 0; while (dep[start] < N) { if (u == end) { long long Min = __LONG_LONG_MAX__; long long inser; for (long long i = 0; i < top; i++) { if (Min > edge[S[i]].cap - edge[S[i]].flow) { Min = edge[S[i]].cap - edge[S[i]].flow; inser = i; } } for (long long i = 0; i < top; i++) { edge[S[i]].flow += Min; edge[S[i] ^ 1].flow -= Min; } ans += Min; top = inser; u = edge[S[top] ^ 1].to; continue; } bool flag = false; long long v; for (long long i = cur[u]; i != -1; i = edge[i].next) { v = edge[i].to; if (edge[i].cap - edge[i].flow && dep[v] + 1 == dep[u]) { flag = true; cur[u] = i; break; } } if (flag) { S[top++] = cur[u]; u = v; continue; } long long Min = N; for (long long i = head[u]; i != -1; i = edge[i].next) if (edge[i].cap - edge[i].flow && dep[edge[i].to] < Min) { Min = dep[edge[i].to]; cur[u] = i; } gap[dep[u]]--; if (!gap[dep[u]]) return ans; dep[u] = Min + 1; gap[dep[u]]++; if (u != start) u = edge[S[--top] ^ 1].to; } return ans; } long long n, m; int a[MAXN], b[MAXN], c[MAXN]; void reISAP() { init(); for (int i = 0; i < m; i++) { if (c[i] == dist[b[i]] - dist[a[i]]) { addedge(a[i], b[i], c[i]); addedge(b[i], a[i], 0); } } } int main() { #ifdef ACM_LOCAL freopen("./in.txt", "r", stdin); freopen("./out.txt", "w", stdout); #endif ios::sync_with_stdio(false); long long t; cin >> t; while (t--) { cin >> n >> m; source = 1; vertex_num = n + 1; init(); for (long long i = 0; i < m; i++) { cin >> a[i] >> b[i] >> c[i]; addedge(a[i], b[i], c[i]); } if (!SPFA()) { cout << '0' << endl; continue; } reISAP(); cout << sap(1, n, n) << endl; } return 0; } 总结 理解了题意之后感觉就是一道板子题…… 人尽皆知**题

2019/7/23
articleCard.readMore

【2019多校第一场补题 / HDU6578】2019多校第一场A题1001Blank——dp

HDU6578链接 题意 有一串字符串,仅由 ${0, 1, 2, 3}$ 组成,长度为 $n$,同时满足 $m$ 个条件。每个条件由三个整数组成:$l、r、x$ 表示在这个字符串的 $[l, r]$ 这个区间内,有且仅有 $x$ 个不同的字符,求问可能的组合有多少种(mod 998244353) 分析题意 因为前几天刚刚写了牛客暑期多校第二场,其中有一道题:ABBA(我的题解)感觉有点接近,所以第一想法就是dp了。但是这道题的字符多,不能像ABBA一样压缩至一维。所以只能想想看当时牛客官方给的题解的方法了。 考虑到之后需要判断条件是否满足,所以我第一感觉就是得定义一个超多维度的dp数组: 1 2 const int MAXN = 110; long long dp[MAXN][MAXN][MAXN][MAXN][MAXN]; 各个维度的定义如下: 对于 dp[a][b][c][d][t] 表示整个字符串长度为 t ,最后一次出现 0 的位置为 a,最后一次出现 1 的位置为 b ,最后一次出现 2 的位置为 c ,最后一次出现 3 的位置为d 然后可以得到状态转移方程 1 2 3 4 dp[t + 1][b][c][d][t + 1] += dp[a][b][c][d][t]; dp[a][t + 1][c][d][t + 1] += dp[a][b][c][d][t]; dp[a][b][t + 1][d][t + 1] += dp[a][b][c][d][t]; dp[a][b][c][t + 1][t + 1] += dp[a][b][c][d][t]; 当然这个数组肯定是没法开的,所以把 t 压缩了,变成滚动dp 1 2 const int MAXN = 110; long long dp[MAXN][MAXN][MAXN][MAXN][2]; 然后通过滚动的方式来实现。 但是这并不是卡死这种方法的原因。 根据上面这些,可以写出整个dp过程,大概就是这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for (int t = 1; t <= n; t++) { for (int a = 0; a <= t; a++) for (int b = 0; b <= t; b++) for (int c = 0; c <= t; c++) for (int d = 0; d <= t; d++) dp[a][b][c][d][t & 1] = 0; for (int a = 0; a <= t; a++) for (int b = 0; b <= t; b++) for (int c = 0; c <= t; c++) for (int d = 0; d <= t; d++) { dp[t + 1][b][c][d][t & 1] += dp[a][b][c][d][t ^ 1]; dp[a][t + 1][c][d][t & 1] += dp[a][b][c][d][t ^ 1]; dp[a][b][t + 1][d][t & 1] += dp[a][b][c][d][t ^ 1]; dp[a][b][c][t + 1][t & 1] += dp[a][b][c][d][t ^ 1]; } } 这个复杂的……这还没加上判断是否满足条件…… 无论是时间是还是空间上,估计都悬(时间上应该是一定过不了了) 然后只能继续压缩。 考虑到最后无论哪种状态下,$a, b, c, d$ 四个变量中,必定有一个且仅有一个变量的值为 t 。而且在这道题中,字符 ${0, 1, 2, 3}$ 完全等价。所以我们再压缩一维 1 2 const int MAXN = 110; long long dp[MAXN][MAXN][MAXN][2]; 此时的意义如下: 对于变量 dp[x][y][z] 表示字符 ${0, 1, 2, 3}$ 中,其中一个最后出现的位置为当前的字符最后(由于这个条件恒成立,所以并未被记录在数组中),剩下的三个字符分别出现在$x, y, z$处,且保证 $i < x \leq y \leq z (i 为当前字符长度)$ (仅当 $x = y = 0$ 时满足前面一个等于号,后面的等于号同理。而字符串长度至少为1,且此时$x、y、z$均为0,所以不存在 $i = x$ 的情况。)而后面的长度为 2 的维度指代当前状态和 上一个状态(滚动dp) 可以得到状态转移方程: 1 2 3 4 5 // i 为当前字符串长度,cur 为当前状态,last 为上一个状态 dp[x][y][z][cur] += dp[x][y][z][last];// 加入的字符与上一个加入的字符相同 dp[i - 1][y][z][cur] += dp[x][y][z][last]; dp[i - 1][x][z][cur] += dp[x][y][z][last]; dp[i - 1][x][y][cur] += dp[x][y][z][last]; 得到整个 dp 过程: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 dp[0][0][0][0] = 1; int cur = 1; int last = 0; for (int i = 1; i <= n; i++) for (int x = 0; x <= i; x++) for (int y = 0; y <= x; y++) for (int z = 0; z <= y; z++) dp[x][y][z][cur] = 0; for (int x = 0; x < i; x++) for (int y = 0; y <= x; y++) for (int z = 0; z <= y; z++) { dp[x][y][z][cur] += dp[x][y][z][last]; dp[i - 1][y][z][cur] += dp[x][y][z][last]; dp[i - 1][x][z][cur] += dp[x][y][z][last]; dp[i - 1][x][y][cur] += dp[x][y][z][last]; dp[x][y][z][cur] %= mod; // 别忘了 mod dp[i - 1][y][z][cur] %= mod; dp[i - 1][x][z][cur] %= mod; dp[i - 1][x][y][cur] %= mod; } swap(cur, last); 考虑条件 需要判断一个区间内是否满足有多个不同的字符 我们可以根据区间右端为基准,当前dp的字符串长度到达一个条件的右端的时候,通过 $x、y、z$ 的值来判断是否到达了要求,如果没有则将此 dp 的赋值为0。如果懒得思考可以直接分类讨论一下就行了。虽然代码会比较长。 AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <bits/stdc++.h> using namespace std; const int MAXN = 110; const int mod = 998244353; long long dp[MAXN][MAXN][MAXN][2]; // dp[i][j][k] 表示上一次出现不同数字的位置分别是 i、j、k、当前位置 struct Conditions { int l, x; Conditions(int ll, int xx) : l(ll), x(xx) {} }; vector <Conditions> conditions[MAXN]; // 用来保存要求 int main() { #ifdef ACM_LOCAL freopen("./in.txt", "r", stdin); freopen("./out.txt", "w", stdout); #endif ios::sync_with_stdio(false); int t; cin >> t; while (t--) { int n, m; cin >> n >> m; int a, b, c; for (int i = 0; i < MAXN; i++) { conditions[i].clear(); } for (int i = 0; i < m; i++) { cin >> a >> b >> c; conditions[b].push_back(Conditions(a, c)); // 要求按照 r 的不同来保存 } dp[0][0][0][0] = 1; int cur = 1; int last = 0; for (int i = 1; i <= n; i++) { for (int x = 0; x <= i; x++) { for (int y = 0; y <= x; y++) { for (int z = 0; z <= y; z++) { dp[x][y][z][cur] = 0; } } } for (int x = 0; x < i; x++) { for (int y = 0; y <= x; y++) { for (int z = 0; z <= y; z++) { dp[x][y][z][cur] += dp[x][y][z][last]; dp[i - 1][y][z][cur] += dp[x][y][z][last]; dp[i - 1][x][z][cur] += dp[x][y][z][last]; dp[i - 1][x][y][cur] += dp[x][y][z][last]; dp[x][y][z][cur] %= mod; dp[i - 1][y][z][cur] %= mod; dp[i - 1][x][z][cur] %= mod; dp[i - 1][x][y][cur] %= mod; } } } for (int s = 0; s < conditions[i].size(); s++) { for (int x = 0; x < i; x++) { for (int y = 0; y <= x; y++) { for (int z = 0; z <= y; z++) { int cnt = 1 + (x >= conditions[i][s].l ? 1 : 0) + (y >= conditions[i][s].l ? 1 : 0) + (z >= conditions[i][s].l ? 1 : 0); // 判断剩下三个位置是否满足条件 if (cnt != conditions[i][s].x) dp[x][y][z][cur] = 0; } } } } swap(cur, last); } long long ans = 0; // 求算最终答案。需要把所有的情况都加起来 for (int x = 0; x < n; x++) { for (int y = 0; y <= x; y++) { for (int z = 0; z <= y; z++) { ans += dp[x][y][z][last]; ans %= mod; } } } cout << ans << endl; } return 0; }

2019/7/23
articleCard.readMore

C/C++ 实现下标为负数的数组

C/C++语言中规定,数组下标为 $[0, n)$ 但是我们可以通过指针的方式来自定义数组下标 例如如下代码: 1 2 int a[10]; int *pa = a + 5; 此时,数组 pa 就是一个下标范围在 -5 到 4 的数组

2019/7/22
articleCard.readMore

【2019牛客暑期多校第一场】E题ABBA

题目链接 大致题意 有$(n + m)$个字母A和$(n + m)$个字母B,组成一个长度为 $2*(n + m)$的字符串,并且使得字符串中有$n$个“AB”和$m$个“BA”,求出可能的组合数(mod 1e9+7) 例如,n = 1 m = 2时,可以有这样的字符串(并不是全部的字符串): ABBABA ABBBAA BBABAA 上面三个字符串均满足条件 解题思路 考虑递推,假设已经有一个字符串满足一定的“先决条件”(此处应当理解为数学归纳法,及假设n - 1时满足) 下面考虑==在字符串最后加入一个字符==的情况。仅有两种可能:加A或者加B(这不是白说吗) 但是考虑一下极端情况,我们可以得到一些简单的且明显的条件(N~A~表示已经在字符串中的A个数,N~B~同理) 假如字符串的组成类似这样: AAAAABBBBBBBB 则此字符串中,只能组合出 AB 而不可能组合出 BA 此时,我们假设 $n$ 为 5 而这个字符只能且必定要组合成 5 个AB,也就是说,我们接下来加入字符,只能加入 A 而不能加入 B 此时我们往前推,如果出现了这样一个字符串,则在之前,必定出现如下状态: AAAAA 也即是 5 个A的情况,此时我们可以得到一个确定的关系式: N~B~ = 0 and N~A~ <= n 推广到有B的情况,最优的情况就是所有的B都是用来组成BA,那么可以得到我们真正需要的关系式: N~A~ - N~B~ <= n 同理,相对于 B 而言,我们可以得到 N~B~ - N~A~ <= m 合并上述两式 -n < N~A~ - N~B~ <= m 所以根据下标为 N~A~ - N~B~ 建立DP数组,下标范围为 -n 到 m (均包含)DP的内容为方案数量(mod 1e9 + 7),递推公式为 $dp[i] = dp[i - 1] + dp [i + 1]$ 其中,dp[i - 1]指的是加入一个B(增加一个B使得N~A~ - N~B~变小)。而dp[i + 1]指的是加入一个A 当考虑到无论是正向dp还是逆向dp,均有值优先于dp[i]先更新(dp[i - 1]和dp[i + 1]会比dp[i]先更新),所以采用两个dp数组的方式,初始值dp[0]=1。每两次dp完后,dp[0]的值及为答案。 AC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <bits/stdc++.h> using namespace std; #define MAXN 2100 #define MOD (int)(1e9 + 7) typedef long long ll; ll dp[MAXN][2]; int trans(int x) { return x + 1000; } int main() { #ifdef ACM_LOCAL freopen("debug.txt", "r", stdin); #endif ios::sync_with_stdio(false); int n, m; while (cin >> n >> m) { memset(dp, 0, sizeof(dp)); dp[n][1] = 1; int cur = 0; int last = 1; for (int i = 0; i < 2 * (n + m); i++) { for (int j = -n; j <= m; j++) { if (j != -n) { dp[j + n][cur] += dp[j - 1 + n][last]; dp[j + n][cur] %= MOD; } if (j != m) { dp[j + n][cur] += dp[j + 1 + n][last]; dp[j + n][cur] %= MOD; } } for (int j = -n; j <= m; j++) { dp[j + n][last] = 0; } swap(cur, last); } cout << dp[n][last] << endl; } return 0; }

2019/7/22
articleCard.readMore

HDU2883 kebab——最大流

题目链接 把“时间粒子”作为最大流的计算结果 设置超级源点为 0 顾客点范围为 1 - 204 时间点 205 - 610 超级汇点 615 超级源点与所有顾客连线,容量为需求的烤肉数 * 需求的每块烤肉的时间(即此顾客需要占用的总时间粒子) 顾客与时间点进行连线,仅当此时间点在顾客等待的时间段内,容量为INF 每个时间点与汇点连线,容量为 m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #include <bits/stdc++.h> using namespace std; /* * 最大流 SAP 算法,用 GAP 优化后 * 先把流量限制赋值到 maps 数组 * 然后调用 SAP 函数求解 * 可选:导出路径 */ #define MAXN 620 int maps[MAXN][MAXN]; // 存图 int pre[MAXN]; // 记录当前点的前驱 int level[MAXN]; // 记录距离标号 int gap[MAXN]; // gap常数优化 // vector<int> roads[MAXN]; // 导出的路径(逆序) // int curRoads; // 导出的路径数 // 入口参数vs源点,vt汇点 int SAP(int vs, int vt) { memset(pre, -1, sizeof(pre)); memset(level, 0, sizeof(level)); memset(gap, 0, sizeof(gap)); gap[0] = vt; int v, u = pre[vs] = vs, maxflow = 0, aug = INT_MAX; // curRoads = 0; while (level[vs] < vt) { // 寻找可行弧 for (v = 1; v <= vt; v++) { if (maps[u][v] > 0 && level[u] == level[v] + 1) { break; } } if (v <= vt) { pre[v] = u; u = v; if (v == vt) { // int neck = 0; // Dnic 多路增广优化,下次增广时,从瓶颈边(后面)开始 aug = INT_MAX; // 寻找当前找到的一条路径上的最大流 (瓶颈边) for (int i = v; i != vs; i = pre[i]) { // roads[curRoads].push_back(i); // 导出路径——可选 if (aug > maps[pre[i]][i]) { aug = maps[pre[i]][i]; // neck = i; // Dnic 多路增广优化,下次增广时,从瓶颈边(后面)开始 } } // roads[curRoads++].push_back(vs); // 导出路径——可选 maxflow += aug; // 更新残留网络 for (int i = v; i != vs; i = pre[i]) { maps[pre[i]][i] -= aug; maps[i][pre[i]] += aug; } // 从源点开始继续搜 u = vs; // u = neck; // Dnic 多路增广优化,下次增广时,从瓶颈边(后面)开始 } } else { // 找不到可行弧 int minlevel = vt; // 寻找与当前点相连接的点中最小的距离标号 for (v = 1; v <= vt; v++) { if (maps[u][v] > 0 && minlevel > level[v]) { minlevel = level[v]; } } gap[level[u]]--; // (更新gap数组)当前标号的数目减1; if (gap[level[u]] == 0) break; // 出现断层 level[u] = minlevel + 1; gap[level[u]]++; u = pre[u]; } } return maxflow; } // 超级源点 0 // 顾客点 1 - 204 // 时间点 205 - 610 // 超级汇点 615 int n, m; const int MaxPeople = 210; const int Ed = 615; struct costman { int b, e, need, time; }; set<int> timelist; costman costmanlist[MaxPeople]; void init() { memset(maps, 0, sizeof(maps)); set<int>::iterator iterl = timelist.begin(); set<int>::iterator iterr = timelist.begin(); iterr++; int curiter = 0; for (size_t i = 0; i < n; i++) { maps[0][i + 1] = costmanlist[i].need * costmanlist[i].time; } while (iterr != timelist.end()) { maps[205 + curiter][Ed] = ((*iterr) - (*iterl)) * m; for (size_t i = 0; i < n; i++) { if (costmanlist[i].b <= *iterl && costmanlist[i].e >= *iterr) { maps[i + 1][curiter + 205] = INT_MAX; } } iterl++; iterr++; curiter++; } } int main() { #ifdef ACM_LOCAL freopen("./in.txt", "r", stdin); freopen("./out.txt", "w", stdout); #endif ios::sync_with_stdio(false); while (cin >> n >> m) { timelist.clear(); long long sum = 0; for (size_t i = 0; i < n; i++) { cin >> costmanlist[i].b >> costmanlist[i].need >> costmanlist[i].e >> costmanlist[i].time; sum += costmanlist[i].need * costmanlist[i].time; timelist.insert(costmanlist[i].b); timelist.insert(costmanlist[i].e); } init(); cout << (sum == SAP(0, Ed) ? "Yes" : "No") << endl; } return 0; }

2019/7/11
articleCard.readMore

2019年-西北大学集训队选拔赛——D温暖的签到题

题目链接 一道珂朵莉树题,非常有意思 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <bits/stdc++.h> #define NUM #define ll long long using namespace std; int n, m; struct node { int l; int sta;//第一个值 int r;//长度 ll sum(int l, int r) { // 区间求和 if (l < this->l) l = this->l; if (r > this->r) r = this->r; return (((l - this->l) + sta + (r - this->l) + sta)) * ((ll) r - l + 1) / 2; } }; map<int, node> mp; // 利用map自动排序,形成一个一维的块链,每一个块为一个给定规则的数据组成 ll cal(int l, int r) { // 计算区间和,直接调用块内的自定义函数求和 map<int, node>::iterator itl = mp.upper_bound(l); itl--; map<int, node>::iterator itr = mp.upper_bound(r); ll ans = 0; while (itl != itr) { ans += (*itl).second.sum(l, r); itl++; } return ans; } void update(int l, int r) { // 更新块,删除被覆盖的块,形成新块 if (r == 1) return; map<int, node>::iterator itl = mp.upper_bound(l); itl--; map<int, node>::iterator itr = mp.upper_bound(r); itr--; node templ = (*itl).second; node tempr = (*itr).second; tempr.sta = tempr.sta + (r + 1 - tempr.l); templ.r = l - 1; tempr.l = r + 1; itr++; while (itl != itr) { auto tmp = itl++; mp.erase(tmp); } node newnode; newnode.l = l; newnode.r = r; newnode.sta = 1; mp[l] = newnode; if (templ.l <= templ.r) { mp[templ.l] = templ; } if (tempr.l <= tempr.r) { mp[tempr.l] = tempr; } } int main() { scanf("%d%d", &n, &m); int opt, l, r; node tot; tot.l = 1; tot.r = n; tot.sta = 1; mp[1] = tot; while (m--) { scanf("%d%d%d", &opt, &l, &r); if (opt == 1) update(l, r); else printf("%lld\n", cal(l, r)); } return 0; }

2019/7/11
articleCard.readMore