CY的个人博客

记录生活

Markdown 数学公式语法指南

Markdown 中使用 LaTeX 语法来编写数学公式.

常见运算符

基本算术运算符

运算符 LaTeX 语法 示例 效果
加号 + $a + b$
减号 - $a - b$
乘号 \times $a \times b$
点乘 \cdot $a \cdot b$
除号 \div $a \div b$
分数 \frac{a}{b} $\frac{a}{b}$
平方根 \sqrt{x} $\sqrt{x}$
n次方根 \sqrt[n]{x} $\sqrt[3]{x}$
对数 \log $\log x$
自然对数 \ln $\ln x$
以a为底对数 \log_a b $\log_a b$

取模运算

Markdown/LaTeX 中有多种方式表示取模运算:

运算符 LaTeX 语法 示例 效果 说明
取模 \bmod $a \bmod b$ 二元取模运算符
同余 \pmod $a \equiv b \pmod{n}$ 模 n 同余
同余 \mod $a \equiv b \mod n$ 同余关系
函数形式 \operatorname{mod} $\operatorname{mod}(a, b)$ 函数形式

逻辑运算符

运算符 LaTeX 语法 示例 效果
\land\wedge $a \land b$
\lor\vee $a \lor b$
\lnot\neg $\lnot a$
异或 \oplus $a \oplus b$
同或 \odot $a \odot b$
蕴含 \rightarrow\implies $a \rightarrow b$
等价 \leftrightarrow\iff $a \leftrightarrow b$
对于所有 \forall $\forall x \in \mathbb{R}$
存在 \exists $\exists x \in \mathbb{R}$

上下标

类型 LaTeX 语法 示例 效果
上标 ^ $x^2$
下标 _ $x_1$
组合 ^{}_{} $x^{2}_{1}$

关系运算符

运算符 LaTeX 语法 示例 效果
等于 = $a = b$
不等于 \neq $a \neq b$
约等于 \approx $a \approx b$
大于 > $a > b$
小于 < $a < b$
大于等于 \geq $a \geq b$
小于等于 \leq $a \leq b$
正比于 \propto $a \propto b$

求和、积分、极限

运算符 LaTeX 语法 示例 效果
求和 \sum $\sum_{i=1}^{n} i$
积分 \int $\int_{a}^{b} f(x)dx$
极限 \lim $\lim_{x \to \infty} f(x)$
乘积 \prod $\prod_{i=1}^{n} i$

希腊字母

字母 LaTeX 语法 示例 效果
α \alpha $\alpha$
β \beta $\beta$
γ \gamma $\gamma$
Δ \Delta $\Delta$
π \pi $\pi$
θ \theta $\theta$
μ \mu $\mu$
σ \sigma $\sigma$
ω \omega $\omega$

集合运算符

运算符 LaTeX 语法 示例 效果
属于 \in $a \in A$
不属于 \notin $a \notin A$
子集 \subset $A \subset B$
真子集 \subseteq $A \subseteq B$
并集 \cup $A \cup B$
交集 \cap $A \cap B$
空集 \emptyset $\emptyset$

函数表达式

常用数学函数

函数 LaTeX 语法 示例 效果
正弦 \sin $\sin x$
余弦 \cos $\cos x$
正切 \tan $\tan x$
指数 \exp $\exp(x)$
最大值 \max $\max(a, b)$
最小值 \min $\min(a, b)$

矩阵和行列式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$$
\begin{matrix}
a & b \\
c & d
\end{matrix}
$$

$$
\begin{pmatrix}
a & b \\
c & d
\end{pmatrix}
$$

$$
\begin{vmatrix}
a & b \\
c & d
\end{vmatrix}
$$

效果:

括号和定界符

类型 LaTeX 语法 示例 效果
圆括号 () $(a+b)$
方括号 [] $[a+b]$
花括号 \{ \} $\{a+b\} ${a+b}$
自适应括号 \left( \right) $\left(\frac{a}{b}\right)$

P1226 【模板】快速幂

题目描述

给你三个整数 $a,b,p$,求 $a^b \bmod p$。

输入格式

输入只有一行三个整数,分别代表 $a,b,p$。

输出格式

输出一行一个字符串 a^b mod p=s,其中 $a,b,p$ 分别为题目给定的值, $s$ 为运算结果。

