乘法减法按位或
首先 是非常好想的,但是发现优化很难。观察到值域较小,所以可以想到设 表示满足 最大的 。然而根本不会转移,所以直接趋势。
不过,观察式子中 是一个二次项,增长幅度特别快,而 是一次项,当 过大的时候后面的那一项几乎可以忽略,所以可以猜测答案 肯定在靠后的位置。
严格证明如下:
- [n] 假设 是最终最大的,那么我们需要推翻这个结论的话一定会有一个更大的。设更大的为 ,最好情况下该二元组的值为 ,而 最坏情况下值为 。所以有方程 ,解得 (也可以不取等),所以我们只需要枚举 即可。
时间复杂度 。
#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;
}队列
首先,把 个同学变成 个点, 要站在 前面可以看做 往 连一条边,那么最后会形成一个图,有多个连通块。如果一个连通块可以满足条件,那么这个连通块是一条链(因为不仅有前后关系还要相邻)。
如何判断每个连通块是否是链?每个点的入度、出度均不超过 ,且没有环。判环用拓扑排序即可。注意去重边。去掉重边之后若剩下 条边那么连通块数量为 ,这些连通块彼此之间没有前后关系因此方案数 。
时间复杂度 (去重边我带了个 )。
#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;
}权值
贡献可以拆为
- 所有满足条件序列的数量;
- 每个数的出现次数之和。
首先是第一问,对于长度为 且取值范围在 的整数序列,一共有 种。因此序列数量为 。这是一个等比数列,应用等比数列求和公式得到
至于每个数的出现次数之和,由于每个数的地位相等,因此每个数的出现次数之和也相等。对于 ,可以采用容斥,用总序列数量减去不使用 的序列数量,即
再将这个数乘上 即可得到所有数的出现次数之和,再加上一类贡献,得到最终答案
用快速幂和乘法逆元即可。时间复杂度 。
#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 分
瞎搜就行了。状态就是 表示当前到达第 个地点,当前深度为 ,然后它可以转移到 或者 ,累加以下贡献就行了。
50 分
可以发现上面的算法对于重复状态 在不同时刻结果不一样,但是结果肯定是越大越好,因此采用动态规划,将贡献列为最优化属性。设 表示当前到达第 个地点,深度为 所可以获得的最大饱腹值,转移方程 。
80 分
想到了 100 分的思路,但是用的是双 实现,或者直接用费用流模拟贪心过程了导致的。
- [b] 赛时被 暴力卡过去了,下次再搞大一点。
100 分
假设已经凿完了,我们可以将层数“抽丝剥茧”,把第一层抽下来,然后把第二层抽下来,不难发现第 层会完全被包含在第 层中,而且这两层左端点和右端点一定是不会重合的(因为如果重合了相邻深度差就会大于 )。
因此这道题目可以简化为“区间覆盖”的形式,即进行若干次选择,每次选择 对 的每一个地点都凿深一个单位,并且每个地点都只能作为左端点和作为右端点最多一次。
由于涉及到区间求和,可以整一个前缀和 ,这样我们选择一个区间的操作就可以简化为选择一前一后两个 相减。这时候就和买股票那题很像了。
我们枚举右端点 ,因为一个点只能最多作为右端点一次,因此我们需要在之前的左端点中找到 最小的然后让 减去 进行一次区间覆盖(当然如果结果是负数那就不要做了)。同时由于一个点只能作为左端点一次,因此 用完之后就需要把 扔掉。
这样不一定是最后的,原因是因为以后可能会有更大的 来减 会更优。但是注意到 ,也就是说先让 减去 之后,遇到后面更大的让它直接减 而不是 ,是和不选 直接让后面的减 是一样的,因此我们只需要把 额外放进队列,静待后面的 减去 “反悔”就行了。最后将 放入队列,作为以后的左端点。
时间复杂度 。
#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,然后查询就爬取整个一条链,每个节点 (使用 STL kth-element 可以到 )计算 值,链最长 ,所以总时间复杂度 。
不过由于树是随机生成的,期望高度 ,时间复杂度可能跑不到上界,但是我的数据还是太超模了所以卡不过去。
50 分
预先计算所有的 值,修改只需要修改所有的祖先。预处理 值时间复杂度 。由于树高期望 ,因此修改时间复杂度 查询和路径长度有关系,随机数据的话也许时间复杂度为 吧,不过实测会更慢。可以通过 的数据点
100 分
首先看到路径上查询,先写个树链剖分。值域太大了,但是只用到比大小因此可以离散化。首先快速查询第 大,我们通常采用权值线段树处理。查询 的值,这里有两种办法:对于每一个节点维护它的子树内所有 的值的权值线段树,然后 dfs 做一个线段树合并,这样就能快速查子树第 小;或者树套树,用树链剖分的普通线段树套上权值线段树,然后在多颗权值线段树上同时二分找第 小。由于后面的方案不影响整体时间复杂度,而且可以复用维护 的树套树,因此我选择用后面的方式实现。
现在我们获得了所有的 值,那就用树套树维护它。对于查询路径 值第 小,我们先树剖把他剖成若干条链,然后在存着权值线段树的根的普通线段树上查询,得到若干个可以覆盖整个询问路径的权值线段树的根,然后在这些权值线段树上同时二分第 小。时间复杂度 ,原因是树剖一个 ,外层线段树一个 ,内层一个 。
至于查询,往上遍历祖先,对 值和 值的树套树进行修改。由于树高期望 ,因此时间复杂度也是 的。
因此总时间复杂度为 。
不过我没有卡树上莫队,如果你实现得非常好是可以过的。要卡的就加个强制在线操作,不过这样子就无法离散化导致常数爆炸了。但是树上带修莫队本身复杂度也就不咋地,应该是过不了的。
#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;
}