## 乘法减法按位或
#数学 #结论 #位运算
首先 $\mathcal O(n^2)$ 是非常好想的,但是发现优化很难。观察到值域较小,所以可以想到设 $dp_i$ 表示满足 $a_j|a_k=i$ 最大的 $j\times k$。然而根本不会转移,所以直接趋势。
不过,观察式子中 $i\times j$ 是一个二次项,增长幅度特别快,而 $-k(a_i|a_j)$ 是一次项,当 $i,j$ 过大的时候后面的那一项几乎可以忽略,所以可以猜测答案 $(i,j)$ 肯定在靠后的位置。
严格证明如下:
- [n] 假设 $(n-1,n)$ 是最终最大的,那么我们需要推翻这个结论的话一定会有一个更大的。设更大的为 $(i,n)$,最好情况下该二元组的值为 $in-0=in$,而 $(n-1,n)$ 最坏情况下值为 $n(n-1)-2kn$。所以有方程 $in\ge n(n-1)-2kn$,解得 $i\ge n-2k-1$(也可以不取等),所以我们只需要枚举 $i\in [n-2k-1,n]$ 即可。
时间复杂度 $\mathcal O(kn)$。
```cpp
#include <iostream>
using namespace std;
using LL = long long;
const LL kMaxN = 3e5 + 5;
LL a[kMaxN], t, n, k;
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
LL ans = -1e18;
for (LL i = max(1LL, n - 2 * k - 1); i <= n; i++) {
for (LL j = 1; j <= n; j++) {
if (i != j) {
ans = max(ans, i * j - k * (a[i] | a[j]));
}
}
}
cout << ans << '\n';
}
return 0;
}
```
## 队列
#数学
首先,把 $n$ 个同学变成 $n$ 个点,$a$ 要站在 $b$ 前面可以看做 $a$ 往 $b$ 连一条边,那么最后会形成一个图,有多个连通块。如果一个连通块可以满足条件,那么这个连通块是一条链(因为不仅有前后关系还要相邻)。
如何判断每个连通块是否是链?每个点的入度、出度均不超过 $1$,且没有环。判环用拓扑排序即可。注意去重边。去掉重边之后若剩下 $m$ 条边那么连通块数量为 $n-m$,这些连通块彼此之间没有前后关系因此方案数 $(n-m)!$。
时间复杂度 $\mathcal O(n+m)$(去重边我带了个 $\log$)。
```cpp
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
using LL = long long;
const LL kMaxN = 2e5 + 5, kMod = 1e9 + 7;
LL id[kMaxN], od[kMaxN], n, m, cnt, sum, ans = 1;
vector<LL> e[kMaxN];
queue<LL> q;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (LL i = 1, u, v; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v);
}
for (LL i = 1; i <= n; i++) {
sort(e[i].begin(), e[i].end());
e[i].erase(unique(e[i].begin(), e[i].end()), e[i].end());
od[i] = e[i].size();
for (int j : e[i]) {
id[j]++;
}
}
if (count_if(id + 1, id + n + 1, [](LL i) {
return i > 1;
}) || count_if(od + 1, od + n + 1, [](LL i) {
return i > 1;
})) {
cout << "0\n";
return 0;
}
for (LL i = 1; i <= n; i++) {
if (!id[i]) {
q.push(i);
}
}
for (; q.size(); q.pop()) {
LL t = q.front();
cnt++;
for (LL i : e[t]) {
if (!--id[i]) {
q.push(i);
}
}
}
if (cnt != n) {
cout << "0\n";
return 0;
}
for (LL i = 1; i <= n; i++) {
sum += e[i].size();
}
sum = n - sum;
for (LL i = 1; i <= sum; i++) {
ans = ans * i % kMod;
}
cout << ans << '\n';
return 0;
}
```
## 权值
#数学 #等比数列求和 #快速幂 #乘法逆元
贡献可以拆为
1. 所有满足条件序列的数量;
2. 每个数的出现次数之和。
首先是第一问,对于长度为 $i$ 且取值范围在 $[1,n]$ 的整数序列,一共有 $n^i$ 种。因此序列数量为 ${} f(n,m)=\sum\limits_{i=0}^m n^i {}$。这是一个等比数列,应用等比数列求和公式得到
$
f(n,m)=\sum\limits_{i=0}^m n^i=
\begin{cases}
m+1&\text{if}~n=1 \\
\cfrac{n^{m+1}-1}{n-1}&\text{else}
\end{cases}
$
至于每个数的出现次数之和,由于每个数的地位相等,因此每个数的出现次数之和也相等。对于 $1$,可以采用容斥,用总序列数量减去不使用 $1$ 的序列数量,即
$
\sum_{i=0}^m n^i-\sum_{i=0}^n (n-1)^i=f(n,m)-f(n-1,m)
$
再将这个数乘上 $n$ 即可得到所有数的出现次数之和,再加上一类贡献,得到最终答案
$
f(n,m)+n\times f(n,m)-n\times f(n-1,m)=(n+1)f(n,m)-n\times f(n-1,m)
$
用快速幂和乘法逆元即可。时间复杂度 $\mathcal O(t(\log_2 m+\log_2 n))$。
```cpp
#include <iostream>
using namespace std;
using LL = long long;
const LL kMod = 1e9 + 7;
LL t, n, m;
LL pow(LL a, LL b) {
LL res = 1;
for (; b; b /= 2) {
b % 2 && (res = res * a % kMod);
a = a * a % kMod;
}
return res;
}
LL solve(LL n, LL m) {
if (n == 1) {
return m + 1;
}
return (pow(n, m + 1) - 1 + kMod) % kMod * pow(n - 1, kMod - 2) % kMod;
}
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> n >> m;
cout << ((n + 1) * solve(n, m) % kMod - n * solve(n - 1, m) % kMod + kMod) % kMod << '\n';
}
return 0;
}
```
## 钓鱼
#贪心 #优先队列
### 20 分
瞎搜就行了。状态就是 $(i,j)$ 表示当前到达第 $i$ 个地点,当前深度为 $j$,然后它可以转移到 $(i+1,j\pm 1)$ 或者 $(i+1,j)$,累加以下贡献就行了。
### 50 分
可以发现上面的算法对于重复状态 $(i,j)$ 在不同时刻结果不一样,但是结果肯定是越大越好,因此采用动态规划,将贡献列为最优化属性。设 $dp_{i,j}$ 表示当前到达第 $i$ 个地点,深度为 $j$ 所可以获得的最大饱腹值,转移方程 $dp_{i,j}=\max(dp_{i-1,j},dp_{i-1,j\pm 1})+j\times(B_i-A_i)$。
### 80 分
想到了 100 分的思路,但是用的是双 $\log$ 实现,或者直接用费用流模拟贪心过程了导致的。
- [b] 赛时被 $\mathcal O(n^2)$ 暴力卡过去了,下次再搞大一点。
### 100 分
假设已经凿完了,我们可以将层数“抽丝剥茧”,把第一层抽下来,然后把第二层抽下来,不难发现第 $K$ 层会完全被包含在第 $K-1$ 层中,而且这两层左端点和右端点一定是不会重合的(因为如果重合了相邻深度差就会大于 $1$)。
因此这道题目可以简化为“区间覆盖”的形式,即进行若干次选择,每次选择 $[L,R]$ 对 $L\sim R$ 的每一个地点都凿深一个单位,并且每个地点都只能作为左端点和作为右端点最多一次。
由于涉及到区间求和,可以整一个前缀和 $P_i=\sum\limits_{j=1}^i(B_j-A_j)$,这样我们选择一个区间的操作就可以简化为选择一前一后两个 $P_i$ 相减。这时候就和买股票那题很像了。
我们枚举右端点 $R$,因为一个点只能最多作为右端点一次,因此我们需要在之前的左端点中找到 $P_{L-1}$ 最小的然后让 $P_R$ 减去 $P_{L-1}$ 进行一次区间覆盖(当然如果结果是负数那就不要做了)。同时由于一个点只能作为左端点一次,因此 $L$ 用完之后就需要把 $L$ 扔掉。
这样不一定是最后的,原因是因为以后可能会有更大的 $P_R$ 来减 $P_{L-1}$ 会更优。但是注意到 $P_k-P_i=(P_k-P_j)+(P_j-P_i)$,也就是说先让 $P_{R}$ 减去 $P_{L-1}$ 之后,遇到后面更大的让它直接减 $P_R$ 而不是 $P_{L-1}$,是和不选 $P_{R}$ 直接让后面的减 $P_{L}$ 是一样的,因此我们只需要把 $P_R$ 额外放进队列,静待后面的 $P$ 减去 $P_R$ “反悔”就行了。最后将 $P_R$ 放入队列,作为以后的左端点。
时间复杂度 $\mathcal O(n\log_2 n)$。
```cpp
#include <iostream>
#include <queue>
using namespace std;
using LL = long long;
const LL kMaxN = 1e6 + 5;
LL p[kMaxN], n, ans;
priority_queue<LL, vector<LL>, greater<>> q;
int main() {
// 输入并作前缀和
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (LL i = 1, a, b; i <= n; i++) {
cin >> a >> b;
p[i] = p[i - 1] + b - a;
}
// 反悔贪心
q.push(p[0]);
for (LL i = 1; i <= n; i++) {
if (p[i] - q.top() > 0) { // 进行区间覆盖
ans += p[i] - q.top();
q.pop();
q.push(p[i]); // 留下反悔机会
}
q.push(p[i]);
}
cout << ans << '\n';
return 0;
}
```
## 渴鹅村
#线段树 #二分 #离散化 #树链剖分
本题主要就是靠随机树来保证做法复杂度的
### 20 分
纯暴力,修改只改 `a[x] = y`,然后查询就爬取整个一条链,每个节点 $\mathcal O(n \log_2 n)$(使用 STL kth-element 可以到 $\mathcal O(n)$)计算 $v$ 值,链最长 $\mathcal O(n)$,所以总时间复杂度 $\mathcal O(m \times n^2 (\times \log n))$。
不过由于树是随机生成的,期望高度 $\log n$,时间复杂度可能跑不到上界,但是我的数据还是太超模了所以卡不过去。
### 50 分
预先计算所有的 $v$ 值,修改只需要修改所有的祖先。预处理 $v$ 值时间复杂度 ${} \mathcal O(n^2 (\log n)) {}$。由于树高期望 $\log n$,因此修改时间复杂度 $\mathcal O(n (\log n))$ 查询和路径长度有关系,随机数据的话也许时间复杂度为 $\mathcal O(\log n (\log n))$ 吧,不过实测会更慢。可以通过 $n,m\le 1000$ 的数据点
### 100 分
首先看到路径上查询,先写个树链剖分。值域太大了,但是只用到比大小因此可以离散化。首先快速查询第 $k$ 大,我们通常采用权值线段树处理。查询 $v$ 的值,这里有两种办法:对于每一个节点维护它的子树内所有 $a$ 的值的权值线段树,然后 dfs 做一个线段树合并,这样就能快速查子树第 $k$ 小;或者树套树,用树链剖分的普通线段树套上权值线段树,然后在多颗权值线段树上同时二分找第 $k$ 小。由于后面的方案不影响整体时间复杂度,而且可以复用维护 $v$ 的树套树,因此我选择用后面的方式实现。
现在我们获得了所有的 $v$ 值,那就用树套树维护它。对于查询路径 $v$ 值第 $k$ 小,我们先树剖把他剖成若干条链,然后在存着权值线段树的根的普通线段树上查询,得到若干个可以覆盖整个询问路径的权值线段树的根,然后在这些权值线段树上同时二分第 $k$ 小。时间复杂度 $\mathcal O(\log^3 n)$,原因是树剖一个 $\log$,外层线段树一个 $\log$,内层一个 $\log$。
至于查询,往上遍历祖先,对 $a$ 值和 $v$ 值的树套树进行修改。由于树高期望 ${} \log n$,因此时间复杂度也是 ${} \mathcal O(\log^3 n) {}$ 的。
因此总时间复杂度为 $\mathcal O(m \log^3 n)$。
不过我没有卡树上莫队,如果你实现得非常好是可以过的。要卡的就加个强制在线操作,不过这样子就无法离散化导致常数爆炸了。但是树上带修莫队本身复杂度也就不咋地,应该是过不了的。
```cpp
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
template <class T>
using Ref = const T &;
const int kMaxN = 1e5 + 5, kMaxM = 128 * kMaxN;
// 操作类
struct Operation {
int o, x, y, k;
};
int a[kMaxN], k[kMaxN], fa[kMaxN], dep[kMaxN], siz[kMaxN], son[kMaxN], top[kMaxN], dfn[kMaxN], rnk[kMaxN], v[kMaxN], n, m, cnt;
int val[kMaxN], q;
vector<int> e[kMaxN];
vector<Operation> opt;
// 权值线段树动态开点
struct Node {
int lc, rc, sum;
};
// 权值线段树
struct ValueSegmentTree {
Node tr[kMaxM];
int cnt;
// 合并儿子节点
void pushUp(int p) {
tr[p].sum = tr[tr[p].lc].sum + tr[tr[p].rc].sum;
}
// 修改 x 处值增加 d
void modify(int &p, int l, int r, int x, int d) {
if (!p) {
p = ++cnt;
}
if (l == r) {
tr[p].sum += d;
return;
}
int m = (l + r) / 2;
if (x <= m) {
modify(tr[p].lc, l, m, x, d);
} else {
modify(tr[p].rc, m + 1, r, x, d);
}
pushUp(p);
}
// 多个结点同时线段树二分查询第 k 小
int query(Ref<vector<int>> &v, int l, int r, int k) {
if (l == r) {
return l;
}
int sum = 0;
for (int p : v) {
sum += tr[tr[p].lc].sum;
}
int m = (l + r) / 2;
if (k <= sum) {
vector<int> nxt;
for (int p : v) {
nxt.push_back(tr[p].lc);
}
return query(nxt, l, m, k);
}
vector<int> nxt;
for (int p : v) {
nxt.push_back(tr[p].rc);
}
return query(nxt, m + 1, r, k - sum);
}
} w_tr, v_tr;
int w_rt[4 * kMaxN], v_rt[4 * kMaxN];
// 在树链线段树上修改 x 点。在 t 权值线段上修改,根为 rt
void modify(ValueSegmentTree &t, int *rt, int p, int l, int r, int x, int val, int d) {
t.modify(rt[p], 1, q, val, d);
if (l == r) {
return;
}
int m = (l + r) / 2;
if (x <= m) {
modify(t, rt, 2 * p, l, m, x, val, d);
} else {
modify(t, rt, 2 * p + 1, m + 1, r, x, val, d);
}
}
// 获取同一条树链上 s~t 的权值线段树的所有根
void getRoot(int *rt, int p, int l, int r, int s, int t, vector<int> &res) {
if (s <= l && r <= t) {
res.push_back(rt[p]);
return;
}
int m = (l + r) / 2;
if (s <= m) {
getRoot(rt, 2 * p, l, m, s, t, res);
}
if (m < t) {
getRoot(rt, 2 * p + 1, m + 1, r, s, t, res);
}
}
// 获取离散化之后的值
int getHash(int x) {
return lower_bound(val + 1, val + q + 1, x) - val;
}
// 处理出父亲、深度、子树大小和重儿子
void dfs1(int x, int f) {
fa[x] = f;
dep[x] = dep[f] + 1;
siz[x] = 1;
for (int i : e[x]) {
if (i == f) {
continue;
}
dfs1(i, x);
siz[x] += siz[i];
if (siz[i] > siz[son[x]]) {
son[x] = i;
}
}
}
// 获取链头、dfs 序
void dfs2(int x, int t) {
top[x] = t;
dfn[x] = ++cnt;
rnk[cnt] = x;
if (!son[x]) {
return;
}
dfs2(son[x], t);
for (int i : e[x]) {
if (i == fa[x] || i == son[x]) {
continue;
}
dfs2(i, i);
}
}
// 查询子树中 A 值的第 k 小
int querySubtree(int x, int k) {
vector<int> v;
getRoot(w_rt, 1, 1, n, dfn[x], dfn[x] + siz[x] - 1, v);
return w_tr.query(v, 1, q, k);
}
// 查询路径上 V 值的第 k 小
int queryPath(int x, int y, int k) {
vector<int> v;
for (; top[x] != top[y]; x = fa[top[x]]) {
if (dep[top[x]] < dep[top[y]]) {
swap(x, y);
}
getRoot(v_rt, 1, 1, n, dfn[top[x]], dfn[x], v);
}
if (dep[x] > dep[y]) {
swap(x, y);
}
getRoot(v_rt, 1, 1, n, dfn[x], dfn[y], v);
return v_tr.query(v, 1, q, k);
}
int main() {
// 输入
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
val[++q] = a[i];
}
for (int i = 1; i <= n; i++) {
cin >> k[i];
}
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
for (int i = 1, o, x, y, k; i <= m; i++) {
cin >> o >> x >> y;
if (o == 1) {
opt.push_back(Operation {o, x, y});
val[++q] = y;
} else {
cin >> k;
opt.push_back(Operation {o, x, y, k});
}
}
// 离散化
sort(val + 1, val + q + 1);
q = unique(val + 1, val + q + 1) - val - 1;
// 树链剖分
dfs1(1, 0);
dfs2(1, 1);
// 插入 A 值到线段树
for (int i = 1; i <= n; i++) {
modify(w_tr, w_rt, 1, 1, n, dfn[i], getHash(a[i]), 1);
}
// 插入 V 值到线段树
for (int i = 1; i <= n; i++) {
v[i] = querySubtree(i, k[i]);
modify(v_tr, v_rt, 1, 1, n, dfn[i], v[i], 1);
}
// 操作与询问
for (auto &i : opt) {
auto &&[o, x, y, k] = i;
if (o == 1) {
int old = getHash(a[x]), now = getHash(y);
a[x] = y;
modify(w_tr, w_rt, 1, 1, n, dfn[x], old, -1);
modify(w_tr, w_rt, 1, 1, n, dfn[x], now, 1);
for (int cur = x; cur; cur = fa[cur]) {
old = v[cur];
now = querySubtree(cur, ::k[cur]);
if (old != now) {
modify(v_tr, v_rt, 1, 1, n, dfn[cur], old, -1);
modify(v_tr, v_rt, 1, 1, n, dfn[cur], now, 1);
v[cur] = now;
}
}
} else {
cout << val[queryPath(x, y, k)] << '\n';
}
}
return 0;
}
```