输入输出样例 #1

输入 #1

1
2 10 9

输出 #1

1
2^10 mod 9=7

说明/提示

样例解释

$2^{10} = 1024$,$1024 \bmod 9 = 7$。

数据规模与约定

对于 $100\%$ 的数据,保证 $0\le a,b < 2^{31}$,$a+b>0$,$2 \leq p \lt 2^{31}$。

思路分析 + 代码实现

对于指数 exp ,我们通过 右移(>>) 运算符,遍历其在二进制表示下的每一位,用 按位与(&) 运算符取出 exp 在二进制表示下的最低位。在循环到第 i 次时,变量 base 中存储的是 $base^{2^i}$ ,若 exp 该位为 1 ,则把此时的变量 base 累乘到 result 中。代码如下:

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;
// 计算 base 的 exponent 次方,结果对 mod 取模
long long power(long long base, long long exp, const long long mod) {
if (exp == 0) return 1;
long long result = 1;
while (exp) { // 每次处理 exp 的二进制最低位,然后右移 1 位
if (exp & 1) // 若 exp 的二进制最低位为 1
result = result * base % mod; // 相当于将这一位二进制位变为 1
base = base * base % mod; // base 平方,相当于指数左移 1 位
exp >>= 1; // 处理下一个二进制位
}
return result;
}
int main() {
long long a, b, p;
cin >> a >> b >> p;
cout << a << "^" << b << " mod " << p << "=" << power(a,b,p);
return 0;
}

UVA11572 唯一的雪花 Unique Snowflakes

题目描述

企业家 Emily 有一个很酷的主意:把雪花包起来卖。她发明了一台机器,这台机器可以捕捉飘落的雪花,并把它们一片一片打包进一个包裹里。一旦这个包裹满了,它就会被封上送去发售。

Emily 的公司的口号是“把独特打包起来”,为了实现这一诺言,一个包裹里不能有两片一样的雪花。不幸的是,这并不容易做到,因为实际上通过机器的雪花中有很多是相同的。Emily 想知道这样一个不包含两片一样的雪花的包裹最大能有多大,她可以在任何时候启动机器,但是一旦机器启动了,直到包裹被封上为止,所有通过机器的雪花都必须被打包进这个包裹里,当然,包裹可以在任何时候被封上。

输入格式

第一行是测试数据组数 $T$,对于每一组数据,第一行是通过机器的雪花总数 $n$($n \le {10}^6$),下面 $n$ 行每行一个在 $[0, {10}^9]$ 内的整数,标记了这片雪花,当两片雪花标记相同时,这两片雪花是一样的。

输出格式

对于每一组数据,输出最大包裹的大小。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
7
1
5
1
2
3
2
1

输出 #1

1
3

思路分析 + 代码实现

相当于求一个序列的一个最长子序列,使得没有重复数字。注意到数据范围最大为 $Tn$ (虽然时限2秒),说明需要一个 $\operatorname{O}(n)$ 或者 $\operatorname{O}(n\log n)$ 的算法。那么选择用双指针法,先固定 $l = 0$ ,然后让 $r$ 递增,直至出现重复,此时更新答案,然后令 $l$ 自增,删掉区间最左边的数(即引起重复的数)。不断重复上述过程,得到答案。

那么怎么高效记录数字是否重复呢?用 map、set 或者用哈希?但这无疑使得时间复杂度大大增加。直接使用布尔数组标记的话,则需要用 $10^9 \div 1024^2 \approx 954 MB$ 。因此我们用 bitset ,内存开销降低为原来的 $\frac{1}{8}$ 。要注意内层 while 循环的循环条件为 $l$ < $n$ ,这是为了重置 $vis$ 数组,保证在下一组测试用例时初始化正确,代码如下:

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
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e9 + 1, MAXM = 1e6 + 1;
int nums[MAXM];
int T, n, l, r, ans;
bitset<MAXN> vis;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
l = r = ans =0;
if (n <= 0) continue;
for(int i = 0; i < n; i++)
cin >> nums[i];
while (l < n) { // 注意
while (r < n && !vis[nums[r]])
vis.set(nums[r++]);
ans = max(ans, r - l);
vis.reset(nums[l++]);
}
cout << ans << "\n";
}
return 0;
}

