CY的个人博客

记录生活

P4017 最大食物链计数

题目背景

你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧。

题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 $1$ 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 $80112002$ 的结果。

输入格式

第一行,两个正整数 $n$、$m$,表示生物种类 $n$ 和吃与被吃的关系数 $m$。

接下来 $m$ 行,每行两个正整数,表示被吃的生物 A 和吃 A 的生物 B。

输出格式

一行一个整数,为最大食物链数量模上 $80112002$ 的结果。

输入输出样例 #1

输入 #1

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

输出 #1

1
5

说明/提示

各测试点满足以下约定:

测试点编号 $n$ $m$
$1,2$ $\le 40$ $\le 400$
$3,4$ $\le 100$ $\le 2\times 10^3$
$5,6$ $\le 10^3$ $\le 6\times 10^4$
$7,8$ $\le 2\times 10^3$ $\le 2\times 10^5$
$9,10$ $\le 5\times 10^3$ $\le 5\times 10^5$

对于 $100\%$ 的数据,$1 \le n \le 5\times 10^3,1\le m \le 5\times 10^5$

【补充说明】

数据中不会出现环,满足生物学的要求。(感谢 @AKEE)

思路分析 + 代码实现

本题是经典的拓扑排序例题,加上动态规划的思想。

什么是拓扑排序?

拓扑排序是对 有向无环图(DAG) 进行线性排序的方法,使得对于图中的每条有向边 $(u, v)$ ,节点 $u$ 在排序中都出现在节点 $v$ 之前。

为什么本题需要拓扑排序?

因为食物链是有方向的(A被B吃),且不能有环(生物学上食物链不能循环),这正好符合DAG的性质。我们需要按照食物链的方向计算路径数。

具体步骤

用 $indeg[i]$ 记录节点 $i$ 的入度, $cnt[i]$ 记录以节点 $i$ 结尾的食物链条数(取模后)

  1. 初始化队列: 将入度为零的点(生产者)加入队列(作为食物链的起点),并初始化 $cnt[i] = 1$

  2. BFS遍历:

  • 从队列中取出节点 $u$

  • 遍历 $u$ 的所有后继节点 $i$

  • 对每个后继节点 $v$:

    • 减少 $v$ 的入度(相当于删除边 $u \rightarrow i$)

    • 进行状态转移:$cnt[i] = (cnt[i] + cnt[u]) \mod MOD$

      • 状态转移方程 $cnt[i] = \sum cnt[u]$(其中 $u$ 是 $i$ 的前驱)

      • 意义:到达 $i$ 的路径数 = 所有到达 $u$ 的路径数之和(因为每条到 $u$ 的路径都可以延伸到 $i$)

    • 如果 $v$ 的入度变为 $0$ ,将 $v$ 加入队列

  • 重复直到队列为空

  1. 统计结果: 遍历所有节点,如果节点的出度为 $0$(即 $G[i].empty()$,顶级消费者),将 $cnt[i]$ 累加到答案中

本题中拓扑排序的作用

拓扑排序确保了计算顺序的正确性:

  • 只有在节点 $i$ 的所有前驱节点都被处理完后,$i$ 的入度才会变为0,$i$ 才会被加入队列

  • 这意味着当计算 $cnt[i]$ 时,所有到达 $i$ 的路径都已经被考虑到了

  • 从而保证了 $cnt[i] = \sum cnt[u]$ 的正确性

时间复杂度分析

  • 每个节点入队一次:$O(n)$

  • 每条边被访问一次:$O(m)$

  • 总时间复杂度:$O(n + 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
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 5e3 + 5, MOD = 80112002;
vector<int> G[MAXN];
int indeg[MAXN], cnt[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, ans = 0;
cin >> n >> m;
for(int u, v, i = 0; i < m; i++) {
cin >> u >> v;
indeg[v]++;
G[u].push_back(v);
}
queue<int> Q;
for(int i = 1; i <= n; i++)
if (indeg[i] == 0) {
Q.push(i);
cnt[i] = 1;
}
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for(int i : G[u]) {
indeg[i]--;
cnt[i] = (cnt[i] + cnt[u]) % MOD;
if (indeg[i] == 0) Q.push(i);
}
}
for(int i = 1; i <= n; i++)
if (G[i].empty())
ans = (ans + cnt[i]) % MOD;
cout << ans;
}

