From 1d3034c22b6de19d015ce3485618a4f0339ec662 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 27 Oct 2026 10:19:23 +0000 Subject: [PATCH] Sync solutions from hanyixuanten/luogu@e128f1f --- AT_agc013_e.md | 266 +++++++++++++++++++++++++++++++++++++++++++++++++ CF1187E.md | 23 +++++ CF1912E.md | 111 +++++++++++++++++++++ CF739A.md | 39 ++++++++ P13366.md | 51 ++++++++++ P1453.md | 7 ++ P16055.md | 124 +++++++++++++++++++++++ P16060.md | 79 +++++++++++++++ P2016.md | 16 +++ P2607.md | 11 ++ P3199.md | 79 +++++++++++++++ P3572.md | 13 +++ P6650.md | 192 +++++++++++++++++++++++++++++++++++ 13 files changed, 1011 insertions(+) create mode 100644 AT_agc013_e.md create mode 100644 CF1187E.md create mode 100644 CF1912E.md create mode 100644 CF739A.md create mode 100644 P13366.md create mode 100644 P1453.md create mode 100644 P16055.md create mode 100644 P16060.md create mode 100644 P2016.md create mode 100644 P2607.md create mode 100644 P3199.md create mode 100644 P3572.md create mode 100644 P6650.md diff --git a/AT_agc013_e.md b/AT_agc013_e.md new file mode 100644 index 0000000..b92e416 --- /dev/null +++ b/AT_agc013_e.md @@ -0,0 +1,266 @@ +# AT_agc013_e + +[luoshunran](https://www.acwing.com/file_system/file/content/whole/index/content/14321537/) + +## 20 pts O(n) + +把正方形的交界处看作隔板,在每两个隔板之间放入一黑一白两个球,正方形的大小也就等于区间内放入两个球的方案数,正方形的面积的乘积就是所有的放球方案乘法原理乘起来。于是变成了求有多少种放隔板、放球的方案。 + +做 dp $f(i, 0/1/2)$,代表目前这个区间内放入了几个球的方案。 + +注意 $f(1, 1) = 2$ + +若 $i - i + 1$ 之间可以放隔板, + +- $f(i+1, 0) = f(i, 0) + f(i, 2)$ +- $ f(i+1, 1) = 2 * f(i, 0) + f(i, 1) + 2 * f(i, 2)$, 这里乘以 2 代表决策放入的是黑球还是白球 +- $ f(i+1, 2) = f(i, 0) + f(i, 1) + 2 * f(i, 2) $ + +类似地,也可以列出不可以放隔板的情况。 + +- $f(i+1, 0) = f(i, 0)$ +- $f(i+1, 1) = 2 * f(i, 0) + f(i, 1)$ +- $f(i+1, 2) = f(i, 0) + f(i, 1) + f(i, 2)$ + +```cpp +#include +using namespace std; +const int mod = 1e9 + 7; +int n, m; +long long f[5000005][3]; +int a[100005]; +int main() +{ + scanf("%d%d", &n, &m); + for (int i = 1; i <= m; ++i) + scanf("%d", &a[i]); + sort(a + 1, a + m + 1); + int tot = 1; + f[1][0] = 1, f[1][1] = 2, f[1][2] = 1; + for (int i = 1; i < n; ++i) + { + if (i == a[tot]) + { + tot++; + f[i + 1][0] = f[i][0]; + f[i + 1][1] = (f[i][0] * 2 + f[i][1]) % mod; + f[i + 1][2] = (f[i][0] + f[i][1] + f[i][2]) % mod; + } + else + { + f[i + 1][0] = (f[i][0] + f[i][2]) % mod; + f[i + 1][1] = (f[i][0] * 2 + f[i][1] + 2 * f[i][2]) % mod; + f[i + 1][2] = (f[i][0] + f[i][1] + f[i][2] * 2) % mod; + } + } + printf("%lld\n", f[n][2]); + return 0; +} +``` + +## 100 pts O(m log n) + +注意到一个区间内需要多次转移,非常浪费。可以考虑使用矩阵快速幂优化。 + +对于非标记点,转移矩阵为: + +$$ +T=\begin{bmatrix} + 1&0&1\\ + 2&1&2\\ + 1&1&2 +\end{bmatrix} +$$ + +对于标记点: + +$$ +T=\begin{bmatrix} + 1&0&0\\ + 2&1&0\\ + 1&1&1 +\end{bmatrix} +$$ + +初始位置矩阵为: + +$$ +T=\begin{bmatrix} + 1\\ + 2\\ + 1 +\end{bmatrix} +$$ + +矩阵快速幂代码可参考[P1962](https://luogu.com.cn/problem/P1962) + +```cpp +#include +using namespace std; +const long long MOD = 1000000007LL; +struct Mat { + long long a[105][105]; + int n, m; + Mat(int _n = 0, int _m = 0) : n(_n), m(_m) { + memset(a, 0, sizeof(a)); + } + void operator*=(const Mat &b) { + if (m != b.n) { + exit(-1); + } + long long res[105][105] = {}; + for (int i = 1; i <= n; ++i) { + for (int j = 1; j <= b.m; ++j) { + for (int k = 1; k <= m; ++k) { + res[i][j] = (res[i][j] + a[i][k] * b.a[k][j]) % MOD; + } + } + } + for (int i = 1; i <= n; ++i) { + for (int j = 1; j <= b.m; ++j) { + a[i][j] = res[i][j]; + } + } + m = b.m; + } +}; + +Mat qpow(Mat a, long long k) { + Mat res(a.n, a.n); + for (int i = 1; i <= a.n; ++i) { + res.a[i][i] = 1; + } + while (k) { + if (k & 1) res *= a; + a *= a; + k >>= 1; + } + return res; +} + +int main() { + long long n; + scanf("%lld", &n); + + if (n <= 2) { + printf("1\n"); + return 0; + } + + Mat base1(1, 2), base2(2, 2); + + base1.a[1][1] = 1; + base1.a[1][2] = 1; + base2.a[1][1] = 1; + base2.a[1][2] = 1; + base2.a[2][1] = 1; + base2.a[2][2] = 0; + base2 = qpow(base2, n - 2); + base1 *= base2; + printf("%lld\n", base1.a[1][1]); + return 0; +} +``` + +最终程序: + +```cpp +#include +using namespace std; +const int mod = 1e9 + 7; +int n, m; +int a[100005]; +const long long fir[4][2] = {{0, 0}, {0, 1}, {0, 2}, {0, 1}}; +const long long yes[4][4] = {{0, 0, 0, 0}, {0, 1, 0, 0}, {0, 2, 1, 0}, {0, 1, 1, 1}}; +const long long no[4][4] = {{0, 0, 0, 0}, {0, 1, 0, 1}, {0, 2, 1, 2}, {0, 1, 1, 2}}; +struct Mat +{ + long long a[5][5] = {}; + int n, m; + Mat(int _n = 0, int _m = 0, int cpy = 0) : n(_n), m(_m) + { + if (cpy == 1) + for (int i = 1; i <= 3; ++i) + a[i][1] = fir[i][1]; + else if (cpy == 2) + for (int i = 1; i <= 3; ++i) + for (int j = 1; j <= 3; ++j) + a[i][j] = yes[i][j]; + else if (cpy == 3) + for (int i = 1; i <= 3; ++i) + for (int j = 1; j <= 3; ++j) + a[i][j] = no[i][j]; + } + void operator*=(const Mat &b) + { + if (m != b.n) + exit(-1); + long long res[5][5] = {}; + for (int i = 1; i <= n; ++i) + for (int j = 1; j <= b.m; ++j) + for (int k = 1; k <= m; ++k) + res[i][j] = (res[i][j] + a[i][k] * b.a[k][j]) % mod; + for (int i = 1; i <= n; ++i) + for (int j = 1; j <= b.m; ++j) + a[i][j] = res[i][j]; + m = b.m; + } + Mat operator*(const Mat &b) + { + if (m != b.n) + exit(-1); + Mat res(n, b.m); + for (int i = 1; i <= n; ++i) + for (int j = 1; j <= b.m; ++j) + for (int k = 1; k <= m; ++k) + res.a[i][j] = (res.a[i][j] + a[i][k] * b.a[k][j]) % mod; + return res; + } +}; + +Mat qpow(Mat a, long long k) +{ + Mat res(a.n, a.n); + for (int i = 1; i <= a.n; ++i) + res.a[i][i] = 1; + while (k) + { + if (k & 1) + res *= a; + a *= a; + k >>= 1; + } + return res; +} +Mat yess(3, 3, 2); +Mat noo(3, 3, 3); + +int main() +{ + scanf("%d%d", &n, &m); + for (int i = 1; i <= m; ++i) + scanf("%d", &a[i]); + Mat now(3, 1, 1); + int pos = 1; + for (int i = 1; i <= m; ++i) + { + int x = a[i]; + if (x < pos) + continue; + if (x > pos) + { + now = qpow(noo, x - pos) * now; + } + { + now = yess * now; + } + pos = x + 1; + } + if (n - pos > 0) + { + now = qpow(noo, n - pos) * now; + } + printf("%lld\n", now.a[3][1]); + return 0; +} +``` diff --git a/CF1187E.md b/CF1187E.md new file mode 100644 index 0000000..7ac7ff2 --- /dev/null +++ b/CF1187E.md @@ -0,0 +1,23 @@ +# CF1187E Tree Painting 题解 + +题意:树上 $01$ 染色,一个点染上色获得的分数等于这个点原本所在的连通块的大小。 + +考虑树形 dp,则 $dp_{fa} = size_{fa} + \sum_{u \in child_{fa}} dp_u$ +,其中 $size_{fa}$ 表示以 $fa$ 为根的子树的大小。 + +注意到总答案和根的选择有关系,朴素做法是直接运行 $n$ 次树形 dp,复杂度为 $O(n^2)$,无法通过。 + +考虑使用换根 dp,考虑确定以 $fa$ 为根时的答案,如何转移到以 $u (u \in child_{fa})$ 为根的答案。 + +$$ +\begin{aligned} +\because ans_{root} =& size_{root} + \sum_{u \in child_{root}} dp_u \\ +\therefore ans_{u} =& (f_u - size_u) + (ans_{root} - f_u - size_u) + n \\ +&(u\text{的孩子的贡献}) (root\text{其他孩子的贡献}) \\ +=& ans_u - 2\times size_u + n +\end{aligned} +$$ + +因此,只需要以 $1$ 为根节点计算一次,随后进行答案转移即可。 + +[CF1187E.cpp](../CF1187E.cpp) diff --git a/CF1912E.md b/CF1912E.md new file mode 100644 index 0000000..f55f2c4 --- /dev/null +++ b/CF1912E.md @@ -0,0 +1,111 @@ +# CF1912E Evaluate It and Back Again题解 + +## 题意 + +给定两个数,要求你写出一个只包含数字, $+, \times, -$ 的式子,正着看是第一个数,反着看是第二个数。 + +## 题解 + +观察发现只有**一位数**和 $+, \times$ 的**单项式**倒过来读还是一样的,由 $-0$ 结尾的单项式倒过来读和原式结果相反。 + +发现可以将一个要求倒着读后数值不变的数表示为 $9$ 进制的形式,这样多项式中每一项都是倒着读后不变的。 + +- 对于两个奇偶性相同的数,可以求两个数的平均数,将 $p, q$ 分别表示为 $p = \overline{pq} + \frac{p - q}{2}, q = \overline{pq} - \frac{p - q}{2}$ +- 对于两个奇偶性不同的数,也可以求平均数,但是为了让 $p, q$ 奇偶性统一,需要给 $p, q$ 分别减去一个正着读是奇数,反着读是偶数的数,如 $12$,然后即可按照第一种形式做 + +然后就是注意特判两个相同的数和 `0 0` 的情况,别忘记开 long long 就结束了 + +不懂看注释 + +## 参考代码 + +```cpp +#include +using namespace std; +string ans; +string to_9(long long n) //转换为正反均相同的多项式 +{ + bool flag = n >= 0; + if (!flag) + n = -n; + ans = ""; + long long cnt = 0; + long long yu[20] = {}; + while (n >= 10ll) + yu[cnt++] = n % 9ll, n /= 9ll; + yu[cnt] = n; + if (!flag) + ans += "0-"; + for (long long i = 0; i <= cnt; ++i) + { + if (yu[cnt - i] != 0) + { + for (long long j = 0; j < cnt - i; ++j) + ans += "9*"; + ans += yu[cnt - i] + '0'; + if (flag) + ans += "+0+"; + else + ans += "-0-"; + } + } + while (ans[ans.size() - 1] == '+' || ans[ans.size() - 1] == '-') + ans.pop_back(); // 去掉多于符号 + return ans; +} +string to_9_0(long long n) // 转换为正反读相反的多项式 +{ + bool flag = n >= 0; + if (!flag) + n = -n; + ans = ""; + long long cnt = 0; + long long yu[20] = {}; + while (n >= 10) + { + yu[cnt++] = n % 9ll; + n /= 9ll; + } + yu[cnt] = n; + if (!flag) + ans += "0-"; + for (long long i = 0; i <= cnt; ++i) + { + if (yu[cnt - i] != 0) + { + for (long long j = 0; j < cnt - i; ++j) + ans += "9*"; + ans += yu[cnt - i] + '0'; + if (flag) + ans += "-0+"; + else + ans += "+0-"; + } + } + while (ans[ans.size() - 1] == '+' || ans[ans.size() - 1] == '-') + ans.pop_back(); // 去掉多于符号 + return ans; +} +int main() +{ + long long p = 0, q = 0; + scanf("%lld%lld", &p, &q); + if (abs(p) % 2 == abs(q) % 2) // 同奇偶 + { + long long _pq = p + q >> 1, p_q = p - _pq; + if (p == 0 && q == 0) // 特判 + { + puts("0"); + return 0; + } + cout << to_9(_pq) << ((_pq != 0 && p_q != 0) ? "+" : "") << to_9_0(p_q) << endl; + } + else // 异奇偶 + { + long long _pq = (p - 21 + q - 12) >> 1, p_q = p - 21 - _pq; // 转换为同奇偶 + cout << to_9(_pq) << ((_pq != 0 && p_q != 0) ? "+" : "") << to_9_0(p_q) << ((p_q != 0 || _pq != 0) ? "+" : "") << "21" << endl; + } + return 0; +} + +``` \ No newline at end of file diff --git a/CF739A.md b/CF739A.md new file mode 100644 index 0000000..57d96dc --- /dev/null +++ b/CF739A.md @@ -0,0 +1,39 @@ +# [CF739A Alyona and mex](https://www.luogu.com.cn/problem/CF739A) + +## 题意 + +给定 $m$ 个区间,构造出一个长度为 $n$ 的序列,使得这 $m$ 个区间的最小 $mex$ 最大。 $mex$ 定义为最小的没有出现过的自然数。 + +## 题解 + +先观察样例,发现两次输出的 $mex$ 均等于**最短的区间长度**,显然这不是巧合。 + +首先很容易得出 $mex(S) \le \lvert S \rvert$,且当 $S$ 覆盖 $0$ 到 $\lvert S \rvert - 1$ 时取等,所以最终答案不会超过最短的区间的长度,然后思考如何构造出答案等于最短区间长度的情况。 + +求出最短的区间长度为 $x$,只要让这个区间完全覆盖 $0, 1, 2, ..., x - 1$ ,就可以取到 $mex$ 的最大值,所以则只要在数组中循环填入 $0, 1, 2, ..., x - 1$,就能保证每一个长度大于等于 $x$ 的区间 $mex$ 一定都等于 $x$,因为每一个长度大于等于 $x$ 的区间都至少覆盖一次 $0, 1, 2, ..., x - 1$。(可以自己手搓几个样例试试) + +## 代码 + +```cpp +#include +using namespace std; +int main() +{ + int n, m; + scanf("%d%d", &n, &m); + int minlen = INT_MAX; + for (int i = 0; i < m; ++i) + { + int l, r; + scanf("%d%d", &l, &r); + minlen = min(minlen, r - l + 1); + } + printf("%d\n", minlen); + int cnt = 0; + for (int i = 1; i <= n; ++i) + printf("%d ", i % minlen); + return 0; +} +``` + +--- \ No newline at end of file diff --git a/P13366.md b/P13366.md new file mode 100644 index 0000000..bcb17cb --- /dev/null +++ b/P13366.md @@ -0,0 +1,51 @@ +参考了[官方pdf题解](https://www.luogu.com.cn/fe/api/problem/downloadAttachment/zsdw8913)。 + +## 思路 + +如果对于每一张字母表、每一个单词都单独模拟 Hangman,复杂度会达到 $O(N^2M)$,无法通过大数据。 + +关键观察是: + +- 游戏开始时,Sean 只能根据单词长度判断候选单词,因此不同长度的单词互不影响。 +- 当 Sean 猜测一个字母时,当前候选集合中的单词会根据该字母出现的位置得到不同反馈。 +- 得到相同反馈的单词在之后仍然无法区分,因此可以继续放在同一个候选组中。 +- 如果某个候选组中没有单词包含当前字母,Sean 会跳过该字母,候选组不发生变化。 +- 如果候选组中存在单词包含当前字母,Sean 会猜这个字母。此时不包含该字母的单词所在分支会让 Sean 失去 $1$ 分。 + +因此,不必为每个目标单词分别模拟游戏,而是同时维护所有单词的候选集合。 + +## 状态表示 + +对于每个长度,初始时将所有该长度的单词放入同一个候选组。 + +一个候选组表示在已经猜过的字母及其反馈下,Sean 仍然无法区分的所有单词。 + +对于某个字母,使用一个二进制表示它在单词中出现的位置。 + +例如对于单词 banana 和字母 a,对应二进制为 $101010_2$。 + +两个单词对于当前字母得到相同编码,当且仅当 Sean 从黑板上看到的反馈相同,因此它们应被分到同一个新候选组中。 + +## 转移过程 + +对于一张字母表 L。 + +1. 按单词长度初始化候选组。 +2. 按 L 中的顺序枚举每个字母。 +3. 对每个当前候选组。 + - 若组内只剩一个单词,Sean 已经确定答案,不再需要处理该组。 + - 计算组内每个单词对当前字母的出现位置二进制表示。 + - 若所有二进制表示均为 $0$,说明没有候选单词包含该字母,Sean 会跳过该字母,候选组保持不变。 + - 否则,Sean 会猜该字母。 + - 二进制为 $0$ 的单词不含该字母,对这些单词的失分加一。 + - 按二进制表示将原候选组拆分为多个新候选组。 +4. 所有字母处理结束后,选择失分最多的单词。 +5. 若失分相同,保留字典中下标较小的单词即可满足题目的平局规则。 + +## 复杂度分析 + +单词长度最大为 $10$,字母表长度固定为 $26$。 + +对于每一张字母表,每个单词最多参与 $26$ 次分组,每次计算位置二进制表示。 + +因此总时间复杂度为 $O(NM)$,可以通过本题。 \ No newline at end of file diff --git a/P1453.md b/P1453.md new file mode 100644 index 0000000..8067fe0 --- /dev/null +++ b/P1453.md @@ -0,0 +1,7 @@ +# P1453 城市环路 题解 + +题意可转化为有一棵树,每个节点有权值,一个边两端的点不能同时去,同时 $S, T$ 也不能同时取(断开环上的两个点)。 + +因此可转化两次为[P1352 没有上司的舞会](https://www.luogu.com.cn/problem/P1352#),设 $f_{i, 01}$ 以 $i$ 为根的子树,是否选择本节点的收益,只需[以 $S$ 为根时令 $f_{T, 1} = -\inf$,以 $T$ 为根时令 $f_{S, 1} = -\inf$ 即可。](../P1453.cpp#L17-19) + +[code](../P1453.cpp) \ No newline at end of file diff --git a/P16055.md b/P16055.md new file mode 100644 index 0000000..14ea833 --- /dev/null +++ b/P16055.md @@ -0,0 +1,124 @@ +# P16055 [CSPro 31] 坐标变换(其二) 题解 + +这篇题解主要针对不懂得矩阵乘法的人。主要思想: + +视每次操作为: + +$$ +newx = x * k1 - y * k2\\ +newy = x * k2 + y * k1\\ +x = newx, y = newy +$$ + +其中: + +- 操作为 $1$ 时,$k1 = k, k2 = 0$。 +- 操作为 $2$ 时,$k1 = \cos(\theta), k2 = \sin(\theta)$。 + +## 80 pts 暴力 + +注意到 $80%$ 的测试点满足 $n, m \le 1000$,此时最大执行次数为 $1000 \times 1000 = 10^6$,可以获得此部分分。 + +::::info[[80 pts 代码](https://www.luogu.com.cn/record/303033043)] + +```cpp +#include +using namespace std; +int n, m; +vector> o; +double k1[100005], k2[100005]; +int main() { + scanf("%d%d", &n, &m); + o.push_back({0, 0}); + for (int i = 1; i <= n; ++i) { + int op; + double k; + scanf("%d%lf", &op, &k); + o.push_back({op, k}); + if (op == 2) { + k1[i] = cos(k); + k2[i] = sin(k); + } else { + k1[i] = k; + k2[i] = 0; + } + } + for (int i = 1; i <= m; ++i) { + int l, r, x, y; + scanf("%d%d%d%d", &l, &r, &x, &y); + double nx = x, ny = y; + for (int j = l; j <= r; ++j) { + if (o[j].first == 1) + nx *= o[j].second, ny *= o[j].second; + else { + double tmpx = nx * k1[j] - ny * k2[j], + tmpy = nx * k2[j] + ny * k1[j]; + nx = tmpx, ny = tmpy; + } + } + printf("%f %f\n", nx, ny); + } + return 0; +} +``` + +:::: + +## 100 pts st 表 + +注意到连续一段的操作可以由各部分操作叠加,可使用 st 表解决。 + +::::info[[100pts 代码](https://www.luogu.com.cn/record/303042049)] + +```cpp +#include +using namespace std; +int n, m; +vector> o; +double st1[200005][21], st2[200005][21]; +int main() { + scanf("%d%d", &n, &m); + o.push_back({0, 0}); + for (int i = 1; i <= n; ++i) { + int op; + double k; + scanf("%d%lf", &op, &k); + o.push_back({op, k}); + if (op == 1) { + st1[i][0] = k; + st2[i][0] = 0; + + } else { + st1[i][0] = cos(k); + st2[i][0] = sin(k); + } + } + for (int i = 1; i <= 20; i++) { + for (int j = 1; j + (1 << i) - 1 <= n; j++) { + double tmp11 = st1[j][i - 1], tmp12 = st2[j][i - 1]; + double tmp21 = st1[j + (1 << (i - 1))][i - 1], + tmp22 = st2[j + (1 << (i - 1))][i - 1]; + st1[j][i] = tmp11 * tmp21 - tmp12 * tmp22; + st2[j][i] = tmp11 * tmp22 + tmp12 * tmp21; + } + } + while (m--) { + int l, r, x, y; + scanf("%d%d%d%d", &l, &r, &x, &y); + double nx = x, ny = y; + r++; + for (int i = 20; i >= 0; i--) { + if (l + (1 << i) <= r) { + double tmpx = st1[l][i] * nx - st2[l][i] * ny, + tmpy = st2[l][i] * nx + st1[l][i] * ny; + nx = tmpx, ny = tmpy; + l += 1 << i; + } + } + printf("%f %f\n", nx, ny); + } + return 0; +} +``` + +:::: diff --git a/P16060.md b/P16060.md new file mode 100644 index 0000000..abcb70b --- /dev/null +++ b/P16060.md @@ -0,0 +1,79 @@ +# P16060 [CSPro 24] 序列查询新解 题解 + +## 75 pts 暴力 + +直接遵循题意使用 `lower_bound` 即可得到 75 pts + +::::info[[75 pts 代码](https://www.luogu.com.cn/record/303048871)] + +```cpp +#include +using namespace std; +int n, N; +int a[100005]; +int main() { + scanf("%d%d", &n, &N); + for (int i = 1; i <= n; ++i) { + scanf("%d", &a[i]); + } + long long ans = 0; + for (int i = 0; i <= N - 1; i++) { + int f = lower_bound(a, a + n + 1, i) - a; + while (a[f] <= i && f <= n) + f++; + f--; + int g = i / (N / (n + 1)); + ans += abs(f - g); + } + printf("%lld\n", ans); + return 0; +} +``` + +:::: + +## 100 pts 正解 + +注意到对于两个数 $a_i, a_{i + 1}$,$f(x) = i, x \in [a_i, a_{i + 1})$ + +且对于 $g$,只有 $i = k \times\frac{N}{(n + 1)}$ 的时候数值才会变化。 + +因此,统计 $f, g$ 分别变化的节点进行计算即可。 + +::::info[[100 pts 代码](https://www.luogu.com.cn/record/303064173)] + +```cpp +#include +using namespace std; +int n, N; +map changes; +int main() { + scanf("%d%d", &n, &N); + for (int i = 1, a; i <= n; ++i) + scanf("%d", &a), changes[a] = 1; + for (int i = 1; i * (N / (n + 1)) < N; i++) { + if (changes.find(i * (N / (n + 1))) != changes.end()) + changes[i * (N / (n + 1))] = 3; + else + changes[i * (N / (n + 1))] = 2; + } + changes[N] = 2; + int nowf = 0, nowg = 0; + long long ans = 0; + pair pre = {0, 0}; + for (pair now : changes) { + ans += abs(nowg - nowf) * (now.first - pre.first); + if (now.second == 1) + nowf++; + else if (now.second == 2) + nowg++; + else + nowf++, nowg++; + pre = now; + } + printf("%lld\n", ans); + return 0; +} +``` + +:::: diff --git a/P2016.md b/P2016.md new file mode 100644 index 0000000..b52e2a4 --- /dev/null +++ b/P2016.md @@ -0,0 +1,16 @@ +# P2016 [SEERC 2000] 战略游戏 题解 + +题意:给你一棵树,要求选中一些点,使得每条边至少一个端点被选中。 + +令 $dp_{i, 01}$ 表示以 $i$ 为根的子树,是否选中该节点的最少选中节点数。易得: + +$$ +\begin{aligned} +f_{i, 0} =& \sum f_{u, 1} (u \in child_i) \\ +f_{i, 1} =& \sum \min(dp_{u, 0}, dp_{u, 1}) +\end{aligned} +$$ + +树形 dp 即可。 + +[code](../P2016.cpp) \ No newline at end of file diff --git a/P2607.md b/P2607.md new file mode 100644 index 0000000..1b7c758 --- /dev/null +++ b/P2607.md @@ -0,0 +1,11 @@ +# P2607 [ZJOI2008] 骑士 题解 + +先看[P1453](./P1453.md) + +只需要将仇恨关系视作一个连边即可。 + +本题与[P1453](../P1453.md)的区别是形成的不只是一个树+断边,而是一个森林+断边。 + +只需要将 [P1453 的并查集](../P1453.cpp#L35-42)改为[保存 $S, T$ vector](../P2607.cpp#L33-40),随后对于每个树+断边仿照 P1453 的实现即可 + +[code](../P2607.cpp) diff --git a/P3199.md b/P3199.md new file mode 100644 index 0000000..14f77d8 --- /dev/null +++ b/P3199.md @@ -0,0 +1,79 @@ +# P3199 最小圈 + +思路:找到最小圈相当于找到最小环。 + +只须二分答案环的平均值的最小值 $mid$,并把每条边均减去 $mid$,使用 SPFA 判断负环即可。 + +调试记录: + +- 二分范围错误 + - 因为存在负边权,二分范围应该为 $-10^7$ 到 $10^7$ +- SPFA 判负环逻辑错误 + - SPFA 判断负环用的是单个路径的长度 $> n$ 而非访问的点个数 $> n$ +- 初始入队点错误 + - 存在圈且有一个点能到达其他所有点,不一定是点 $1$ +- 入队条件错误 + - SPFA 入队的条件是未在队列中且进行了松弛操作 +- 队列长度不足 + - 一个点可能多次入队,队列长度应该为 $n \times m$ + +```cpp +#include +using namespace std; +int n, m; +vector> fri[3005]; +int q[30000005]; +double d[3005]; +bool in[3005]; +int cnt[3005]; +int ql = 1, qr = 0; +bool check(double minus) +{ + ql = 1, qr = 0; + memset(d, 0, sizeof d); + memset(cnt, 0, sizeof cnt); + for (int i = 1; i <= n; ++i) + { + q[++qr] = i; + in[i] = 1; + } + while (qr >= ql) + { + int now = q[ql++]; + in[now] = 0; + for (pair to : fri[now]) + { + if (d[now] + to.second - minus < d[to.first]) + { + d[to.first] = d[now] + to.second - minus, cnt[to.first]++; + if (!in[to.first]) + q[++qr] = to.first, in[to.first] = 1; + } + if (cnt[to.first] > n) + return 1; + } + } + return 0; +} +int main() +{ + scanf("%d%d", &n, &m); + for (int i = 1, u, v; i <= m; ++i) + { + double w; + scanf("%d%d%lf", &u, &v, &w); + fri[u].push_back({v, w}); + } + double l = -1e7, r = 1e7; + while (r - l > 1e-9) + { + double mid = (l + r) / 2; + if (check(mid)) + r = mid; + else + l = mid; + } + printf("%.8lf\n", l); + return 0; +} +``` diff --git a/P3572.md b/P3572.md new file mode 100644 index 0000000..154aa3c --- /dev/null +++ b/P3572.md @@ -0,0 +1,13 @@ +# P3572 [POI 2014] PTA-Little Bird 题解 + +$n$ 棵有高度的树。有鸟要从第 $1$ 棵树到第 $n$ 棵树。当第 $i$ 只鸟每次最多向前 $k$ 棵树。如果一只鸟飞到一颗高度大于等于当前树的树,那么它的劳累值会增加 $1$,否则不会。最小化劳累值。 + +朴素做法为设 $f_x$ 为这只鸟飞到 $x$ 的最小劳累值。则: + +$$f_x = \min(f_i + (a_x >= a_i)), i \in [x - k, x - 1]$$ + +但这样是 $O(n^2q)$ 的,无法通过。 + +注意到 $f_x$ 只会由 $f_i, i \in [x - k, x - 1]$ 转移得到,且不会选择其中值过大的。考虑使用单调队列维护滑动窗口,记录可能产生贡献的值。 + +[code](../P3572.cpp) \ No newline at end of file diff --git a/P6650.md b/P6650.md new file mode 100644 index 0000000..18bf91a --- /dev/null +++ b/P6650.md @@ -0,0 +1,192 @@ +## 题意简述 + +给定长度为 $n$ 的序列 $a$ 和整数 $k$。选择一个区间 $[l,r]$,满足区间最大值与最小值之差不超过 $k$。 + +将区间乘积质因数分解: + +$$ +\prod_{i=l}^r a_i = \prod_p p^{E_p}。 +$$ + +对于每个质数 $p$,把 $E_p$ 拆成若干个互不相同的正整数 $e_{p,1},e_{p,2},\dots,e_{p,t_p}$,并选取对应的质数幂 $p^{e_{p,i}}$。所有选取的数必须互不相同。 + +得分为所有选取数的约数个数之和: + +$$ +\sum_p \sum_i (e_{p,i}+1)。 +$$ + +求最大得分。 + +## 思路分析 + +### 单个质数的贡献 + +固定一个质数 $p$,设它在区间乘积中的总指数为 $E$。 + +若把 $E$ 拆成 $t$ 个互不相同的正整数,则这些数的和至少为 + +$$ +1+2+\cdots+t = \frac{t(t+1)}2。 +$$ + +当 $E \ge \frac{t(t+1)}2$ 时,可以取 $1,2,\dots,t-1$ 以及 $E-\frac{t(t-1)}2$,它们互不相同且总和为 $E$。 + +因此最大可行的 $t$ 为满足 $\frac{t(t+1)}2 \le E$ 的最大整数,即 + +$$ +t_{\max} = \left\lfloor \frac{\sqrt{8E+1}-1}{2} \right\rfloor。 +$$ + +对应的得分为 + +$$ +\sum_i (e_i+1) = E + t_{\max}。 +$$ + +定义 + +$$ +gx(E) = E + \left\lfloor \frac{\sqrt{8E+1}-1}{2} \right\rfloor, +$$ + +则一个质数指数为 $E$ 时的贡献为 $gx(E)$。特别地,$gx(0)=0$。 + +### 区间选择 + +对于固定的右端点 $r$,如果区间 $[l,r]$ 合法,那么区间 $[l+1,r]$ 也合法,因为子区间的极差不会增大。 + +又因为窗口扩大时,每个质数的指数 $E_p$ 只会增加或不增加,同时 $gx(E)$ 单调递增,所以固定 $r$ 时,区间越长得分越高。于是只需要找到最小的合法左端点。 + +使用双指针维护滑动窗口。令左端点 $l$ 单调右移,并用两个单调队列分别维护当前窗口的最大值和最小值。当窗口极差超过 $k$ 时,不断右移 $l$,同时删除 $a_l$ 的质因子贡献。 + +## 算法流程 + +1. 预处理最小质因子 $spf$,用于快速分解每个 $a_i$。 +2. 将每个 $a_i$ 分解为质因子列表,保留重数。 +3. 维护滑动窗口 $[l,r]$,其中: + - $cnt[p]$ 表示当前窗口内质数 $p$ 的总指数; + - $cur$ 表示当前窗口得分 $\sum_p gx(cnt[p])$; + - 两个单调队列维护窗口最大值和最小值。 +4. 枚举右端点 $r$: + - 将 $a_r$ 的质因子加入窗口,更新 $cnt$ 和 $cur$; + - 将 $r$ 加入单调队列; + - 若当前窗口极差超过 $k$,则不断右移 $l$,并删除 $a_l$ 的质因子贡献; + - 用当前 $cur$ 更新答案。 +5. 输出答案。 + +## 复杂度分析 + +设 $A=\max a_i \le 10^5$。 + +- 预处理最小质因子:$O(A \log \log A)$。 +- 分解所有 $a_i$:总质因子个数为 $O(n \log A)$。 +- 滑动窗口:每个元素的每个质因子加入一次、删除一次,复杂度 $O(n \log A)$。 +- 单调队列:$O(n)$。 + +总时间复杂度为 + +$$ +O(A \log \log A + n \log A)。 +$$ + +## 参考代码 + +```cpp +#include +using namespace std; + +int n, k; +int a[100005]; +int spf[100005]; // 最小质因子 +vector fac[100005]; // 每个数的质因子列表(带重数) +int cnt[100005]; // 当前窗口内每个质数的总指数 +long long cur, ans; +deque dq_max, dq_min; + +// 计算 e 的贡献 +long long gx(int e) { + if (e == 0) return 0; + long long t = (sqrt(8.0 * e + 1) - 1) / 2; + while ((t + 1) * (t + 2) / 2 <= e) ++t; + while (t * (t + 1) / 2 > e) --t; + return e + t; +} + +// 预处理最小质因子 +void init_spf() { + for (int i = 2; i < 100005; ++i) { + if (!spf[i]) { + spf[i] = i; + if ((long long)i * i < 100005) { + for (int j = i * i; j < 100005; j += i) { + if (!spf[j]) spf[j] = i; + } + } + } + } +} + +// 分解质因数 +void get_fac(int idx, int x) { + while (x > 1) { + int p = spf[x]; + while (x % p == 0) { + fac[idx].push_back(p); + x /= p; + } + } +} + +int main() { + scanf("%d%d", &n, &k); + init_spf(); + + for (int i = 1; i <= n; ++i) { + scanf("%d", &a[i]); + get_fac(i, a[i]); + } + + int l = 1; + cur = 0, ans = 0; + + for (int r = 1; r <= n; ++r) { + // 加入 a[r] + for (int p : fac[r]) { + int e = cnt[p]; + cur += gx(e + 1) - gx(e); + cnt[p]++; + } + + while (!dq_max.empty() && a[dq_max.back()] <= a[r]) dq_max.pop_back(); + dq_max.push_back(r); + + while (!dq_min.empty() && a[dq_min.back()] >= a[r]) dq_min.pop_back(); + dq_min.push_back(r); + + // 收缩左边界 + while (1) { + while (!dq_max.empty() && dq_max.front() < l) dq_max.pop_front(); + while (!dq_min.empty() && dq_min.front() < l) dq_min.pop_front(); + + int mx = a[dq_max.front()]; + int mn = a[dq_min.front()]; + + if (mx - mn <= k) break; + + // 移除 a[l] + for (int p : fac[l]) { + int e = cnt[p]; + cur += gx(e - 1) - gx(e); + cnt[p]--; + } + l++; + } + + ans = max(ans, cur); + } + + printf("%lld\n", ans); + return 0; +} +``` \ No newline at end of file