P10446 64位整数乘法

题目描述

求 $a$ 乘 $b$ 对 $p$ 取模的值。

输入格式

第一行输入整数 $a$,第二行输入整数 $b$,第三行输入整数 $p$。

输出格式

输出一个整数,表示 a*b mod p 的值。

输入输出样例 #1

输入 #1

1
2
3
3
4
5

输出 #1

1
2

说明/提示

$1 \le a,b,p \le 10^{18}$

思路分析 + 代码实现

注意到数据范围 $1 \le a,b,p \le 10^{18}$ ,这说明什么?结合 long long 的表示范围 $2^{63}-1 > 2 \times 10^{18}$ 可知,我们可以把 $a \times b$ 分解成 $a \times [\frac{b}{2} + \frac{b}{2} + (b \bmod 2)]$ 。因此我们可以写一个递归函数,结束递归的条件是 $b \leq 2$。复杂度为 $\operatorname{O}(nlogn)$ 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<bits/stdc++.h>
using namespace std;
long long solve(long long a, long long b, const long long mod) {
if (b <= 2) return a * b % mod;
if (b & 1) return (2 * solve(a, b >> 1, mod) % mod + a) % mod;
return solve(a, b >> 1, mod) * 2 % mod;
}
int main() {
long long a, b, p;
cin >> a >> b >> p;
cout << solve(a, b, p);
return 0;
}

P14576 Lamborghini (Remix)

题目描述

光标是一种可以出现在一行代码的相邻两个字符之间,或者某行代码末尾,或者某行代码开头的标识。

在现代的代码编辑器中,我们通常可以同时放置许多光标。

当按下左方向键时,所有光标都会各自同时向左移动一个字符。特别地,在当前行开头的移动至上一行末尾,在第一行开头的光标消失。

当按下右方向键时,所有光标都会各自同时向右移动一个字符。特别地,在当前行末尾的移动至下一行开头,在最后一行末尾的光标消失。


现在张均好有一份 $n$ 行的代码,第 $i$ 行代码的长度为 $a_i$(即一个长度为 $a_i$ 的字符串)。张均好想知道,如果他选择一些行的末尾放置光标,再通过若干次按方向键,最多能使同一行包含多少个光标,注意某一行的开头和末尾也属于这一行。

你只需输出这个最大值。

输入格式

本题包含多组测试。

每个测试点第一行一个整数 $T$,表示测试数据组数。

对于每组测试数据:

第一行一个整数 $n$,表示代码行数。

接下来一行 $n$ 个用空格隔开的整数,表示第 $i$ 行的代码长度为 $a_i$。

输出格式

对于每组测试数据,输出一行一个整数,表示答案。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
7
3
7
24 20 20 12 6 6 22
10
10 7 2 3 5 6 4 13 21 30
5
2 3 4 5 6

输出 #1

1
2
3
3
6
2

说明/提示

样例 1 解释

这个样例描述了如下的代码:

1
2
3
4
5
6
7
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int pi=3.14;
int a;
int b;
#define ld long double

显然,每行代码的长度分别为 $24,20,20,12,6,6,22$。

张均好可以在第 $4,5,6$ 行末尾放置光标:

通过不断按下左方向键,这三个光标可以同时出现在第二行:

也可以通过不断按下右方向键,使得这三个光标同时出现在第七行:

可以证明,不存在使得更多光标出现在同一行的方案。

数据范围

对于 $100\%$ 的数据,$1\le T\le 10$,$1\le n\le 2\times 10^5$,$1\le a_i\le 10^9$。

子任务 $n\le$ 特殊性质 分数
Subtask 1 $20$ $20$
Subtask 2 $200$ $20$
Subtask 3 $5000$ $20$
Subtask 4 $2\times 10^4$ $a_i\le 1000$ $20$
Subtask 5 $2\times 10^5$ $20$

思路分析 + 代码实现

我们希望有尽可能多的光标同时出现在一行内,那么这一行需要尽量长,所以我们找最长的一行作为我们最后放置这些光标的位置。现在的问题是,在这一行最多能同时放下几个光标呢?

