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