比赛连接:跳转
A、Last Blood
题目大意:
给你一个长度为 n n n的序列,然后让你输出该序列从左至右最后一个在这个序列中出现至少三次的数,没有就输出 − 1 -1 −1。
分析:
观察到数据范围 ( 1 ≤ a i ≤ n ) (1 \le a_{i} \le n) (1≤ai≤n)所以我们只需要对 n n n个数映射一下,从左至右遍历,知要当前数字的个数大于等于 3 3 3,就令答案为当前数字。
AC代码:
#include <bits/stdc++.h>
int n, m;
void solve()
{
std::cin >> n;
std::vector<int> rec(n + 5, 0);
// 其实就是开了一个n + 5大小的数组,令其初始值为0
int ans = -1;
for (int i = 1; i <= n; i ++) {
int a; std::cin >> a;
rec[a] ++;
if (rec[a] >= 3) ans = a;
}
std::cout << ans << '\n';
return ;
}
int main(){
int _ = 1; std::cin >> _; // 多组输入
while (_ --) {
solve();
}
return 0;
}
B、Funny version parity check
题目大意:
给你一个长度为n的数组,然后你现在可以进行如下操作任意次:
- 选择所有奇数位置上的数字,令它们都加 1 1 1
- 选择所有偶数位置上的数字,令它们都加 1 1 1
问你有没有可能使得所有这个 n n n个数的奇偶性都一样。
分析:
这一题虽然是英文题,但是题意就是这么简单,我们只需要知道在这个数组中,奇数位置上的数奇偶性是否一致,因为我们在进行一次操作时,其所有对象的奇偶性都会发生变化,而偶数位置和奇数位置是毫不相交的两个区间,所以当无执行一次无法成功时,就一定无法成功。
同理偶数位置上亦是如此。
AC代码:
#include <bits/stdc++.h>
int n, m, k;
void solve()
{
std::cin >> n;
std::vector<int> a(n + 5, 0);
for (int i = 1; i <= n; i ++) {
std::cin >> a[i];
}
bool isFlag1 = true, isFlag2 = true;
for (int i = 3; i <= n; i += 2) {
if (a[i] % 2 != a[1] % 2) isFlag1 = false;
}
for (int i = 4; i <= n; i += 2) {
if (a[i] % 2 != a[2] % 2) isFlag2 = false;
}
// 对于奇数位置,我们以a[1]为基准,看是否都一样。偶数同理以a[2]为基准
if (isFlag1 && isFlag2) std::cout << "YES" << '\n';
else std::cout << "NO" << '\n';
return ;
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
C、xhxhxxh’s contest
题目大意:
同样给你一个长度为n的序列, a i a_{i} ai代表题目的难度,现在你能解决难度小于等于 k k k的问题,但是你只能从题单的两侧来解决问题,并且只有这个问题解决了才能继续解决下一个问题。
问你最多能解决几个问题。
分析:
英文题,题目意思也很简单,做法也似乎一目了然。直接从数组两边都便利一遍,便利到不能再便利了为止,统计个数,然后输出成功AC 。
这样的做法有个 b u g bug bug,例如:
5 10
1 2 3 4 5
这个数据我们每次都会遍历到数组结束。所以结果会是 10 10 10,但 10 10 10是错的。如何修改,其实做法有很多。我没用一个变量存我从左至右遍历时,遍历到了哪个地方,然后再到我们从右至左时,不能涉及到这个部分,因为这个部分我们都已经遍历过了,问题都解决了。
然后统计个数即可。
AC代码
#include <bits/stdc++.h>
void solve()
{
int n, k; std::cin >> n >> k;
std::vector<int> rec(n + 5, 0);
for (int i = 1; i <= n; i ++) {
std::cin >> rec[i];
}
int pos = 0, ans = 0;
for (int i = 1; i <= n; i ++) {
if (rec[i] <= k) {
ans ++;
pos = i;
}
else break ;
}
for (int i = n; i && i > pos; i --) {
if (rec[i] <= k) ans ++;
else break ;
}
std::cout << ans << '\n';
return ;
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; // std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
D、仓库记录
题目大意:
有一个仓库,每天都有进出货的记录,现在要求对不定时对仓库进行检查,要求报告仓库中最重货物的重量。
仓库的进出货的行动为:每次入库我们都会将货物放置在仓库的最里层,除非里层已经有货物了,而出库时我们会在最外层拿走货物,即每次出库操作出库的货物为当前在仓库里所有货物中最晚入库的货物。
一共有 n n n次操作,每次操作有如下三种形式(至少会有一次检查操作):
- 格式 1 1 1: 0 0 0 x x x: 入库操作,表示将重量为𝑥的货物装入仓库 ( 1 ≤ x ≤ 1 0 9 ) (1 \le x \le 10^{9}) (1≤x≤109)。
- 格式 2 2 2: 1 1 1: 出库操作
- 格式 3 3 3: 2 2 2: 查询操作,你需要输出当前仓库中最重货物的重量。
分析:
先进后出,一目了然栈结构,只是要求对当前栈中的元素输出最大值。
其实不难,我们只需要在模拟栈的过程中,对加入栈结构的元素做一下改变,我不需要加入入库的货物的重量,我没只需要将当前要入库的货物的重量和栈中已经存在的货物的重量取个最大值,再加入到栈中即可。
这样以来,我没能时刻保证我们的栈顶元素是加入到栈中的所有元素的最大值。
AC代码:
#include <iostream>
const int N = 2e5 + 10;
int n;
int stack[N];
void solve()
{
std::cin >> n;
int pos = 0;
for (int i = 1; i <= n; i ++) {
int op; std::cin >> op;
if (op == 0) { // 入栈操作
int xx; std::cin >> xx;
++ pos;
stack[pos] = std::max(stack[pos - 1], xx);
// 我们只需要加入当前需要加入的元素和栈中所有元素中的最大元素中的最大值即可
// 即:我们只需要加入 max(需要加入的元素, 栈中所有元素的最大值)
}
else if (op == 1) pos -- ; // 出栈操作
else std::cout << stack[pos] << '\n'; // 输出栈顶,栈顶一定是所有元素中的最大值
}
return ;
}
int main()
{
int _ = 1; // std::cin >> (_);
while (_ --) {
solve();
}
}
E、爱在西元前
题目大意:
现在你有 n n n ( 1 ≤ n ≤ 2 × 1 0 4 ) (1 \le n \le 2 \times 10^{4}) (1≤n≤2×104),每个指令有以下五种形式:
- 1: 向右移动一个单位
- 2: 向左移动一个单位
- 3: 向下移动一个单位
- 4: 向上移动一个单位
- 0 x x x: 表示执行第 x x x 条指令相同的动作。这里我们保证 x x x 是一个正整数,且不超过之前执行指令数。
假设你输出位置在 ( 0 , 0 ) (0, 0) (0,0),那么问你执行 n n n次操作之后,你所在的位置。数据包含多组输入 ( 1 ≤ t ≤ 100 ) (1 \le t \le 100) (1≤t≤100)
分析:
看上去很简单,可以直接暴力模拟,但是其实不可以的。
首先因为
0
0
0操作的存在,所以我们需要将每次操作的内容都存下来。我们简单计算一下,对于
0
0
0操作,如果我们只是简单的执行第
k
k
k次操作,那么时间复杂度是
O
(
1
)
O(1)
O(1)的,但是若是第
k
k
k次操作也是
0
0
0操作,那我们将继续返回上去。假设我们当前是第
i
i
i次操作除了第一次操作,其他操作都是
0
0
0操作,那么我们的时间复杂度是
O
(
i
)
O(i)
O(i)的,
当
n
n
n次操作的时间复杂度都是
O
(
i
)
O(i)
O(i)的的话,那么总时间复杂度就会是
O
(
t
∗
n
2
)
O({t*n^2})
O(t∗n2)的。那么一定会超时。如图所示:
我们注意到,
0
0
0操作的结果是固定的,在中途我们不会有其他操作来导致这个结果改变。我没之前说到如果我们只是简单的执行第
k
k
k次操作,那么时间复杂度是
O
(
1
)
O(1)
O(1)的,那么当我们第一次遇到
0
0
0操作时,我们可以直接将第
k
k
k次的内容替换到当前操作,那么对于以后的每次
0
0
0操作,我们都将会是第一次
0
0
0操作,则 每一次
0
0
0操作的时间复杂地都是
O
(
1
)
O(1)
O(1)。即 总时间复杂度为
O
(
t
∗
n
)
O(t*n)
O(t∗n)。随便过。下图为转变过程:
AC代码:
#include <bits/stdc++.h>
int dx[5] = {0, 1, -1, 0, 0}, dy[5] = {0, 0, 0, -1, 1};
// 对应下,上,左,右关于x和y的变化
int n;
void solve()
{
std::cin >> n;
int x = 0, y = 0;
std::vector<int> rec(n + 5, 0);
for (int i = 1; i <= n; i ++) {
int op; std::cin >> op;
if (op == 0) {
int k; std::cin >> k;
x += dx[rec[k]]; y += dy[rec[k]];
rec[i] = rec[k];
} // 是0操作,我们就直接存储第k次操作的内容
else { x += dx[op]; y += dy[op]; rec[i] = op; }
}
std::cout << x << ' ' << y << '\n';
return ;
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
F、鱼塘计划
题目大意:
我们有 n n n根珊瑚在鱼塘里,第 i i i根珊瑚的高度为 a i a_{i} ai。之后,我们需要把珊瑚周围围起来,具体如下:
- 选取一个整数 h ≥ 1 h \ge 1 h≥1——鱼塘高度。所围起来鱼塘距塘底的高度。
- 然后,往鱼塘中注满水,使得每一列的高度都是 h h h,除非珊瑚的的高度超过 h h h,否则,这一列必须将水注满至 h h h。
求:当我们最多可以使用 x x x个单位的水来装满鱼塘时, h h h最大可以是多少?
分析:
对于 n n n根珊瑚,高度错综复杂,但是根据题意来看,我们的答案 h h h高度。若已经确定,那就说明对于所有小于等于 h h h的珊瑚,我们都是可以完全围住的。所以我们答案 h h h可以从小到大遍历所有的珊瑚来确定。这里首先对 a a a数组进行排序。
- 我们仅围住第一根珊瑚的时候,我们不需要水,所以我们从第二根珊瑚考虑。
- 当我们的水足够围住第二根珊瑚时,第一根肯定会被水浸住。此时我们需要用掉 ( a [ 2 ] − a [ 1 ] ) ∗ 1 (a[2] - a[1]) * 1 (a[2]−a[1])∗1个单位的水来将第一根珊瑚的高度填充至第二根一样的高度,此时水的单位减少这么多。
- 当我们的水不能够支撑我们围住第二根珊瑚的时候,我们将水全部用掉, h h h能提升多少是多少,则我们可以用 m / 1 m / 1 m/1个单位的水来将第一根珊瑚提高这么多高度,此时水的单位仅剩下 m m m m o d mod mod 1 1 1。
现在,我们将上述推广一下,将我们想要判断水是否能围住第
i
+
1
i + 1
i+1根珊瑚时,那我们一定是将
1
1
1 ~
i
i
i根珊瑚都提高到了
a
[
i
]
a[i]
a[i]这么高。所以我们在判断是,需要用
(
a
[
i
+
1
]
−
a
[
i
]
)
∗
i
(a[i + 1] - a[i]) * i
(a[i+1]−a[i])∗i来判断,如下图所示:
当我们需要判断我们如今的水是否能完全围住第六根珊瑚的时候,我前五根珊瑚都已经被水填充到了
a
[
5
]
a[5]
a[5]的高度,所以我们再想提升高度的时候,我们需要这五根同时提高。
在 n n n根珊瑚全便利完之后,我们需判断剩下的水是否能再次让答案提高,即需要 a n s = a n s + m / n ; ans = ans + m / n; ans=ans+m/n;
ps.当然这一题也可以二分答案不过没有直接遍历所有珊瑚效率高。
AC代码:
#include <bits/stdc++.h>
using i64 = long long;
const int N = 2e5 + 10;
int n, m;
i64 a[N];
void quick_sort(i64 q[],i64 l,i64 r) // 手写快排
{
if(l >= r) return ;
i64 x = q[(l + r) / 2], i = l, j = r;
while(i <= j) {
for(; q[i] < x; i ++);
for(; q[j] > x; j --);
if(i <= j){
std::swap(q[i], q[j]); i ++; j --;
}
}
quick_sort(q, l, j);
quick_sort(q, i, r);
return ;
}
void solve()
{
std::cin >> n >> m;
for (int i = 1; i <= n; i ++) {
std::cin >> a[i];
}
quick_sort(a, 1, n);
// 对于会C++选手可以直接用库函数
// std::sort(a + 1, a + n + 1);
// 这个函数就是库函数快排 默认从小到大
i64 ans = a[1];
for (int i = 1; i < n; i ++) {
if (m >= (a[i + 1] - a[i]) * i) {
m -= (a[i + 1] - a[i]) * i;
ans = a[i + 1];
}
else {
ans += m / i; // 虽然不够完全围住i + 1根珊瑚,但是高度能提高多少算多少
m = m % i;
break ; // 都已经不够完全围住第i + 1根珊瑚了,直接break;
}
} ans += m / n;
std::cout << ans << '\n';
return ;
}
int main()
{
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
G、蜂巢计划
题目大意:
我们有为蜜蜂打造了形如下图的蜂巢:
我们可将蜂巢看成一个由
m
m
m个小六边形组成的一个大六边形,在图一中
m
=
7
m = 7
m=7。为了使它们适宜居住,我们需要往每个巢穴里打蜡,规则如下:
- 你可以放一只蜜蜂进任意小六边形为其打蜡,但一旦放入便不能拿出,除非杀死它。
- 如果一个未打蜡巢穴与三个以上有蜡的巢穴相邻,蜜蜂就会进入该巢穴并为它打蜡。一旦蜜蜂改造了所有巢穴后,再放入一只蜜蜂,他就会变成蜂王,如此该蜂巢中的蜜蜂才会延续下去。
如今你有 n n n只蜜蜂和无限多个由 m m m个小六边形组成的大六边形蜂巢,问:你最多能让多少个蜂巢延续下去。 ( 1 ≤ n ≤ 1 0 18 , 7 ≤ m ≤ 1 0 18 ) (1 \le n \le 10^{18}, 7 \le m \le 10^{18}) (1≤n≤1018,7≤m≤1018)
输入的 m m m保证用 m m m个小六边形一定能组成一个大六边形 (本题包含多组输入 1 ≤ t ≤ 1 0 5 1 \le t \le 10^{5} 1≤t≤105)
分析:
我们假设放三只蜜蜂进去,且让它们都不相邻但有一个格子与它们三个相邻。我们的蜜蜂为其打蜡的时候,会融合相邻的三条边,但打蜡之后会将未相邻三条边变为已打蜡。如下图所示:
我们可以观察到,初始时,我们已打蜡的格子的总周长为 s u m = 3 ∗ 6 = 18 sum = 3 * 6 = 18 sum=3∗6=18,我们将中间那个格子完成打蜡后,周长变化为 s u m − 3 + 3 ≡ s u m sum - 3 + 3 \equiv sum sum−3+3≡sum。即未发生改变。
同理,当四个已打蜡的蜂巢格子与一个未打蜡的格子相邻,那在将其打蜡的时候,会融合相邻的四条边,打蜡之后也仅仅只会将两条未相邻的边变为已打蜡,即周长变化为 s u m − 4 + 2 ≡ s u m − 2 sum - 4 + 2 \equiv sum - 2 sum−4+2≡sum−2。
于是我们可以得到,当蜜蜂对这中间未打蜡的格子打蜡后,相邻边从周长中剔除,然后最多想周边增加三条新边。因此改变后的周长要么不变,要么变小。我们的目标是将整个蜂巢完成打蜡,例如题目中图二:这个蜂巢的总周长为 30 30 30,所以我们一开始蜜蜂的巢穴至少也要是 30 30 30。这里我们不需要考虑蜜蜂放的位置,所以我们假设蜜蜂都不两两相邻,则一只蜜蜂对初识周长的贡献为 6 6 6,则总周长为 30 30 30的蜂巢,我们一开始需要放入 30 ÷ 6 = 5 30 \div 6 = 5 30÷6=5只蜜蜂即可完成对蜂巢的打蜡。
注意最后我们完成打蜡后还需要放入一只蜜蜂使其成为蜂王,让这个蜂巢延续下去。则 对于周长为 30 30 30的蜂巢,我们想让它延续下去,至少需要 6 6 6只蜜蜂。
现在只需要对
m
m
m个小六边形组成的大六变形求周长了。方法可能有很多,这里我介绍一种,众所周知 大六变形的周长由最外围的小六变形决定,如图:
我们可以看到最外围的小六边形最周长的贡献至少为 2 2 2,而在大六边形六个角的位置的小六边形的贡献均为 3 3 3,于是我们可以得到这样一个二元一次方程 2 x + 6 = p e r i m e t e r 2x + 6 = perimeter 2x+6=perimeter,其中 x x x 为当前大六变形最外围的小六变形个数。
于是我们又得当前大六变形最外围的小六变形个数,通过观察图形我们可知,以大六变形最中间的小六变形开始,六变形个数为 1 , 6 , 12 , 18 , … 1, 6, 12, 18, \dots 1,6,12,18,… 去掉第一个数我们发现这是一个等差数列,当然了,这题数据很强大,所以这个等差数列暴力不了。
我们可以推公式:设这个等差数列的项数为
x
x
x,则尾项为
6
x
6x
6x,即当前大六变形最外围的小六变形个数为
6
x
6x
6x,所以可以得到公式:
x
(
6
x
+
6
)
÷
2
+
1
=
m
⇒
6
x
2
+
6
x
=
m
−
1
x(6x + 6) \div 2 + 1= m \quad \Rightarrow \quad 6x^{2} + 6x = m - 1
x(6x+6)÷2+1=m⇒6x2+6x=m−1
关于这个公式我们二分求解即可,已知
7
≤
m
≤
1
0
18
7 \le m \le 10^{18}
7≤m≤1018,则令二分边界为
l
=
1
,
r
=
1
e
9
l = 1, r = 1e9
l=1,r=1e9可以得到正确结果,时间复杂度为
O
(
t
∗
l
o
g
1
0
9
)
O(t*log10^{9})
O(t∗log109)
AC代码:
#include <bits/stdc++.h>
using i64 = long long;
i64 n, m;
void solve()
{
std::cin >> n >> m;
m --;
i64 l = 0, r = 1e9;
while (l < r) {
i64 mid = (l + r + 1) >> 1;
i64 now = (6 * mid * mid + 6 * mid) / 2;
if (now <= m) l = mid;
else r = mid - 1;
}
i64 now = ((l * 6) - 6) / 6 + 2;
now = now * 6 * 2 - 6;
std::cout << n / ((now / 6) + 1) << '\n';
return ;
}
int main()
{
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
H、吃包子咯
题目大意:
你有 n n n个数和一个数字 m m m,每次你将重复如下操作知道不能再操作为止:
- 首先,从数字中选择两个数字。
- 然后,令你的答案加上 ( x y + y x ) (x^{y} + y ^{x}) (xy+yx) m o d mod mod m m m。
- 最后,选择两个数中的一个,删除它,并将另一个数字放回盒子中。
问:当操作到不能在操作时,你可以得到的答案最大是多少?
这里
(
x
y
+
y
x
)
(x^{y} + y ^{x})
(xy+yx)
m
o
d
mod
mod
m
m
m 为
(
x
y
+
y
x
)
(x^{y} + y ^{x})
(xy+yx) 除以
m
m
m 的余数,数据范围:
1
≤
n
≤
500
;
2
≤
m
≤
1
0
9
;
1
≤
a
i
≤
m
−
1
1 \le n \le 500; 2 \le m \le 10^{9}; 1 \le a_{i} \le m - 1
1≤n≤500;2≤m≤109;1≤ai≤m−1
分析:
首先我们注意到每次操作都会删除一个数字,则 n n n个数字必定操作 n − 1 n - 1 n−1次。假设我们将操作修改为:首先从盒子中选出来的 x x x 和 y y y 。
1. 令你的答案加上
(
x
y
+
y
x
)
(x^{y} + y ^{x})
(xy+yx)
m
o
d
mod
mod
m
m
m。
2. 删除
x
x
x,
y
y
y不放入盒子中,然后令当前的
y
y
y为
x
x
x,并在盒子中再选一个数为新的
y
y
y。
在这个操作中,我们除了第一个数字,其他每个数字至少会保留一次,假设现在我取 n n n个数是按编号顺序取的,上述操作见下图:
其中箭头左边为 x x x,右边为 y y y,箭头即为 ( x y + y x ) (x^{y} + y ^{x}) (xy+yx) m o d mod mod m m m,非顺序取数的话,如下图所示:
现在让我们回溯到题目本来的意思,在本题中,我们所删除的数一定是在之后的计算过程中对答案的贡献较小的那一个。我们如何判断呢?其实如上图,第一个数字和第三个数字并非不能一起运算,也就是说对于 n n n个数字来说,我们任意两个数字运算后对答案都有一定贡献,只不过是对答案贡献的大小不同,则其实对于所有数字而言,应是如下图所示(下图 n = 5 n = 5 n=5):
看上去错综复杂的,但其实也就一个意思,在我不知道当前这个 x x x, y y y谁对以后的答案贡献更大时,谁都有可能是被保留的那一个。观察上图我们可以发现这是一个完全图,因为我们 n n n个数都会用到,且一定会操作 n − 1 n - 1 n−1次,我们将操作次数看做边,则 我们需要做的就是将 n n n个点的完全图用 n − 1 n - 1 n−1条边连接起来,使其每个点都用得上。
于是我们很自然地想到可以用最小生成树的算法,这里是稠密图,则 用 P r i m Prim Prim 或 K r u s k a l Kruskal Kruskal算法最优。题目所求是最大值,所以我们只需要将最小生成树模版改为最大生成树即可AC。
注意:数据范围 2 ≤ m ≤ 1 0 9 ; 1 ≤ a i ≤ m − 1 2 \le m \le 10^{9}; 1 \le a_{i} \le m - 1 2≤m≤109;1≤ai≤m−1,对于式子 x y + y x x^{y} + y ^{x} xy+yx而言是会超出64位数据类型存储范围,也有超时的风险,这里我在题目中给出了快速幂,大家直接调用即可。
快幂:
long long qmi(long long qmi_now, long long qmi_k, long long qmi_mod)
{
long long qmi_res = 1 % qmi_mod;
while(qmi_k) {
if(qmi_k & 1) {
qmi_res = (qmi_res * qmi_now) % qmi_mod;
}
qmi_now = (long long)(qmi_now * qmi_now) % qmi_mod;
qmi_k /= 2;
}
return qmi_res % qmi_mod;
}
AC代码:
#include <bits/stdc++.h>
using i64 = long long;
const int N = 510;
i64 n, m;
i64 dis[N], ans;
i64 a[N], g[N][N];
std::vector<bool> st(N, false);
inline i64 qmi(i64 qmi_now, i64 qmi_k, i64 qmi_mod)
{
i64 qmi_res = 1;
while(qmi_k) {
if(qmi_k & 1) {
qmi_res = (qmi_res * qmi_now) % qmi_mod;
}
qmi_now = (i64)(qmi_now * qmi_now) % qmi_mod;
qmi_k >>= 1;
}
return qmi_res % qmi_mod;
}
bool prim() // prim 算法模版
{
for (int i = 0; i <= n + 1; i ++) dis[i] = -1;
for (int i = 0; i < n; i ++) {
int t = -1;
for (int j = 1; j <= n; j ++) {
if (!st[j] && (t == -1 || dis[t] < dis[j])) t = j;
}
if (i && dis[t] == -1) return false;
// 无解返回false,本题不可能出现这种情况
if (i) ans += dis[t];
for (int j = 1; j <= n; j ++) {
dis[j] = std::max(dis[j], g[t][j]);
} st[t] = true;
} return true;
}
void solve()
{
std::cin >> n >> m;
for (int i = 1; i <= n; i ++) std::cin >> a[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++) {
if (i == j) g[i][j] = -1;
else g[i][j] = (qmi(a[i], a[j], m) + qmi(a[j], a[i], m)) % m;
} // 邻接矩阵建图
} prim();
std::cout << ans << '\n';
return ;
}
int main(void)
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; // std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
I、自制华容道
题目大意:
起初,有 𝑘 张卡片堆叠在 ( 1 , 1 ) (1,1) (1,1)单元格中。每张卡片上都写着一个从 1 1 1到 k k k 的整数。更具体地说,在 ( 1 , 1 ) (1,1) (1,1)单元格中,从最上面起的第 i i i 张卡片上写着数字 a i a_{i} ai。我们保证没有两张卡片上写着相同的数字,换句话说,写在卡片上的数字是从 1 1 1到 k k k 的整数,卡片堆叠在 ( 1 , 1 ) (1,1) (1,1)单元格上形成了一个 1 1 1到 k k k 的排列。其他单元格是空的。
你需要将 k k k 张卡片移至 ( n , m ) (n, m) (n,m)单元格中,并以从大到小排列 k k k 张卡片,最上面那张为 1 1 1。即,假设 b i b_{i} bi为卡牌移值 ( n , m ) (n, m) (n,m)单元格后,从上往下的第 i i i 张卡片上面的数字,那么 b i b_{i} bi应该满足对于所有的 1 ≤ i ≤ k , b i = i 1 \le i \le k,b_{i} = i 1≤i≤k,bi=i。
在一次移动中,你可以从一个单元格中移除最上面的牌,并将它放到相邻的单元格中(两个单元格有一条公共边我们称之为相邻)。如果目标格子中已经有了一张牌或多张牌,则将你移动的牌放在牌堆的顶部。每个操作必须满足以下限制条件:
- 除了 ( 1 , 1 ) (1, 1) (1,1)和 ( n , m ) (n, m) (n,m)格子之外,其他所有单元格都不能有一张以上的牌。
- 你不能将牌移动到 ( 1 , 1 ) (1, 1) (1,1)格子,也就是说,对于 ( 1 , 1 ) (1, 1) (1,1)单元格,我们的牌只出不进。
- 不能从 ( n , m ) (n, m) (n,m)格子移动纸牌,也就是说,对于 ( n , m ) (n, m) (n,m)单元格,我们的牌只进不出。
现在请你根据 n , m , k n,m,k n,m,k和数组 a i a_{i} ai的值,判断我们能否将牌全部移动到 ( n , m ) (n, m) (n,m)并且在 ( n , m ) (n, m) (n,m)中的牌从上往下读是严格递增的。 ( 3 ≤ n , m ≤ 1 0 6 , n m ≤ 1 0 6 , 1 ≤ k ≤ 1 0 5 ) (3 \le n, m \le 10^{6}, nm \le 10^{6}, 1 \le k \le 10^{5}) (3≤n,m≤106,nm≤106,1≤k≤105)
分析:
玩过 W i n d o w s 7 Windows7 Windows7桌面小游戏华容道的都知道,对于 4 × 4 4\times4 4×4的格子,但里面只有 15 15 15个数字,有一个位置是空的,这样以来,我们不考虑还原华容道的情况下,我们将一个数字移动到我们想要他到达的任何一个地方。
有了这样一个性质我们可以推出,当我棋盘上还有一个空位置,我就可以将正确顺序的牌移动到 ( n , m ) (n, m) (n,m)格子。因为我们的 ( 1 , 1 ) (1, 1) (1,1)格子只出不进, ( n , m ) (n, m) (n,m)格子只进不出,所以这两个格子要排除。即对于 n ∗ m n*m n∗m的棋盘上,我们从 ( 1 , 1 ) (1, 1) (1,1)移出的牌的个数之要小于等于 n ∗ m − 3 n * m - 3 n∗m−3我们就可以将这移出的牌按照规定顺序移动到 ( n , m ) (n, m) (n,m)格子。
下图为模拟过程其中
n
=
m
=
3
,
k
=
6
n = m = 3, k = 6
n=m=3,k=6:
因为要求移动到 ( n , m ) (n, m) (n,m)中的牌从上往下读是严格递增的。即 移动到 ( n , m ) (n, m) (n,m)的牌顺序应为 n , n − 1 , n − 2 , ⋯ , 3 , 2 , 1 n, n - 1, n - 2, \cdots, 3, 2, 1 n,n−1,n−2,⋯,3,2,1。首先我们可以利用队列,将 ( 1 , 1 ) (1, 1) (1,1)牌顶中的牌都移出到队列中,若当前牌 x x x满足移动到 ( n , m ) (n, m) (n,m)的条件,则我们将它移到 ( n , m ) (n, m) (n,m)格子中。即 弹出队列,这里我们每次要弹出的应该是队列中最大的数字,但单纯的队列是不肯办到的,于是我们在队列中加一个二叉树,即可以完成在 O ( l o g n ) O(log n) O(logn)的时间复杂度之内找到队列中的最大值并将它与队列队头交换后成功弹出。(这里的 n n n为当前队列中的元素个数)。
当然了,C++选手可以直接使用priority_queue容器,这个本身就是一个大根堆。(队列 + + + 二叉树 = = = 堆,大根堆小根堆根据自己写的二叉树来决定)
AC代码:
数据结构手动模拟大根堆:
#include <stdio.h>
#include <iostream>
const int N = 1e6 + 10;
int n, m, k;
struct priority_queue {
int size = 0, val[N] = {0};
} pq;
void swap (int &o1, int &o2)
{
int now = o1;
o1 = o2; o2 = now;
}
void priority_queue_push(int idx)
{
int u = idx;
if(idx / 2 > 0 && pq.val[idx] > pq.val[idx / 2]) u = idx / 2;
if(u != idx){
swap(pq.val[u], pq.val[idx]);
priority_queue_push(u);
}
return ;
}
int priority_queue_top()
{
return pq.val[1];
}
void priority_queue_pop(int idx)
{
int u = idx;
if(2 * idx <= pq.size && pq.val[2 * idx] > pq.val[u]) u = 2 * idx;
if(2 * idx + 1 <= pq.size && pq.val[2 * idx + 1] > pq.val[u]) u = 2 * idx + 1;
if(u != idx){
swap(pq.val[idx], pq.val[u]);
priority_queue_pop(u);
}
}
void solve()
{
std::cin >> n >> m >> k;
pq.size = 0;
bool isFlag = true;
int maxv = n * m - 3, st = k;
for (int i = 1; i <= k; i ++) {
int now; std::cin >> now;
pq.val[++ pq.size] = now;
priority_queue_push(pq.size);
if (pq.size > maxv) isFlag = false;
while (pq.size && priority_queue_top() == st) {
swap(pq.val[1], pq.val[pq.size]);
pq.size --;
st --;
priority_queue_pop(1);
}
}
std::cout << (isFlag ? "YES" : "NO") << '\n';
return ;
}
int main()
{
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
C++容器:
#include <bits/stdc++.h>
typedef long long ll;
typedef pair<int, int> pii;
const int N = 2e5 + 10;
int n, m, k;
void solve()
{
std::cin >> n >> m >> k;
std::priority_queue<int> pq; // 这就是一个大根堆
int st = k, mx = n * m - 3, ans = 1;
for (int i = 1; i <= k; i ++)
{
int xx; std::cin >> xx;
pq.push(xx);
if (pq.size() > mx) ans = 0;
while (!pq.empty() && pq.top() == st)
{
pq.pop();
st --;
}
}
std::cout << (ans ? "YES" : "NO") << '\n';
return ;
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int t = 1; cin >> t;
while (t --)
solve();
return 0;
}
J、反方向的钟
题目大意:
你有 n n n 个两个相关联的线段, [ l i , r i ] , [ a i , b i ] [l_{i}, r_{i}], [a_{i}, b_{i}] [li,ri],[ai,bi],其中 [ l i , r i ] ⊇ [ a i , b i ] [l_{i}, r_{i}] \supseteq [a_{i}, b_{i}] [li,ri]⊇[ai,bi],即 [ a i , b i ] [a_{i}, b_{i}] [ai,bi]段包含在 [ l i , r i ] [l_{i}, r_{i}] [li,ri]。
当你在 [ l i , r i ] [l_{i}, r_{i}] [li,ri]上时,你可以传送到 [ a i , b i ] [a_{i}, b_{i}] [ai,bi]上任意位置,我们在线段上是不可移动的,只能通过传送来改变位置。
现在你有 q q q个数代表你的初始位置,问你对于每个初始位置最远能传送到的坐标。
分析:
由题可知,当我们在线段
[
l
i
,
r
i
]
[l_{i}, r_{i}]
[li,ri]上时,我们才能传送到
[
a
i
,
b
i
]
[a_{i}, b_{i}]
[ai,bi],如图
当我们在点
1
1
1时,我们最远可以到达点
8
8
8,而当我们在点
10
10
10时,由于我们无法移动,并且线段
[
a
i
,
b
i
]
[a_{i}, b_{i}]
[ai,bi]比我当前位置还要小,所以不动即是最远的距离。
而上图中,当我们在点
10
10
10时,因为
10
∈
[
l
2
,
r
2
]
10 \in [l_{2}, r_{2}]
10∈[l2,r2],所以我们可以传送到点
16
16
16。
那我们想一想,上图与下图对于答案有何区别:
结果是 没有任何区别,设我们当前的位置为
x
x
x,当
l
i
≤
x
≤
b
i
l_{i} \le x \le b_{i}
li≤x≤bi时,我们可以通过传送们,传送到点
b
i
b_{i}
bi上,这对于我们当前来当个线段来说,已经是最远的地方了;而当
b
i
<
x
≤
r
i
b_{i} < x \le r_{i}
bi<x≤ri时,由于传送门会将我往后传送,所以我当前的位置就已经是最远的地方了,除非它也属于其他
[
l
,
r
]
[l, r]
[l,r]。于是我们知道了对于一个
[
l
i
,
r
i
]
,
[
a
i
,
b
i
]
,
[
l
i
,
r
i
]
⊇
[
a
i
,
b
i
]
[l_{i}, r_{i}], [a_{i}, b_{i}],[l_{i}, r_{i}] \supseteq [a_{i}, b_{i}]
[li,ri],[ai,bi],[li,ri]⊇[ai,bi],它的作用域其实只有
[
l
i
,
b
i
]
[l_{i}, b_{i}]
[li,bi]
然后我们再考虑,如果当前位置无法传送或在我在传送后,当前位置任属于其他线段,把我可以重复操作,则可以得到,上图与下图是没有任何区别的:
因为不论我属于那这个区间中的哪个点。我都可以一直传送下去直到到达点
17
17
17,所以我们对于
n
n
n 个
[
l
i
,
b
i
]
[l_{i}, b_{i}]
[li,bi]按照左段点为主要依据从小到大排序,右段点为次要依据。然后只要首尾相接,我们即可将它们连起来。然后面对
q
q
q个位置,我们只需要输出它所在的线段的最右段点,这就是它所能到达最远的地方,若当前位置不属于任何一个线段,那它本身就是最远的地方。
AC代码:
#include <bits/stdc++.h>
#define _1 first
#define _2 second
#define endl std::cout << '\n'
using i64 = long long;
i64 n, m;
void solve()
{
std::cin >> n;
std::vector<std::pair<int, int>> arr, rec;
for (int i = 1; i <= n; i ++) {
int l, r, a, b; std::cin >> l >> r >> a >> b;
arr.push_back({l, b});
}
std::sort(arr.begin(), arr.end(), [&] (std::pair<int, int> x, std::pair<int, int> y) {
if (x._1 != y._1) return x._1 < y._1;
return x._2 > y._2;
});
// 内置函数排序
rec.push_back({arr[0]._1, arr[0]._2});
for (int i = 1; i < arr.size(); i ++) {
int idx = rec.size() - 1;
if (arr[i]._1 <= rec[idx]._2) {
rec[idx]._2 = std::max(rec[idx]._2, arr[i]._2);
}
else rec.push_back({arr[i]._1, arr[i]._2});
}
std::cin >> m;
while (m --) {
int xx; std::cin >> xx;
int l = 0, r = rec.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (rec[mid]._2 >= xx) {
if (rec[mid]._1 <= xx) l = r = mid;
else r = mid;
}
else l = mid + 1;
}
if (rec[l]._1 <= xx && xx <= rec[l]._2) std::cout << rec[l]._2 << ' ';
else std::cout << xx << ' ';
} endl;
return ;
}
int main(void)
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
K、小杜的猜想
题目大意:
在一次806的卫生打扫时,Duiaao负责拖地,因为天气炎热干燥的缘故,海绵拖把变得很硬,这就需要用水给它浸湿。水浸湿海绵拖把是一个漫长的过程,Duiaao不愿意等待,于是他将水龙头的水开到最大就回了806,过了一段时间Duiaao再次去洗手间时,发现水已经从水缸里溢出来了,但拖把依旧没有浸湿,于是这次他将水龙头关了之后又走了。可当Duiaao再次回去的时候,水已经全都流走了,拖把依然没完全浸湿,他发现水缸出水口的阀门是关不紧的,也就是说不管如何,水都会从水缸溜走。
Duiaao观察到只有当水填满水缸的时候,拖把才会完全浸泡在水里,而水流出水缸的速度是由水缸到下水道之间一系列的管道而决定的,粗细不同的水管流水量也不同。所以Duiaao猜测,若是知道了这些水管的流水量以及连接方式。就一定存在一种水龙头的出水量。使得将水缸里的水填满之后,将水龙头的出水量调至这个值,让水缸的水位永远保持不变。
我们假设这些水缸阀门和下水道之间是由 n n n 个水管连接器和 𝑚 根水管组成。其中 s s s 为水缸, t t t 为下水道,水管连接器并不存储水且水不可能回流,也就是说当前水管与下一个水管是直接连接的而且存在的话一定只存在一条单向边,水缸可能会有多个阀门,即水缸可以由多条水管与其他流水转发处相连接。
现在Duiaao将这些水管的连接方式以及流水量告诉你,请你帮他找到水龙头出水里的值,当然 我们不能保证Duiaao得到的水管连接图是正确的,所以当水缸中的水无法流向下水道时,输出 − 1 -1 −1。
分析:
由题可知,不同粗细的水管容量是不同的。我们可以试想一下,在水缸是满水的情况下,为什么过了一段时间水位会下降,又为什么会溢出。
当这些连接到下水道的流量大于从水缸流向其他其他水管的时候,水位会下降,反之上升。当然这不仅仅取决于直接连向水缸和下水道的水管。中间错综复杂的水管也会影响最终流向下水道的水量。
如图,假设我们水龙头的出水量是无穷大的,即水缸里有无穷多的水,此时水缸里的水流向点
1
1
1是每秒钟
50
50
50个单位,而水管连接器
1
1
1流向点
2
2
2因为水管很细,所以每秒钟只有
10
10
10个单位。此时水管连接器
2
2
2每秒钟接收
10
10
10个单位的水,处理
50
50
50个单位的水,但是由于水不够,所以每秒钟只会处理
10
10
10个单位的水到下水道。由于题目所说,水管连接器并不存储水。所以在水管连接器
1
1
1处理不过来水管水缸流过来的水时,水位就会上升。
对于本题目:源点 + 汇点 + 边 + 结点 + 容量有限 = 网络最大流,直接可以使用最大流算法秒杀。由于篇幅限制,这里便不多介绍最大流算法原理,感兴趣的可以私想我,之前写过一个网络流的算法 p p t ppt ppt可以给你们看一下。
AC代码:
#include <bits/stdc++.h>
using i64 = long long;
const int N = 210, M = 1e4 + 10;
const i64 INF = 1e18;
int n, m;
i64 val[M];
int h[N], ne[M], e[M], tot;
int gap[N], deep[N], cur[N], s, t;
inline void add (int u, int v, int w) {
e[tot] = v; val[tot] = w; ne[tot] = h[u]; h[u] = tot ++;
e[tot] = u; val[tot] = 0; ne[tot] = h[v]; h[v] = tot ++;
}
void init() {
deep[t] = 1;
++ gap[deep[t]];
for (int i = 1; i <= n; i ++) {
cur[i] = h[i];
}
std::queue<int> pq;
pq.push(t);
while (pq.size()) {
int u = pq.front(); pq.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!deep[v]) {
deep[v] = deep[u] + 1;
++ gap[deep[v]];
pq.push(v);
}
}
}
return ;
}
i64 dfs (int u, i64 mi) {
if (u == t) return mi;
i64 flow = 0;
for (int &i = cur[u]; ~i; i = ne[i]) {
int v = e[i];
if (deep[u] == deep[v] + 1) {
i64 k = dfs(v, std::min(mi, val[i]));
flow += k; mi -= k;
val[i] -= k; val[i ^ 1] += k;
if (!mi) return flow;
}
}
if (!(-- gap[deep[u]])) deep[s] = n + 1;
++ deep[u]; ++ gap[deep[u]];
cur[u] = h[u];
return flow;
}
i64 isap() {
init();
i64 res = dfs(s, INF);
while (deep[s] <= n) res += dfs(s, INF);
return res ;
}
void solve()
{
std::cin >> n >> m >> s >> t;
std::memset (h, -1, sizeof h);
for (int i = 1; i <= m; i ++) {
int u, v, w; std::cin >> u >> v >> w;
add(u, v, w);
}
i64 ans = isap();
std::cout << (ans ? ans : -1) << '\n';
return ;
}
int main()
{
std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
int _ = 1; // std::cin >> (_);
while (_ --) {
solve();
}
return 0;
}
结语
生命不息,奋斗不止,致有目标并愿意为之奋斗终身的学弟学妹们:
「人在做自己喜欢的事情的时候,眼神里有光」,这大概说的就是那些被 ACM 虐了千百遍却依然热爱,并且付出的选手。
当我问及我的队友和那些在 World Finals 上拿勇夺金牌的大神差距在哪时,「比我聪明」成为频率出现最高的回答。虽然「比我聪明的人比我还努力」是一句很丧的话,但是他们却从来没有用丧来结束自己所热爱的竞赛。
「千淘万漉虽辛苦,吹尽狂沙始到金」,ACM这条道路检验任何没有走心的努力。我们都不是天赋异禀的人,在茫茫人海中甚至会有些平庸,可是我们的人生不仅仅是潦草诗,待迷雾散尽后,天光大亮,我们一定会看清远方的灯塔。接受普通的自己,但不放任自己的普通,走到山穷水尽处自见到耀眼的自己。
人和人之间的确是有差距的,但这些智商上的差距从来没有成为ACMer们在现有的功勋章上打瞌睡的理由。自驱力、行动力和高标准贯穿每一个ACMer整个学生到职场的生涯,世界冠军也只是得到了他们应有的回报。
「平芜尽处是春山,追风赶月莫停留」,不管这次校赛结局怎样,不管今后你们今后是否会热爱并继续为ACM竞赛奋斗,也不管你们在几年后的面临人生岔路口时会做出怎样的选择,小杜都希望你们可以一直努力下去,我们真正重要的只是要创造一个属于自己的充满意义的人生。
一切不辜负自己,剩下的事就顺其自然就好了呗。