我们定义两个光标的距离为一个光标到另一个光标所在位置所需要的最少移动次数。显然,同时移动所有光标是不会改变两个光标之间的距离的。在这种情况下,不难发现在长度为 $a$ 的一行中能放下 $n$ 个光标的必要条件是第 $1$ 个光标到第 $n$ 个光标的距离不大于 $a$

同时我们发现,第 $i$ 行与第 $i+1$ 行的行末光标的距离为 $1+a_{i+1}$ (因为换一行需要一次移动)。所以,如果第 $i-1$ 行和第 $i+1$ 行的行末光标都能放入最长的那一行,那么第 $i$ 行的自然也行,即我们统计的答案中的光标初始时是在一段连续的行末的。

这样,问题就转化为了,对于 $a_{max}$ ,找出最大的一段连续的行末光标,使得第 $1$ 个光标到第 $n$ 个光标的距离不大于 $a$ ,此时的 $n$ 就是答案。可以用前缀和 + 双指针法实现,时间复杂度为 $O(nT)$ ,代码如下:

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;
typedef long long LL;
constexpr LL MAXN = 2e5 + 1;
LL a[MAXN], b[MAXN];
LL sum(LL l, LL r) { // a_l 不用计入
return b[r] - b[l];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
LL T, ans, max_line, n;
cin >> T;
while (T--) {
cin >> n;
max_line = 0;
ans = 1;
for(LL i = 1; i <= n; i++) {
cin >> a[i];
b[i] = 1 + a[i] + b[i - 1]; // 每次换行会多 1
max_line = max(max_line, a[i]);
}
LL l = 1, r = 1;
while (r <= n) {
while (l < r && sum(l, r) > max_line)
l++;
while (r <= n && sum(l, r) <= max_line)
r++;
ans = max(ans, r - l);
}
cout << ans << "\n";
}
return 0;
}

P14575 坤班

题目描述

传奇颜值中学开始招生了!

学校里面有 $n$ 个老师,需要教授 $m$ 个学科。每一个老师可以用一个三元组 $(a_i,b_i,c_i)$ 表示其教 $a_i$ 这个学科,最多能教 $b_i$ 个班级。若 $c_i = 0$ 表示老师 $i$ 不愿意当班主任,反之表示其愿意当班主任。

特别的,因为担任班主任将会消耗大量的精力,所以如果一个老师 $i$ 选择担任班主任,他就最多只能教授 $b_i - 1$ 个班级。

当然,每一个班必须有一个班主任,每一个学科必须有一名老师教授。需要注意的是一个班主任并不必须担任他所对应班级的科任老师。

学校希望能组建更多的班级,以招到更多优秀的 OIer,招生组特邀作为传奇特级大师的你来协助计算出能够组建出最多的班级数量。

输入格式

第一行读入两个正整数 $n,m$,分别表示传奇颜值中学中老师的数量和学科的数量。

接下来 $n$,每行包含三个整数 $a_i,b_i,c_i$,其含义见题目描述。

输出格式

输出共一行,表示传奇颜值中学能组建出最多的班级数量。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
5 2
1 2 1
1 2 1
1 1 0
2 2 1
2 2 1

输出 #1

1
3

说明/提示

样例解释

编号为 $1,2,4$ 的老师分别担任三个班的班主任。

编号为 $1,2,3$ 的老师分别教授三个班的学科 1。

编号为 $4$ 的老师教授一个班的学科 2,编号为 $5$ 的老师教授两个班的学科 2。

数据范围

子任务 $n \leq $ 特殊性质 分值
Subtask 1 $5 \times 10^5$ A $5$
Subtask 2 $20$ $15$
Subtask 3 $3 \times 10^3$ B $20$
Subtask 4 $3 \times 10^3$ $20$
Subtask 5 $5 \times 10^5$ B $20$
Subtask 6 $5 \times 10^5$ $20$
  • 特殊性质 A:满足 $\forall i \in [1,n],c_i = 0$。
  • 特殊性质 B:满足 $\forall i \in [1,n],b_i = 1$。

对于 $100\%$ 的数据满足:

  • $m \leq n$。
  • $\forall i \in [1,n]$,$1 \leq a_i \leq m$,$1 \leq b_i \leq n$,$c_i \in {0,1}$。

思路分析 + 代码实现

