## 博弈
#模拟 #博弈论
> [!note] 思路分析
>
> 根据题意,选择的两个元素若是一奇一偶那么最终值相对于它们的和将会减少 $1$,否则不变。所以对于第一个人,肯定是优先选择同奇同偶的。第二个人需要最小化答案,那么会优先选择一奇一偶的。
> [!question] 问题
>
> 但是第一个人在奇数和偶数都很多的情况下,会进行什么操作呢?它会优先选择两个奇数的消掉。这样就可以让第二个人没有奇数用;不消偶数的原因是不管任何情况合出来都会增加一个偶数。
> [!success] 正确思路
>
> 所以先进行若干次理想型操作,即第一个人反复拿两个奇数合成一个偶数,第二个人拿一个偶数一个奇数合成一个偶数。即每次消除三个奇数,然后答案减去一。可以 $\mathcal O(1)$ 计算完成。
>
> 对于最后剩下的元素,再进行不超过一轮以后,只会剩下偶数,此时和不变。可以直接模拟最后的操作。
- [n] 对于每个前缀都进行计算。时间复杂度 $\mathcal O(n)$。
```cpp
#include <fstream>
using namespace std;
using LL = long long;
const LL kMaxN = 1e5 + 5;
ifstream cin("makise.in");
ofstream cout("makise.out");
LL a[kMaxN], n, c0, c1, ans;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (LL i = 1; i <= n; i++) {
cin >> a[i];
c0 += a[i] % 2 == 0;
c1 += a[i] % 2 == 1;
LL x0 = c0, x1 = c1;
LL sub = x1 / 3;
x1 %= 3;
ans += a[i];
x0 += sub;
if (x1 >= 2) {
x1 -= 2;
x0++;
} else if (x0 >= 2) {
x0--;
} else if (x0 && x1) {
x0--, x1--;
sub++;
}
if (x0 && x1) {
sub++;
}
cout << ans - sub << ' ';
}
return 0;
}
```
## 购物
#贪心 #优先队列 #排序
> [!summary] 赛时思路
>
> 考虑较多次地选择 $b$ 最大(其次 $a$)的的元素,枚举其它元素的选择次数(最多不超过 $\min(2n, m)$),然后用优先队列每次选择其它元素当中可选的最大值。
> [!attention] 注意
>
> 赛时思路是错误的。考虑这个 $n=2,m=3$ 的样例
>
> $
> \left(\begin{matrix}
> 0&100\\
> 10&0\\
> 5&99
> \end{matrix}\right)
> $
>
> 我们把 $(0,100)$ 单独搞出去,枚举其它的选多少个。如果什么都不选就是 $100$。当选择其它的一个时,程序将贪心地选择 $10$。此时并不优。但是当选择其它的两个是,我们会选择 $5$,这直接导致了我们错过了最优解 $5+99$。
> [!question] 猜想
>
> 这似乎需要通过反悔贪心实现,不过我的实现仍然无法成功地通过大样例。
- [p] 好在只挂了 $15$ 分。
对于这道题目,一共有两种思路。他们都是正确的。
> [!success] 正确思路一
>
> 既然我们不一定会选择 $b_\max$ 较多次,于是我们就可以直接枚举选择哪个 $b_i$,那么就必定同样会选择 $a_i$。对于其它元素 $j\ne i$ 的 $b_j$ 我们都不会再次选择了,所以我们把所有大于 $b_i$ 的 $a_j$ 给选了。可以通过排序后二分或双指针实现。时间复杂度 $\mathcal O(m\textcolor{gray}{\log_2 m})$。
> [!success] 正确思路二
>
> 直接把一个物品拆成两个物品 $a_i,b_i$ 从大到小加入优先队列当中($a$ 还是 $b$ 的信息仍要保留)。对于新的一个元素,如果是 $a$ 直接选上即可;如果是 $b$,如果以前已经选过了其对应的 $a$,直接选上即可;否则若剩余步数允许就把它的 $a$ 选上之后反复选 $b$,和答案求个 $\max$(注意这里不要 `break` 也不是实际选上了)。时间复杂度 $\mathcal O(m\log_2 m)$。
- [n] 我赛后改题实现的是正确思路二。
```cpp
#include <algorithm>
#include <fstream>
#include <queue>
#include <utility>
using namespace std;
using LL = long long;
const LL kMaxN = 1e5 + 5;
ifstream cin("shop.in");
ofstream cout("shop.out");
LL f[kMaxN], t, n, m;
pair<LL, LL> a[kMaxN];
struct Node {
LL id, val, tp;
Node(LL id, LL val, LL tp) : id(id), val(val), tp(tp) {}
friend bool operator<(const Node& a, const Node& b) {
return a.val != b.val ? a.val < b.val : a.tp < b.tp;
}
};
priority_queue<Node> q;
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> n >> m;
for (; q.size(); q.pop()) { }
fill(f + 1, f + m + 1, 0);
for (LL i = 1; i <= m; i++) {
cin >> a[i].first >> a[i].second;
q.emplace(i, a[i].first, 1);
q.emplace(i, a[i].second, 2);
}
LL ans = 0, sum = 0;
while (q.size()) {
auto [id, val, tp] = q.top();
q.pop();
if (!n) {
break;
}
if (tp == 1) {
sum += val;
ans = max(ans, sum);
f[id] = 1;
n--;
} else {
if (f[id]) {
sum += val * n;
ans = max(ans, sum);
break;
} else {
if (n == 1) {
continue;
}
ans = max(ans, sum + a[id].first + val * (n - 1));
}
}
}
cout << ans << '\n';
}
return 0;
}
```
## 划分
#线段树 #序列dp #ST表 #单调栈
> [!note] 思路
>
> 考虑 DP。由于和分段数量没有关系,所以设 $dp_i$ 表示若最后一段的最后一个元素为 $i$,所可以获得的最大价值。
>
> 转移如下:枚举最后一段实际的起点 $j$ $(1\le j\le i)$,那么前一段的终点就是 $j-1$,有 $dp_i=\max\limits_{j=1}^i(dp_{j-1}+f(j,i))$,其中 $f(j,i)$ 为 ${} b_k$ 其中 $b_k\ge a_{p}$ $(p\in[j,i])$。不难发现可以通过倒叙枚举 $j$ 维护最小值,时间复杂度优化为 $\mathcal O(n^2)$。
> [!important] 优化
>
> DP 状态设计已经足够简单了,如果优化转移复杂度呢?考虑维护全局最值,快速求出 $dp_{j-1}+f(j,i)$。
>
> 考虑线段树维护 $dp_{j-1}$ 和上面一大坨。首先我们思考,对于前面的所有状态每个的后方都加入一个 $a_i$,谁的最小值会改变谁不会?可以找到最后一个 ${} a_j\ge a_i$,$k\ge j$ 的就会改变,否则不会。
>
> > [!tip] 提示
> >
> > 可以用单调栈快速求,也可以用 ST 表维护 $a$ 的最小值然后二分。
>
> 现在对于 $k\in[j,i]$,$dp_k$ 后面的增量都会变成 $+b_i$。可以在线段树上直接做区间修改,拆成多段区间每段值都等于这一段 DP 最大值加上 $b_i$。
>
> 现在 $dp_i$ 就可以通过查询线段树全局最小值得到。由于是查询全局不需要写懒标记下放。
>
> 考虑 $dp_i$ 往后面的转移,作为 DP 值插入到线段树 $i$ 位置上即可。
找 $j$ 时间复杂度 $\mathcal O(n\textcolor{gray}{\log_2 n})$,计算 DP 时间复杂度 $\mathcal O(n\log_2 n)$。
```cpp
#include <fstream>
using namespace std;
using LL = long long;
const LL kMaxN = 1e5 + 10, kLog = 20;
ifstream cin("divide.in");
ofstream cout("divide.out");
struct Node {
LL dp, mx;
Node(): dp(-1e18), mx(-1e18) { }
} tr[4 * kMaxN];
LL a[kMaxN], b[kMaxN], f[kMaxN][kLog], lg[kMaxN], dp[kMaxN], n;
LL query(LL l, LL r) {
LL k = lg[r - l + 1];
return min(f[l][k], f[r - (1 << k) + 1][k]);
}
Node merge(const Node &a, const Node &b) {
Node res{};
res.dp = max(a.dp, b.dp);
res.mx = max(a.mx, b.mx);
return res;
}
// 修改增量
void modifyAdd(LL p, LL l, LL r, LL s, LL t, LL val) {
if (s <= l && r <= t) {
tr[p].mx = tr[p].dp + val;
return;
}
LL m = (l + r) / 2;
if (s <= m) {
modifyAdd(2 * p, l, m, s, t, val);
}
if (m < t) {
modifyAdd(2 * p + 1, m + 1, r, s, t, val);
}
tr[p] = merge(tr[2 * p], tr[2 * p + 1]);
}
// 修改dp值
void modifyDp(LL p, LL l, LL r, LL x, LL val) {
tr[p].dp = max(tr[p].dp, val);
if (l == r) {
return;
}
LL m = (l + r) / 2;
if (x <= m) {
modifyDp(2 * p, l, m, x, val);
} else {
modifyDp(2 * p + 1, m + 1, r, x, val);
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (LL i = 1; i <= n; i++) {
cin >> a[i];
f[i][0] = a[i];
}
for (LL i = 1; i <= n; i++) {
cin >> b[i];
}
for (LL j = 1; j < kLog; j++) {
for (LL i = 1; i + (1 << j) - 1 <= n; i++) {
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
for (LL i = 2; i <= n; i++) {
lg[i] = lg[i / 2] + 1;
}
fill(begin(tr), end(tr), Node());
modifyDp(1, 0, n, 0, dp[0] = 0);
for (LL i = 1; i <= n; i++) {
LL p = i;
for (LL l = 1, r = i; l <= r; ) {
LL m = (l + r) / 2;
if (query(m, i) == a[i]) {
p = m;
r = m - 1;
} else {
l = m + 1;
}
}
modifyAdd(1, 0, n, p - 1, i - 1, b[i]);
dp[i] = tr[1].mx;
modifyDp(1, 0, n, i, dp[i]);
}
cout << dp[n] << '\n';
return 0;
}
```
## 树上计数
#树链剖分 #点分治 #树形dp #启发式合并
我写的暴力。
> [!note] 思路
>
> 首先可以枚举三元组的中间点,使得三个点到枚举的点距离相同。这个点肯定不会在边上。
>
> 枚举后以 $i$ 为根开始,跑出所有点的深度。那么我们求的以 $i$ 为中心的三元组肯定深度相同,且在 $i$ 的儿子的不同子树当中。这是一个组合数问题,可以线性解决。
>
> 时间复杂度 $\mathcal O(n^2)$。成功获得 $30$ 分。
正解是长链剖分+树形 DP $\mathcal O(n)$,当然淀粉质+树上启发式合并 $\mathcal O(n\log^2 n)$ 也过了。