此外,还有利用 DFS + 记忆化搜索的方法,代码更简洁。

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;
constexpr int MAXN = 5e3 + 5, MOD = 80112002;
vector<int> G[MAXN];
int indeg[MAXN], cnt[MAXN];
int dfs(int u) {
if (cnt[u]) return cnt[u]; // 已经计算过
if (G[u].empty()) // 顶级消费者,只有自身一条链
return cnt[u] = 1;
int res = 0;
for (int v : G[u]) // 累加所有后继的路径数
res = (res + dfs(v)) % MOD;
return cnt[u] = res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, ans = 0;
cin >> n >> m;
for(int u, v, i = 0; i < m; i++) {
cin >> u >> v;
indeg[v]++;
G[u].push_back(v);
}
for(int i = 1; i <= n; i++)
if (indeg[i] == 0)
ans = (ans + dfs(i)) % MOD;
cout << ans;
}

P3916 图的遍历

题目描述

给出 $N$ 个点,$M$ 条边的有向图,对于每个点 $v$,令 $A(v)$ 表示从点 $v$ 出发,能到达的编号最大的点。现在请求出 $A(1),A(2),\dots,A(N)$ 的值。

输入格式

第 $1$ 行 $2$ 个整数 $N,M$,表示点数和边数。

接下来 $M$ 行,每行 $2$ 个整数 $U_i,V_i$,表示边 $(U_i,V_i)$。点用 $1,2,\dots,N$ 编号。

输出格式

一行 $N$ 个整数 $A(1),A(2),\dots,A(N)$。

输入输出样例 #1

输入 #1

1
2
3
4
4 3
1 2
2 4
4 3

输出 #1

1
4 4 3 4

说明/提示

  • 对于 $60\%$ 的数据,$1 \leq N,M \leq 10^3$。
  • 对于 $100\%$ 的数据,$1 \leq N,M \leq 10^5$。

思路分析 + 代码实现

用反向边建图,这样构建的反向图可以将原问题转化为从每个节点出发在反向图中能到达哪些节点。从编号最大的点开始遍历(这里写了 BFS 和 DFS 两种做法)。如果遍历到的点未被遍历过,则记录答案,继续遍历。时间复杂度和空间复杂度都是为 $O(N+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
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
vector<int> G[MAXN];
int Dans[MAXN], Bans[MAXN];
void dfs(int x, int v) {
if (Dans[x]) return;
Dans[x] = v;
for(int i : G[x])
dfs(i, v);
}
void bfs(int x, int v) {
if (Bans[x]) return;
Bans[x] = v;
queue<int> Q;
Q.push(x);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for(int i : G[u]) {
if (Bans[i]) continue;
Bans[i] = v;
Q.push(i);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
for(int u, v, i = 0; i < M; i++) {
cin >> u >> v;
G[v].push_back(u);
}
for(int i = N; i > 0; i--)
dfs(i, i);
for(int i = N; i > 0; i--)
bfs(i, i);
for(int i = 1; i <= N; i++)
cout << Bans[i] << " ";
// cout << Dans[i] << " ";
return 0;
}

Markdown 数学公式语法指南

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

常见运算符

基本算术运算符

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

取模运算

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

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

逻辑运算符

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

上下标

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

关系运算符

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

求和、积分、极限

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

希腊字母

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

集合运算符

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

函数表达式

常用数学函数

函数 LaTeX 语法 示例 效果
正弦 \sin \sin x $\sin x$
余弦 \cos \cos x $\cos x$
正切 \tan \tan x $\tan x$
指数 \exp \exp(x) $\exp(x)$
最大值 \max \max(a, b) $\max(a, b)$
最小值 \min \min(a, b) $\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}$
自适应括号 \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$ 中。时间复杂度为 $O(\log b)$,其中 $b$ 是指数,代码如下:

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秒),说明需要一个 $O(n)$ 或者 $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$。复杂度为 $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 \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 \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 \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;
}
0%