一开始的想法:我们要想组建出尽可能多的班级,肯定要贪心一点。具体怎么贪心呢?首先,对于不愿意当班主任的老师,我们自然是狠狠压榨让他们多干活,即安排满ta的教学班级。愿意当班主任的老师呢,自然是比较珍贵,不能随意安排,要好好考虑。

但是,分析到这里,我们发现还是很难直接判断出能组建出多少班级。换个思路,判断能否组建出 $k$ 个班级相对容易一些。具体怎么判断呢?

用数组 $head$ 记录每个学科班主任的数量,用数组 $subject$ 记录每个学科的教学能力(含班主任)。对每个 $k$ 先让所有老师都不当班主任,狠狠上课。如果这样都有学科凑不够老师,那么说明肯定组不出 $k$ 个班。如果凑得出,那么就要考虑班主任,用变量 $cnt$ 记录可用的班主任数量,对每个学科来说,多出 $subject[i] - mid$ 个老师,如果 $head[i]$ 足够(不小于这个数)就分配 $subject[i] - mid$ 个班主任,否则只能分 $head[i]$ 个。最后检查班主任够不够 $k$ 个即可。

再加上这道题答案具有“单调性”,因此我们采取二分答案的做法,找出满足条件的最大的 $k$ 即为答案,时间复杂度为 $O(n\operatorname{log}(n_{max}))$ ,代码如下:

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
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr LL MAXN = 5e5 + 2;
// 刚做这道题目只想到模拟,超时了,写个快读玩玩 QWQ
LL read() {
LL ch = getchar(), result = 0;
while (!(ch >= '0' && ch <= '9'))
ch = getchar();
while (ch >= '0' && ch <= '9') {
result = result * 10 + (ch - '0');
ch = getchar();
}
return result;
}
LL head[MAXN]; // 每个学科班主任的数量
LL subject[MAXN]; // 每个学科的教学能力(含班主任)
int main() {
LL n, m, a, b, c, ans = 0;
n = read(); m = read();
for(LL i = 0; i < n; i++) {
a = read(); b = read(); c = read();
if (c) head[a]++;
subject[a] += b;
}
LL l = 0, r = MAXN * MAXN;
while (l <= r) {
LL mid = (l + r) / 2;
LL cnt = 0; // 计算可用班主任的数量
bool valid = true;
for(int i = 1; i <= m; i++) { // 遍历学科
if (subject[i] < mid) { // 老师不够
valid = false;
break;
}
cnt += min(subject[i] - mid, head[i]);
}
if (cnt < mid) // 班主任不够
valid = false;
if (valid) {
ans = mid;
l = mid + 1;
}
else r = mid - 1;
}
cout << ans;
return 0;
}

P3853 [TJOI2007] 路标设置

题目背景

B 市和 T 市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。

题目描述

现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。

输入格式

第 $1$ 行包括三个数 $L,N,K$,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。

第 $2$ 行包括递增排列的 $N$ 个整数,分别表示原有的 $N$ 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 $[0,L]$ 内。

输出格式

输出 $1$ 行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。

输入输出样例 #1

输入 #1

1
2
101 2 1
0 101

输出 #1

1
51

说明/提示

公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点 $50$ 或 $51$ 个单位距离处,这样能达到最小的空旷指数 $51$。

$50\%$ 的数据中,$2 \leq N \leq 100$,$0 \leq K \leq 100$。

$100\%$ 的数据中,$2 \leq N \leq 100000$, $0 \leq K \leq100000$。

$100\%$ 的数据中,$0 < L \leq 10000000$。

思路分析 + 代码实现

这道题跟 P2678 [NOIP 2015 提高组] 跳石头 很像,都是二分,但是本题求的是“最大的最小”,而且在细节处理上不太一样。二分答案,当所用的路标小于 $K$ 时,说明可能还能取到更小的“空旷指数”,所以在记录答案的同时令 r = mid - 1; 。时间复杂度为 $O(n\operatorname{log}n)$ ,代码如下:

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;
constexpr int MAXN = 1e5 + 5;
int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int L, N, K, ans;
cin >> L >> N >> K;
for(int i = 1; i <= N; i++)
cin >> a[i];
int l = 1, r = L;
while (l <= r) {
int mid = (l + r) / 2, cnt = 0;
for(int i = 1; i < N; i++) {
int t = a[i] + mid;
if (t < a[i + 1])
while (t < a[i + 1]) {
cnt++;
t += mid;
}
}
if (cnt <= K) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
cout << ans;
return 0;
}

P2678 [NOIP 2015 提高组] 跳石头

题目背景

NOIP2015 Day2T1

题目描述

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 $N$ 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 $M$ 块岩石(不能移走起点和终点的岩石)。

输入格式

第一行包含三个整数 $L,N,M$,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 $L \geq 1$ 且 $N \geq M \geq 0$。

接下来 $N$ 行,每行一个整数,第 $i$ 行的整数 $D_i\,( 0 < D_i < L)$, 表示第 $i$ 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式

一个整数,即最短跳跃距离的最大值。

输入输出样例 #1

输入 #1

1
2
3
4
5
6
25 5 2 
2
11
14
17
21

输出 #1

1
4

说明/提示

输入输出样例 1 说明

将与起点距离为 $2$ 和 $14$ 的两个岩石移走后,最短的跳跃距离为 $4$(从与起点距离 $17$ 的岩石跳到距离 $21$ 的岩石,或者从距离 $21$ 的岩石跳到终点)。

数据规模与约定

对于 $20\%$的数据,$0 \le M \le N \le 10$。
对于 $50\%$ 的数据,$0 \le M \le N \le 100$。
对于 $100\%$ 的数据,$0 \le M \le N \le 50000,1 \le L \le 10^9$。

思路分析 + 代码实现

题目要求的是“最小的最大”,加上这道题答案具有“单调性”,而且比较容易判断一个目标最短距离是否合法,所以采取二分答案的做法,时间复杂度为 $O(n\operatorname{log}n)$ ,代码如下:

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;
constexpr int MAXN = 5e4 + 5;
int d[MAXN], s[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int L, N, M;
cin >> L >> N >> M;
for(int i = 1; i <= N; i++)
cin >> d[i];
d[N + 1] = L;
int l = 0, r = 1e9, ans = 0;
while (l <= r) {
for(int i = 0; i <= N + 1; i++)
s[i] = d[i];
int mid = (l + r) / 2, cnt = 0;
for(int i = N + 1; i >= 1; i--)
if (s[i] - s[i - 1] < mid) {
cnt++;
s[i - 1] = s[i];
}
if (cnt <= M) {
l = mid + 1;
ans = max(mid, ans);
} else r = mid - 1;
}
cout << ans;
return 0;
}

这里用的方法是用数组 $s$ 作为数组 $d$ 的副本,还能优化。

用变量 $dis$ 记录上一块石头距离起点的距离,如果枚举到的石头需要移走,则不用更新 $dis$ ,否则更新。此外 ans = max(mid, ans); 可以写成 ans = mid;

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;
constexpr int MAXN = 5e4 + 5;
int d[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int L, N, M;
cin >> L >> N >> M;
for(int i = 0; i < N; i++)
cin >> d[i];
d[N] = L;
int l = 0, r = 1e9, ans = 0;
while (l <= r) {
int mid = (l + r) / 2, cnt = 0, dis = 0;
for(int i = 0; i <= N; i++)
if (d[i] - dis < mid)
cnt++;
else dis = d[i];
if (cnt <= M) {
l = mid + 1;
ans = mid;
} else r = mid - 1;
}
cout << ans;
return 0;
}

P2440 木材加工

题目背景

要保护环境。

题目描述

木材厂有 $n$ 根原木,现在想把这些木头切割成 $k$ 段长度为 $l$ 的小段木头(木头有可能有剩余)。

当然,我们希望得到的小段木头越长越好,请求出 $l$ 的最大值。

木头长度的单位是 $\text{cm}$,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。

例如有两根原木长度分别为 $11$ 和 $21$,要求切割成等长的 $6$ 段,很明显能切割出来的小段木头长度最长为 $5$。

输入格式

第一行是两个正整数 $n,k$,分别表示原木的数量,需要得到的小段的数量。

接下来 $n$ 行,每行一个正整数 $L_i$,表示一根原木的长度。

输出格式

仅一行,即 $l$ 的最大值。

如果连 $\text{1cm}$ 长的小段都切不出来,输出 0

输入输出样例 #1

输入 #1

1
2
3
4
3 7
232
124
456

输出 #1

1
114

说明/提示

数据规模与约定

对于 $100\%$ 的数据,有 $1\le n\le 10^5$,$1\le k\le 10^8$,$1\le L_i\le 10^8(i\in[1,n])$。

思路分析 + 代码实现

发现比较容易判断一个 $l$ 是否合法,再加上这道题答案具有“单调性”,因此采取二分答案的方式,找到最大的合法的 $l$ ,时间复杂度为 $O(n\operatorname{log}n)$ ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k, ans = 0;
cin >> n >> k;
for(int i = 0; i < n; i++)
cin >> a[i];
int l = 0, r = 1e8;
while (l <= r) {
int mid = (l + r) / 2, sum = 0;
if (mid == 0) {ans = 0; break;}
for(int i = 0; i < n; i++)
sum += a[i] / mid;
if (sum < k) r = mid - 1;
else {ans = mid; l = mid + 1;}
}
cout << ans;
return 0;
}

P1182 数列分段 Section II

题目描述

对于给定的一个长度为 $N$ 的正整数数列 $A_{1\sim N}$,现要将其分成 $M$($M\leq N$)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 $4\ 2\ 4\ 5\ 1$ 要分成 $3$ 段。

将其如下分段:

第一段和为 $6$,第 $2$ 段和为 $9$,第 $3$ 段和为 $1$,和最大值为 $9$。

将其如下分段:

第一段和为 $4$,第 $2$ 段和为 $6$,第 $3$ 段和为 $6$,和最大值为 $6$。

并且无论如何分段,最大值不会小于 $6$。

所以可以得到要将数列 $4\ 2\ 4\ 5\ 1$ 要分成 $3$ 段,每段和的最大值最小为 $6$。

输入格式

第 $1$ 行包含两个正整数 $N,M$。

第 $2$ 行包含 $N$ 个空格隔开的非负整数 $A_i$,含义如题目所述。

输出格式

一个正整数,即每段和最大值最小为多少。

输入输出样例 #1

输入 #1

1
2
5 3
4 2 4 5 1

输出 #1

1
6

说明/提示

对于 $20\%$ 的数据,$N\leq 10$。

对于 $40\%$ 的数据,$N\leq 1000$。

对于 $100\%$ 的数据,$1\leq N\leq 10^5$,$M\leq N$,$A_i < 10^8$, 答案不超过 $10^9$。

思路分析 + 代码实现

题目求的是“最大的最小”,所以采用二分答案的方法。对于每个 $mid$ ,检查是否能将数组分成不超过 $M$ 段,使得每段和不超过 $mid$。如果可以,说明 $mid$ 是一个可行解,尝试更小的 $mid$ ;如果不行,说明 $mid$ 太小,需要增大 $mid$ 。

二分查找的核心是判断是否能将数组分成 $cnt <= M$ 段,使得每段的和都不超过 $mid$ 。我们用变量 $acc$ 累计本段的和,当 $acc + a[i] > mid$ 时,分出一段,让 $a[i]$ 在新一段的开头,并更新 $cnt$ 和 $acc$ 。但是!有一个细节要注意,这个逻辑能正确工作,有一个隐含的前提: $mid$ 必须大于等于数组中的最大元素 $ma$ 。如果 $mid < ma$,那么数组中存在至少一个元素 $a[i] = ma$,它无法被任何一段容纳(因为 $a[i] > mid$),在这种情况下,判断的逻辑就失效了。所以在读入的过程中记录 $ma$ ,二分时将 $l$ 设为 $ma$ 即可解决。代码如下,时间复杂度 $O(n\operatorname{log}n)$ 。

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;
constexpr int MAXN = 1e5 + 5;
int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, ma = 0;
cin >> N >> M;
for(int i = 0; i < N; i++) {
cin >> a[i];
ma = max(ma, a[i]);
}
int l = ma, r = 1e9, ans;
while (l <= r) {
int mid = (l + r) / 2, cnt = 1, acc = 0;
for(int i = 0; i < N; i++) {
int t = a[i] + acc;
if (t > mid) {
cnt++;
acc = a[i];
} else acc = t;
}
if (cnt <= M) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
cout << ans;
return 0;
}
0%