2025 广东省队集训
2025 广东省队集训
Day 0 - 试机
石堆分裂
有一堆数量为 \(n\) 的石子,给定常数 \(m\) ,每次操作为:设当前剩下 \(k\) 堆石子 \(a_{1 \sim k}\) ,需要指定 \(k\) 个非负整数 \(b_{1 \sim k}\) 满足 \(b_i \leq a_i\) 且 \(\sum_{i = 1}^k b_i \leq m\) ,然后将第 \(i = 1, 2, \cdots, k\) 堆石子分裂为 \(b_i, a_i - b_i\) 两堆。
最小化将其分为 \(n\) 堆石子(每堆一个)的操作次数。
\(n \leq 10^9\)
若一种方案操作了 \(k\) 次,考虑将每次操作中被分出去的石子标为 \(1\) ,不动的标为 \(0\) ,则每个石子对应一个 \(k\) 位二进制串,且它们两两不同,且每一位 \(1\) 的总数 \(\leq m\) 。
考虑二分答案 \(mid\) ,然后考虑构造。每次贪心使用 \(\mathrm{popcount}\) 最小的数,具体地,枚举 \(i = 0, 1, \cdots, mid\) ,用 \(\mathrm{popcount} = i\) 的所有数给二进制串赋值,共有 \(\binom{mid}{i}\) 个,且每列会增加 \(\frac{i \times \binom{mid}{i}}{mid} = \binom{mid - 1}{i - 1}\) 个 \(1\) ,填满 \(n\) 个数后检查每一位 \(1\) 的总数 \(\leq m\) 即可。
注意特殊处理一下最后一种 \(\mathrm{popcount}\) 的数可能没有全部填完的情况,设此时 \(\mathrm{popcount} = x\) ,只填了 \(y\) 个串,则 \(1\) 最多的那一列的个数为 \(\lceil \frac{xy}{mid} \rceil\) 。
时间复杂度 \(O(\log^2 n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
int n, m;
inline bool check(int mid) {
ll mul = 1, surplus = n, num = 0;
for (int i = 0; i <= mid; ++i) {
if (mul >= surplus)
return num + (surplus * i + mid - 1) / mid <= m;
surplus -= mul, num += mul * i / mid, mul = mul * (mid - i) / (i + 1);
}
return false;
}
signed main() {
freopen("split.in", "r", stdin);
freopen("split.out", "w", stdout);
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
if (n == 1) {
puts("0");
continue;
}
int l = 1, r = n, ans = n;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
printf("%d\n", ans);
}
return 0;
}
白
给定一个长度为 \(n\) 的排列 \(p_{1 \sim n}\) ,定义一次操作为:选择若干个不交的子区间,将子区间内部分别排序。求若干次操作后能得到的不同排列数量 \(\bmod 998244353\) 。
\(n \leq 2 \times 10^5\)
设 \(f_i\) 表示 \(i\) 前缀的答案,则 \(f_{j - 1} \to f_i\) 的条件为不存在 \(k \in [j, i)\) 满足将区间 \([i, j]\) 排序等价于分别将区间 \([j, k]\) 和区间 \([k + 1, i]\) 排序。
先考虑如何暴力求合法的 \(j\) 的集合 \(S_i\) ,考虑从右到左模拟插入排序。具体地,枚举 \(j = i - 1, i - 2, \cdots, 1\) ,维护当前的 \(S_i\) 和其中最左的位置 \(l\) 以及区间 \([l, i]\) 的最小值 \(v\) ,初始时 \(S = \{ i \}, l = i, v = p_i\) 。枚举到 \(j\) 时,若 \(p_j > v\) 则 \(j\) 合法,将其加入 \(S\) ,并更新 \(l, v\) 。
考虑优化这个过程,发现:
- 令 \(x\) 为 \(i\) 左边第一个满足 \(p_x < p_i\) 的位置,则 \([x + 1, i] \subseteq S\) 。
- 令 \(y\) 表示 \(x\) 左边第一个满足 \(p_y > p_i\) 的位置,则 \(S_i\) 一定不包含 \([y + 1, x]\) 。
令 \(z\) 为区间 \([y + 1, x]\) 中 \(p\) 最小的位置,则对求 \(S_i\) 和 \(S_z\) 的过程中,枚举到 \(y\) 时均有 \(l = y, v = p_z\) ,因此 \(S_i = [x + 1, i] \cup (S_z \setminus [y + 1, z])\) 。而 \([y + 1, z] \subseteq S_z\) ,因此直接维护前缀和即可。
求 \(x, y, z\) 可以用数据结构做到 \(O(\log n)\) ,简单特判一下不存在 \(x\) 或 \(y\) 的情况,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7, LOGN = 19;
int a[N], f[N], s[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
namespace ST {
int mn[LOGN][N], mx[LOGN][N];
inline int cmpmn(const int &x, const int &y) {
return a[x] < a[y] ? x : y;
}
inline int cmpmx(const int &x, const int &y) {
return a[x] > a[y] ? x : y;
}
inline void prework() {
iota(mn[0] + 1, mn[0] + n + 1, 1);
iota(mx[0] + 1, mx[0] + n + 1, 1);
for (int j = 1; j <= __lg(n); ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
mn[j][i] = cmpmn(mn[j - 1][i], mn[j - 1][i + (1 << (j - 1))]);
mx[j][i] = cmpmx(mx[j - 1][i], mx[j - 1][i + (1 << (j - 1))]);
}
}
inline int querymn(int l, int r) {
int k = __lg(r - l + 1);
return cmpmn(mn[k][l], mn[k][r - (1 << k) + 1]);
}
inline int querymx(int l, int r) {
int k = __lg(r - l + 1);
return cmpmx(mx[k][l], mx[k][r - (1 << k) + 1]);
}
} // namespace ST
signed main() {
freopen("bai.in", "r", stdin);
freopen("bai.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
ST::prework();
f[0] = s[0] = 1;
for (int i = 1; i <= n; ++i) {
int x = i;
for (int j = __lg(i); ~j; --j)
if (x >= (1 << j) && a[ST::querymn(x - (1 << j) + 1, x)] >= a[i])
x -= 1 << j;
if (!x) {
s[i] = add(s[i - 1], f[i] = s[i - 1]);
continue;
}
int y = x;
for (int j = __lg(x); ~j; --j)
if (y >= (1 << j) && a[ST::querymx(y - (1 << j) + 1, y)] <= a[i])
y -= 1 << j;
if (!y) {
s[i] = add(s[i - 1], f[i] = dec(s[i - 1], s[x - 1]));
continue;
}
int z = ST::querymn(y + 1, x);
s[i] = add(s[i - 1], f[i] = add(dec(s[i - 1], s[x - 1]), dec(f[z], dec(s[z - 1], s[y - 1]))));
}
printf("%d", f[n]);
return 0;
}
Day 1 - 马梓航
最小生成树
source:QOJ4415
给出一张无向连通图,第 \(i\) 条边有两个权值 \(a_i, b_i\) 。
对于 \(k = 0, 1, \cdots, m\) ,求恰好选择 \(k\) 条边的权值为 \(a_i\) ,其余边的权值为 \(b_i\) 时 MST 的最大值。
\(n \leq 9\) ,\(m \leq 100\)
考虑 Kruskal 算法,从小到大考虑边。
首先将一条边拆为 \(a, b\) 两条边,将这 \(2m\) 条边排序后升序考虑,此时每一条边会作为 \(a, b\) 各出现一次。
若当前边是第一次出现,则可以考虑选或不选,否则若两个连通块不连通,则必须选这条边,否则必须不选该边。
设 \(f_{i, j, S}\) 表示考虑了前 \(i\) 条边,选了 \(j\) 条 \(a\) 边,连通块的最小表示法为 \(S\) 的答案,转移不难做到 \(O(m^2 \mathrm{Bell}(n))\) ,其中 \(\mathrm{Bell}(9) = 21147\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 11, M = 1e2 + 7;
struct Edge {
int u, v, a, b;
} e[M];
map<vector<int>, int> f[M], g[M];
pair<int, int> edg[M << 1];
bool vis[M];
int n, m;
inline vector<int> merge(vector<int> fa, int x, int y) {
x = fa[x], y = fa[y];
if (x > y)
swap(x, y);
for (int &it : fa)
if (it == y)
it = x;
return fa;
}
inline void update(int k, vector<int> fa, int val) {
if (g[k].find(fa) == g[k].end())
g[k][fa] = val;
else
g[k][fa] = max(g[k][fa], val);
}
signed main() {
freopen("mst.in", "r", stdin);
freopen("mst.out", "w", stdout);
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].a, &e[i].b);
--e[i].u, --e[i].v;
edg[i] = make_pair(i, 0), edg[i + m] = make_pair(i, 1);
}
sort(edg + 1, edg + m * 2 + 1, [](const pair<int, int> &a, const pair<int, int> &b) {
return (a.second ? e[a.first].b : e[a.first].a) < (b.second ? e[b.first].b : e[b.first].a);
});
vector<int> fa(n);
iota(fa.begin(), fa.end(), 0);
f[0][fa] = 0;
for (int i = 1; i <= m * 2; ++i) {
int id = edg[i].first, u = e[id].u, v = e[id].v, w = (edg[i].second ? e[id].b : e[id].a);
if (vis[id]) {
for (int j = 0; j <= m; ++j)
for (auto it : f[j]) {
if (it.first[u] == it.first[v])
update(j, it.first, it.second);
else
update(j, merge(it.first, u, v), it.second + w);
}
} else {
for (int j = 0; j <= m; ++j)
for (auto it : f[j]) {
update(j + edg[i].second, it.first, it.second);
if (it.first[u] == it.first[v])
update(j + !edg[i].second, it.first, it.second);
else
update(j + !edg[i].second, merge(it.first, u, v), it.second + w);
}
vis[id] = true;
}
for (int j = 0; j <= m; ++j)
f[j] = g[j], g[j].clear();
}
fill(fa.begin(), fa.end(), 0);
for (int i = 0; i <= m; ++i)
printf("%d\n", f[i][fa]);
return 0;
}
操作
source:Gym 103428C
初始有一个变量 \(w = 1\) ,给定质数 \(p\) 和 \(n\) 个操作,操作有两种:
- \(w \gets x\) 。
- \(w \gets wx \bmod p\) 。
可以任意排列操作顺序,求 \(0 \sim p - 1\) 中有多少个数字不能成为最终操作完的 \(w\) 。
\(n, p \leq 10^6\)
考虑将每个数用原根的次幂表示,这样第二个操作就变成加法了。
问题转化为 \(\bmod p\) 意义下的 01 背包,不难用 bitset
做到 \(O(\frac{np}{\omega})\) ,无法通过。
考虑优化,发现每个位置只会被有效更新一次(从 \(0\) 变成 \(1\)),总共的有效更新次数只有 \(O(p)\) 次,考虑从这里入手。
考虑分治,需要判断 \([l, r] \to [(l + k) \bmod (p - 1), (r + k) \bmod (p - 1)]\) 是否会产生有效更新,若无法产生有效更新则退出。此时每个有效更新都会对复杂度产生 \(O(\log p)\) 的贡献,因此该部分复杂度为 \(O(p \log p)\) 。
考虑一个简单的判定方式:若 \([l, r]\) 与 \([(l + k) \bmod (p - 1), (r + k) \bmod (p - 1)]\) 的哈希值不同,则认为这个 \([l, r]\) 可以产生有效更新,递归到 \(l = r\) 时判断一下是否是 \(1 \to 0\) 即可。
这样处理唯一的问题在于会多考虑到 \(0 \to 1\) 的无用贡献,由于背包是在模意义下进行的,因此 \((i + k) \bmod (p - 1)\) 必定会形成一个环。环上所有的 \((1, 0)\) 都会对应一个 \((0, 1)\) ,因此不会影响时间复杂度。
用树状数组维护区间哈希值,视 \(n, p\) 同阶,时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int base = 233, Mod = 1e9 + 7, ibase = 90128756;
const int N = 1e6 + 7;
vector<int> upd;
int pw[N], ipw[N], id[N];
int n, m, g;
inline bool check(int g) {
memset(id, -1, sizeof(int) * m);
for (int i = 0, mul = 1; i <= m - 2; ++i, mul = 1ll * mul * g % m) {
if (~id[mul])
return false;
id[mul] = i;
}
return true;
}
namespace BIT {
int c[N];
inline void clear() {
memset(c + 1, 0, sizeof(int) * (m - 1));
}
inline void update(int x) {
int k = pw[x];
for (++x; x <= m - 1; x += x & -x)
c[x] = (c[x] + k) % Mod;
}
inline int query(int x) {
int res = 0;
for (++x; x; x -= x & -x)
res = (res + c[x]) % Mod;
return res;
}
} // namespace BIT
inline int gethash(int l, int r) {
if (l <= r)
return 1ll * (BIT::query(r) - BIT::query(l - 1) + Mod) * ipw[l] % Mod;
else
return (gethash(l, m - 2) + 1ll * BIT::query(r) * pw[m - l - 1]) % Mod;
}
void solve(int l, int r, int k) {
if (gethash(l, r) == gethash((l + k) % (m - 1), (r + k) % (m - 1)))
return;
if (l == r) {
if (gethash(l, r))
upd.emplace_back(l);
return;
}
int mid = (l + r) >> 1;
solve(l, mid, k), solve(mid + 1, r, k);
}
signed main() {
freopen("operation.in", "r", stdin);
freopen("operation.out", "w", stdout);
pw[0] = ipw[0] = 1;
for (int i = 1; i < N; ++i) {
pw[i] = 1ll * pw[i - 1] * base % Mod;
ipw[i] = 1ll * ipw[i - 1] * ibase % Mod;
}
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &m, &n);
g = -1;
for (int i = 1; i < m; ++i)
if (check(i)) {
g = i;
break;
}
vector<int> vec[2];
int flag[2] = {0, 0};
for (int i = 1; i <= n; ++i) {
int op, x;
scanf("%d%d", &op, &x);
if (x)
vec[op].emplace_back(id[x]);
else
flag[op] = 1;
}
if (vec[0].empty()) {
printf("%d\n", m - 1);
continue;
}
sort(vec[0].begin(), vec[0].end()), vec[0].erase(unique(vec[0].begin(), vec[0].end()), vec[0].end());
BIT::clear();
for (int it : vec[0])
BIT::update(it);
for (int it : vec[1]) {
solve(0, m - 2, it);
for (int x : upd)
BIT::update((x + it) % (m - 1));
upd.clear();
}
int ans = flag[0] | flag[1];
for (int i = 0; i < m - 1; ++i)
if (gethash(i, i))
++ans;
printf("%d\n", m - ans);
}
return 0;
}
排列
source:P9265
给出一个排列,\(i, j\) 之间存在无向边当且仅当 \((i, j)\) 位置是一个逆序对。
对于 \(x = 1, 2, \cdots, n\) ,求 \(x\) 到所有点的最短路长度之和,无法到达的点最短路视为 \(0\) 。
\(n \leq 2 \times 10^5\)
首先可以发现,若某个 \(i\) 满足 \(\forall j \leq i, p_j \leq i\) ,则 \([1, i]\) 与 \([i + 1, n]\) 之间不可能相互到达,所以整个图被分成两个连通块,下面假设整个排列连通。
考虑将问题转化为 \(\sum_t \sum_x [\mathrm{dist}(i, x) \geq t]\) ,不妨只考虑 \(x < i\) 的情况,\(x > i\) 只要将序列翻转再做一次即可。
- 当 \(t = 1\) 时,由于整个排列连通,因此该部分贡献为 \(i - 1\) 。
- 当 \(t = 2\) 时,\(p_x < p_i\) 的 \(x\) 都不能一步到达。
- 当 \(t = 3\) 时,考虑最短路为 \(2\) 的 \(x\) 的数量。如果第一步向左走,则一定是走到最左侧的一个点最优,记为 \(l\) 。如果第一步向右走,则一定是走到 \(p\) 最小的点最优,记为 \(r\) 。若 \(l, r\) 均不能到达 \(x\) ,则 \(\mathrm{dist}(i, x) \geq 3\) ,该条件相当于 \([x < l] \and [p_x < p_r]\) 。
\(t\) 更大时也是类似的,如果不是一步可以到达,从一个点要么走到左侧最靠左的点,要么走到右侧值最小的点,并且之后都是交替两种方法行动,而这样能够走到的两个点对应了所有不能走到的点的一个右边界和上边界。
记 \(u_i\) 表示 \(i\) 向左走能走到的最靠左的点,\(d_i\) 表示 \(i\) 向右能走到的值最小的点,\((a_t, b_t)\) 表示 \(\mathrm{dist}(i, j) \geq t\) 时需要满足 \([j < a_t] \and [p_j < b_t]\) ,则可以得出结论:
暴力求出 \(u, d\) 后枚举 \(t\) ,之后对答案的贡献形如矩形查询元素数量,预处理后可以做到 \(O(n^2)\) ,无法通过。
考虑将 \((x, y)\) 向 \((u_y, d_x)\) 连边,则会形成一棵内向树,可以证明所有从 \((i, i)\) 开始能够到达的点是 \(O(n \sqrt{n})\) 级别的。
证明:忽略 \((i, i)\) ,根据点的生成过程容易归纳证明每个有用的点 \((x, y)\) 均满足 \(x < y\) 且 \(p_x > p_y\) 。
对于每个 \(x\) ,假设所有 \(y > x\) 且 \(p_y < p_x\) 的 \(y\) 构成序列 \(b_1 < b_2 < \cdots < b_k\) ,考虑取出前 \(\sqrt{n}\) 个元素和 \(x\) 组成二元组 \((x, b_i)\) ,则所有这样的元素对个数不超过 \(O(n \sqrt{n})\) 。下面称 \(i > \sqrt{n}\) 的二元组 \((x, b_i)\) 为二类二元组,考虑统计二类二元组的数量。
考虑对每个 \(i\) ,从 \((i, i)\) 开始移动时经过的二类二元组数量,该结果一定不小于二类二元组数量(只会算重而不会算漏)。对于一个 \(x\) ,由于 \(b_1 > x\) ,因此二类二元组一定满足 \(y - x > \sqrt{n}\) ,而从 \((x, y)\) 下一步会跳到 \((u_y, d_x)\) ,显然有 \(u_y \leq x, d_x \leq y\) 。因此遇到一个二类二元组之后,跳一次会使得 \(x + y\) 至少减去 \(\sqrt{n}\) ,且 \(x + y\) 是不增的,从而一次跳的过程只会经过 \(O(n \sqrt{n})\) 个二类二元组。
因此能够到达的点是 \(O(n \sqrt{n})\) 级别的。
考虑统计信息,需要做 \(O(n \sqrt{n})\) 次矩形查询,直接上二维数点是 \(O(n \sqrt{n} \log n)\) 的,无法通过。注意到对于 \((x, y)\) 的儿子 \((x', y')\) ,一定满足 \(x \leq x'\) ,因此可以从小到大对 \(x\) 扫描线,做单点加区间求和,这样求出来的贡献是可以直接从父节点加过来的。只需要做 \(O(n)\) 次单点加,\(O(n \sqrt{n})\) 次区间求和,使用 \(O(\sqrt{n}) - O(1)\) 的分块即可做到 \(O(n \sqrt{n})\) 。
接下来考虑建树。注意到 \(u\) 位置具有单调性,\(d\) 值具有单调性,因此可以倒序扫描 \(x\) ,对每个 \(x\) 维护有用的点对 \((x, y)\) 。则扫到 \(x\) 时加入 \((x, x)\) ,然后对于当前每个存在的点对 \((x, y)\) ,将它下一步 \((x', y')\) 在 \(x'\) 对应维护的点对中插入。
由于 \(u, d\) 的单调性,一个 \(x\) 内部的 \(y\) 时单调的,因此插入时判定是否已存在只需判定维护的最后一个点对是否为 \((x', y')\) 即可。该部分时间复杂度为 \(O(n \sqrt{n})\) 。
总时间复杂度 \(O(n \sqrt{n})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, M = 2e7 + 7;
vector<pair<int, int> > vec[N];
ll ans[N], val[M];
int a[N], p[N], q[N], dw[N], up[N], nxt[M];
int n;
namespace FK {
int s[N], tag[N];
int block;
inline void prework(int n) {
block = sqrt(n);
memset(s + 1, 0, sizeof(int) * n);
memset(tag + 1, 0, sizeof(int) * ((n + block - 1) / block));
}
inline void update(int x, int k) {
int bel = (x + block - 1) / block;
for (int i = x; i <= min(n, bel * block); ++i)
s[i] += k;
for (int i = bel + 1; i <= (n + block - 1) / block; ++i)
tag[i] += k;
}
inline int query(int x) {
return s[x] + tag[(x + block - 1) / block];
}
} // namespace FK
inline vector<ll> calc(int n) {
for (int i = n, mn = 0; i; --i) {
if (!mn || p[i] < p[mn])
mn = i;
dw[i] = mn;
}
for (int i = 1; i <= n; ++i)
q[p[i]] = i;
for (int i = n, mn = n + 1; i; --i) {
if (q[i] < mn)
mn = q[i];
up[q[i]] = mn;
}
for (int i = 1; i <= n; ++i)
vec[i].clear();
int tot = 0;
for (int i = n; i; --i) {
vec[i].insert(vec[i].begin(), make_pair(i, ++tot));
for (int j = 0; j < vec[i].size(); ++j) {
int x = up[vec[i][j].first], y = dw[i];
if (vec[x].empty() || vec[x].back().first != y)
vec[x].emplace_back(y, ++tot);
nxt[vec[i][j].second] = vec[x].back().second;
}
}
FK::prework(n);
vector<ll> res(n);
for (int i = 1; i <= n; ++i) {
res[i - 1] += i - 1;
for (int j = vec[i].size() - 1; ~j; --j) {
int x = p[vec[i][j].first], y = vec[i][j].second;
val[y] = val[nxt[y]] + FK::query(x - 1);
}
res[i - 1] += val[vec[i][0].second], FK::update(p[i], 1);
}
memset(val + 1, 0, sizeof(ll) * tot);
memset(nxt + 1, 0, sizeof(int) * tot);
return res;
}
inline void solve(int l, int r) {
int n = 0;
for (int i = l; i <= r; ++i)
p[++n] = a[i] - l + 1;
vector<ll> res = calc(n);
for (int i = l; i <= r; ++i)
ans[i] += res[i - l];
reverse(p + 1, p + n + 1);
for (int i = 1; i <= n; ++i)
p[i] = n - p[i] + 1;
res = calc(n);
for (int i = l; i <= r; ++i)
ans[i] += res[r - i];
}
signed main() {
freopen("permutation.in", "r", stdin);
freopen("permutation.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1, mx = 0, lst = 0; i <= n; ++i) {
mx = max(mx, a[i]);
if (mx == i) {
if (lst + 1 < i)
solve(lst + 1, i);
lst = i;
}
}
for (int i = 1; i <= n; ++i)
printf("%lld ", ans[i]);
return 0;
}
Day 2 - 黄建恒
火力全开
有 \(n\) 个敌人和 \(m\) 个炮弹,第 \(i\) 个敌人有血量上限 \(a_i\) 和威力阈值 \(b_i\) ,第 \(i\) 个炮弹价值为 \(c_i\) 、威力为 \(d_i\) 。
有两种攻击方式:
- 花费 \(1\) 的代价对一个敌人进行一次普通攻击。
- 使用一个炮弹对所有敌人造成一次攻击,每颗炮弹只能使用一次。
对于第 \(i\) 个敌人,若其被普通攻击 \(a_i\) 次,或受到了 \(k\) 次威力 \(\geq b_i\) 的炮弹攻击(\(k\) 是所有人一致的给定常数),则会死亡。
\(q\) 次修改,每次修改一个人或一个炮弹的两个权值,每次修改后求使所有敌人死亡的最小代价。
\(n, m, q \leq 2.5 \times 10^5\) ,\(k \leq 10^4\) ,\(qk \leq 5 \times 10^5\)
特判没有使用炮弹的情况后,显然只会发射 \(k\) 发炮弹,炸掉 \(b_i \leq \min d\) 的敌人,然后对剩下的敌人使用普通攻击。
枚举 \(x = \min d\) ,答案即为 \(d_i \geq x\) 的所有 \(i\) 中最小的 \(k\) 个 \(c_i\) 的和加上 \(\sum_{b_i > x} a_i\) 。
先将所有 \(b, d\) 离散化,相等时钦定炮弹的值更大。用线段树维护 \(mn_{x, i}\) 表示区间 \(c\) 的第 \(i\) 小值,\(f_{x, i}\) 表示区间内用 \(i\) 发炮弹的最小额外代价(炮弹的代价减去攻击到的 \(\sum a_i\) ),答案即为 \(f_{1, k} + \sum a_i\) 。
转移首先有 \(f_{rc, i} - suma_{lc} \to f_{x, i}\) ,然后还有一个 \((\min, +)\) 卷积 \(f_{lc, i} + \sum_{k = 0}^j mn_{rc, k} \to f_{x, i + j}\) 。注意后者 \(\sum_{k = 0}^j mn_{rc, k}\) 是凸的,而前者不一定凸(因为第一部分的朴素转移后面的转移不到),故 \(f_{lc, i}\) 的决策点具有单调性,因此可以用整体二分优化做到 \(O(\min(len_x, k) \log \min(len_x, k))\) 。
时间复杂度 \(O(n \log n \log k + qk \log \frac{n}{k} \log k)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2.5e5 + 7;
struct Node {
int op, id, a, b;
inline bool operator < (const Node &rhs) const {
return b == rhs.b ? op < rhs.op : b < rhs.b;
}
} nd[N * 3];
int upd[N], p[N * 3], id[N * 2];
bool exist[N * 3];
int n, m, q, k;
namespace SMT {
vector<ll> a[N * 3 << 2];
vector<int> b[N * 3 << 2];
ll suma[N * 3 << 2], g[N], pre[N];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void solve(int l, int r, int L, int R, int lim, vector<ll> &f) {
if (L > R)
return;
int mid = (L + R) >> 1, p = -1;
for (int i = max(l, mid - lim); i <= min(mid, r); ++i)
if (g[i] + pre[mid - i] < f[mid])
f[mid] = g[i] + pre[mid - i], p = i;
solve(l, p, L, mid - 1, lim, f), solve(p, r, mid + 1, R, lim, f);
}
inline void pushup(int x) {
if (a[ls(x)].empty())
a[x] = a[rs(x)];
else {
for (int i = 0; i < a[ls(x)].size(); ++i)
g[i] = a[ls(x)][i];
for (int i = 0; i < b[rs(x)].size(); ++i)
pre[i + 1] = pre[i] + b[rs(x)][i];
a[x] = vector<ll>(min((int)a[ls(x)].size() + (int)b[rs(x)].size(), k + 1), inf);
solve(0, a[ls(x)].size() - 1, 0, a[x].size() - 1, b[rs(x)].size(), a[x]);
for (int i = 0; i < a[rs(x)].size(); ++i)
a[x][i] = min(a[x][i], a[rs(x)][i] - suma[ls(x)]);
}
b[x].resize(b[ls(x)].size() + b[rs(x)].size());
merge(b[ls(x)].begin(), b[ls(x)].end(), b[rs(x)].begin(), b[rs(x)].end(), b[x].begin());
if (b[x].size() > k)
b[x].resize(k);
suma[x] = suma[ls(x)] + suma[rs(x)];
}
void build(int x, int l, int r) {
if (l == r) {
if (exist[l]) {
if (nd[l].op)
a[x] = {}, b[x] = {nd[l].a}, suma[x] = 0;
else
a[x] = {-nd[l].a}, b[x] = {}, suma[x] = nd[l].a;
} else
a[x] = {}, b[x] = {}, suma[x] = 0;
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
pushup(x);
}
void update(int x, int nl, int nr, int p) {
if (nl == nr) {
if (exist[p] ^= 1) {
if (nd[p].op)
a[x] = {}, b[x] = {nd[p].a}, suma[x] = 0;
else
a[x] = {-nd[p].a}, b[x] = {}, suma[x] = nd[p].a;
} else
a[x] = {}, b[x] = {}, suma[x] = 0;
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
update(ls(x), nl, mid, p);
else
update(rs(x), mid + 1, nr, p);
pushup(x);
}
} // namespace SMT
signed main() {
freopen("fire.in", "r", stdin);
freopen("fire.out", "w", stdout);
scanf("%d%d%d%d", &n, &m, &q, &k);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &nd[i].a, &nd[i].b), nd[i].op = 0, nd[i].id = i;
for (int i = 1; i <= m; ++i)
scanf("%d%d", &nd[n + i].a, &nd[n + i].b), nd[n + i].op = 1, nd[n + i].id = n + i;
for (int i = 1; i <= q; ++i) {
int op, x, y, z;
scanf("%d%d%d%d", &op, &x, &y, &z);
if (--op)
x += n;
upd[i] = x, nd[n + m + i] = (Node){op, n + m + i, y, z};
}
int len = n + m + q;
sort(nd + 1, nd + len + 1);
for (int i = 1; i <= len; ++i)
p[nd[i].id] = i, exist[i] = (nd[i].id <= n + m);
SMT::build(1, 1, len), iota(id + 1, id + n + m + 1, 1);
for (int i = 1; i <= q; ++i) {
SMT::update(1, 1, len, p[id[upd[i]]]), SMT::update(1, 1, len, p[id[upd[i]] = n + m + i]);
printf("%lld\n", SMT::suma[1] + min(SMT::a[1].size() > k ? SMT::a[1][k] : inf, 0ll));
}
return 0;
}
*异或症测试 3
source:QOJ 6344
给定一个大小为 \(n\) 的集合 \(B\) 和整数 \(X\) ,其中 \(B\) 中的元素和 \(X\) 的范围均为 \([0, 2^m - 1]\) ,求有多少非空集合 \(S \in \{ 1, 2, \cdots, X \}\) 使得 \(B\) 是 \(S\) 的线性基,答案对 \(998244353\) 取模。
\(n, m \leq 2000\)
*树上邻邻域数点
这是一道交互题。
给定一棵树,保证不存在一个点的度数为 \(n - 1\) 。每个点有未知整数点权 \(a_i\) ,其中 \(a_i \in [0, 31]\) 。
每次可以给出 \(x, d, v\) ,询问树上距离 \(x\) 为 \(d\) 的点中点权 \(\leq v\) 的点的数量,需要保证 \(d \geq L\) ,其中 \(L\) 是给定的常数。
求每个点的点权。
\(n = 50000\) ,\(L \in \{ 0, 1, 2 \}\) ,询问次数上界为 \(2.5 \times 10^5\)
Day 4 - 范斯喆
树
source:CF1958I
有两棵 \(n\) 个点的树,均以 \(1\) 为根。
每次可以同时从两棵树中删去点 \(x\) ,并将其所有儿子连到其父亲下,其中 \(x \neq 1\) 。
最小化使得两棵树完全相同的删点数目(一次操作会删去两个点),其中两棵树完全相同当且仅当点集、边集均相同。
\(n \leq 40\)
考虑将问题转化为最多能保留的点的数目。首先可以 \(O(n)\) 判断两个点能否同时存在,只要判断只保留这两个点时二者父亲是否相等即可。由于点对之间相互独立(即不会出现 \((x, y), (y, z)\) 能同时出现而 \((x, y, z)\) 不能同时出现的情况),因此只要求无向图的最大独立集即可。
考虑折半搜索,记录前一半点集的所有可能选取情况,再枚举后一半点集,求出不能同时选的前一半点集,将其取反后取子集内集合大小最大的点集更新答案,时间复杂度 \(O(n^3 + 2^{\frac{n}{2}})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 41;
struct Graph {
vector<int> e[N];
int fa[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
void dfs(int u, int f, int r, ll s) {
fa[u] = r;
for (int v : e[u])
if (v != f)
dfs(v, u, s >> u & 1 ? u : r, s);
}
} T1, T2;
ll e[N], g[1 << (N / 2)];
int f[1 << (N / 2)];
int n;
inline bool check(ll s) {
T1.dfs(0, -1, -1, s), T2.dfs(0, -1, -1, s);
for (int i = 1; i < n; ++i)
if ((s >> i & 1) && T1.fa[i] != T2.fa[i])
return false;
return true;
}
signed main() {
freopen("tree.in", "r", stdin);
freopen("tree.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
T1.insert(u, v), T1.insert(v, u);
}
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
T2.insert(u, v), T2.insert(v, u);
}
if (n == 1)
return puts("0"), 0;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (!check((1ll << i) | (1ll << j)))
e[i] |= 1ll << j, e[j] |= 1ll << i;
for (int s = 1; s < (1 << (n / 2)); ++s) {
int u = __builtin_ctz(s);
f[s] = max(f[s ^ (1 << u)], f[(s ^ (1 << u)) & ~e[u]] + 1);
}
int ans = (n - f[(1 << (n / 2)) - 1]) * 2;
for (int s = 1; s < (1 << (n - n / 2)); ++s) {
int u = __builtin_ctz(s);
g[s] = g[s ^ (1 << u)] | e[u + n / 2];
if (!((g[s] >> n / 2) & s))
ans = min(ans, (n - __builtin_popcount(s) - f[~g[s] & ((1 << (n / 2)) - 1)]) * 2);
}
printf("%d", ans);
return 0;
}
序列
source:ARC066D
给定序列 \(a_{1 \sim n}\) 和常数 \(c\) ,求:
\[\max_{S \in \{ 1, 2, \cdots, n \}} \left( \left( \sum_{1 \leq l \leq r \leq n} \prod_{i = l}^r [i \in S] \right) \times c - \sum_{i \in S} a_i \right) \]\(m\) 次单点修改操作,每次修改后求答案,修改不保留。
\(n, m \leq 3 \times 10^5\)
设 \(f_i\) 表示 \(\max S = i\) 时的答案,\(s_i\) 表示 \(a\) 的前缀和,枚举 \(i\) 的连续段可以得到:
不难斜率优化做到单次 \(O(n)\) 查询答案。
考虑修改 \(a_x \gets k\) ,只需考虑选 \(x\) 和不选 \(x\) 两种情况的答案即可。
先考虑不选 \(x\) 的答案,一开始正反各 DP 一次,求出每个前后缀的答案 \(pre_i, suf_i\) ,则不选 \(x\) 的答案即为 \(pre_{x - 1} + suf_{x + 1}\) 。
再考虑选 \(x\) 的答案,一开始预处理 \(ans_x\) 表示强制选 \(x\) 的答案,则选 \(x\) 的答案即为 \(ans_x + a_x - k\) 。
求 \(ans\) 考虑分治,设当前分治区间为 \([l, r]\) ,只考虑跨过 \(mid\) 的连续段,更新 \(ans\) 。枚举左半边的连续段开头点,右半边的最优化可以用斜率优化处理,然后就可以对该点到 \(mid\) 这一段区间取 \(\max\) ,右半边同理。
由于左半边都是后缀取 \(\max\) ,右半边都是前缀取 \(\max\) ,因此无需线段树,直接做一遍前/后缀 \(\max\) 即可。
时间复杂的 \(O(n \log n + m)\) 。
#include <bits/stdc++.h>
typedef long double ld;
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 3e5 + 7;
ll s[N], f[N], mx[N], x[N], y[N], pre[N], suf[N], ans[N];
int a[N], sta[N];
int n, m, c;
inline ld slope(int i, int j) {
return (ld)(y[i] - y[j]) / (x[i] - x[j]);
}
inline ll solve1() {
int top = 0;
sta[++top] = 0;
for (int i = 1; i <= n; ++i) {
s[i] = s[i - 1] + a[i];
while (top > 1 && slope(sta[top - 1], sta[top]) <= 1ll * (i * 2 + 1) * c)
--top;
f[i] = y[sta[top]] - 1ll * sta[top] * (i * 2 + 1) * c + 1ll * i * (i + 1) * c - s[i];
mx[i] = max(mx[i - 1], f[i]), x[i] = i, y[i] = mx[i] + 1ll * i * i * c + s[i];
while (top > 1 && slope(sta[top - 1], sta[top]) <= slope(sta[top], i))
--top;
sta[++top] = i;
}
return mx[n] / 2;
}
void solve2(int l, int r) {
if (l == r) {
ans[l] = max(ans[l], (l > 1 ? pre[l - 2] : 0) + (r < n ? suf[r + 2] : 0) +
1ll * (r - l + 1) * (r - l + 2) * c - (s[r] - s[l - 1]));
return;
}
int mid = (l + r) >> 1;
solve2(l, mid), solve2(mid + 1, r);
memset(mx + l, -0x3f, sizeof(ll) * (r - l + 1));
for (int i = l; i <= mid; ++i)
x[i] = i * 2 - 3, y[i] = (i > 1 ? pre[i - 2] : 0) + 1ll * (i - 1) * (i - 2) * c + s[i - 1];
for (int i = mid + 1; i <= r; ++i)
x[i] = i, y[i] = (i < n ? suf[i + 2] : 0) + 1ll * i * i * c - s[i];
int top = 0;
for (int i = l; i <= mid; ++i) {
while (top > 1 && slope(sta[top - 1], sta[top]) <= slope(sta[top], i))
--top;
sta[++top] = i;
}
for (int i = mid + 1; i <= r; ++i) {
while (top > 1 && slope(sta[top - 1], sta[top]) <= 1ll * i * c)
--top;
mx[i] = y[i] + y[sta[top]] - 1ll * (sta[top] * 2 - 3) * i * c;
}
top = 0;
for (int i = mid + 1; i <= r; ++i) {
while (top > 1 && slope(sta[top - 1], sta[top]) <= slope(sta[top], i))
--top;
sta[++top] = i;
}
for (int i = l; i <= mid; ++i) {
while (top > 1 && slope(sta[top - 1], sta[top]) <= 1ll * (i * 2 - 3) * c)
--top;
mx[i] = y[i] + y[sta[top]] - 1ll * (i * 2 - 3) * sta[top] * c;
}
for (int i = l; i <= mid; ++i)
ans[i] = max(ans[i], mx[i] = max(mx[i], i == l ? -inf : mx[i - 1]));
for (int i = r; i >= mid; --i)
ans[i] = max(ans[i], mx[i] = max(mx[i], i == r ? -inf : mx[i + 1]));
}
signed main() {
freopen("sequence.in", "r", stdin);
freopen("sequence.out", "w", stdout);
scanf("%d%d%d", &n, &m, &c);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), a[i] *= 2;
solve1();
for (int i = 1; i <= n; ++i)
pre[i] = mx[i];
reverse(a + 1, a + n + 1), solve1(), reverse(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i)
suf[n - i + 1] = mx[i], s[i] = s[i - 1] + a[i];
memset(ans, -0x3f, sizeof(ans)), solve2(1, n);
printf("%lld\n", pre[n] / 2);
while (m--) {
int x, k;
scanf("%d%d", &x, &k);
printf("%lld\n", max(pre[x - 1] + suf[x + 1], ans[x] + a[x] - k * 2) / 2);
}
return 0;
}
栈
source:CF737F
有三个栈 \(S_1, S_2, S_3\) ,初始 \(S_1\) 有 \(n\) 个数,自顶向下构成长度为 \(n\) 的排列 \(p_{1 \sim n}\) ,\(S_2, S_3\) 初始均为空。
给定限制 \(A, B\) ,可以进行两种操作:
- 选择一个整数 \(x \leq A\) ,将 \(S_1\) 栈顶的 \(x\) 个数作为一个整体弹出,并按原来的顺序放进 \(S_2\) 。
- 选择一个整数 \(x \leq B\) ,将 \(S_2\) 栈顶的 \(x\) 个数作为一个整体弹出,并按原来的顺序放进 \(S_3\) 。
目标是让 \(S_3\) 自顶向下构成长度为 \(n\) 的升序排列,构造一组方案或告知无解。
\(\sum n \leq 10^6\)
首先可以发现,当 \(S_2\) 中出现可以移动到 \(S_3\) 的一段前缀,则一定会贪心将其移动过去。
注意到若 \(S_2\) 中出现相邻两个数 \(x, y\) 满足 \(x\) 在上面且 \(x + 1 < y\) ,则一定无解,称这是一个坏点。事实上,若不存在这种情况,则一定有解,可以构造方案证明。
考虑找到 \(p\) 中第一个 \(p_i + 1 < p_{i + 1}\) 的位置,显然 \(i\) 和 \(i + 1\) 一定不能通过一次操作挪过去。那么可以只考虑 \(p_{1 \sim i}\) ,剩下的递归到子问题处理。
若 \(p_{1 \sim i}\) 中存在可以直接放入 \(S_3\) 的数,即存在当前剩余部分的最大值,那么这个最大值前面肯定是次大值(或者最大值在顶部),此时先把 \(p_1\) 到最大值的那一段一个一个扔进 \(S_2\) ,接着把最大值扔进 \(S_3\) ,这样一定是最优的。
否则 \(p_{1 \sim i}\) 必须全部扔到 \(S_2\) 中,且这个过程中不会有 \(S_2 \to S_3\) 的操作。
观察到 \(S_{1 \sim i}\) 的形态为若干个公差为 \(1\) 的等差数列,且这些等差数列的值域范围是递减的。
注意到如果值域范围不连续,就只能一次性把 \(p_{1 \sim i}\) 移动到 \(S_2\) 中,否则必然出现矛盾。而对于值域连续的情况,若 \(A, B\) 足够大,则可以将每个等差数列分别放进 \(S_2\) ,这样放完后最小值在上、最大值在下,可以尽可能减少和其他段之间的坏点。
考虑 \(A, B\) 不一定足够大的情况,具体有两种情况:
- \(A\) 小于其中某个等差数列的长度。
- \(B\) 小于这些等差数列的长度之和。
考虑对等差数列的个数进行分类讨论。对于 \(B\) 不够大的情况:
-
如果只有一个等差数列,既然不能整个放进 \(S_2\) ,那就最优就是一个一个放进 \(S_2\) ,否则可能会出现坏点。
-
如果恰有两个等差数列,设第一个长度为 \(x\) ,第二个长度为 \(y\) 。设第一步移动的前缀长度为 \(k\) :
- 若 \(k \leq x\) ,则剩下的 \(x + y - k\) 个必须作为整体移动过去,否则一定会出现坏点,这要求 \(\max(k, x + y - k) \leq A\) 且 \(\max(x - k, y + k) \leq B\) 。
- 若 \(k > x\) ,则剩下 \(x + y - k\) 个同样必须作为整体移动,这要求 \(\max(k, x + y - k) \leq A\) 且 \(\max(k - x, x + y - (k - x)) \leq B\) 。
此时由于不可能让最小值在顶部、最大值在底部,因此只要 \(k\) 满足条件,就一定是最优的方案。
-
如果有大于两个等差数列,可以发现唯一可能合法的方案就是把所有等差数列作为一个整体移动过去。
否则如果 \(B\) 足够大,但是 \(A\) 不够大:
- 如果只有一个等差数列,同样应该一个一个放进 \(S_2\) 。
- 如果有大于两个等差数列,一定无解。
- 如果恰好有两个等差数列,情况和上面类似。
综上所述,每一步移动都有唯一的最优策略,因此直接模拟即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
vector<pair<int, int> > ans;
vector<int> vec;
int p[N];
bool vis[N];
int n, A, B, m;
bool legal;
inline void insert(int l, int r) {
if (r - l + 1 > A) {
legal = false;
return;
}
ans.emplace_back(1, r - l + 1);
for (int i = r; i >= l; --i)
vis[p[i]] = true, vec.emplace_back(p[i]);
while (vis[m]) {
int p = vec.size() - 1;
for (; ~p; --p) {
if (p + 1 < vec.size() && vec[p] != vec[p + 1] + 1) {
legal = false;
return;
}
if (vec[p] == m)
break;
}
if (vec.size() - p > B) {
legal = false;
return;
}
ans.emplace_back(2, vec.size() - p);
m -= vec.size() - p, vec.resize(p);
}
}
signed main() {
freopen("stack.in", "r", stdin);
freopen("stack.out", "w", stdout);
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &A, &B);
for (int i = 1; i <= n; ++i)
scanf("%d", p + i);
memset(vis + 1, false, sizeof(bool) * n);
ans.clear(), vec.clear(), m = n, legal = true;
for (int i = 1, lst = 1; i <= n && legal; ++i) {
if (p[i] == m) {
for (int j = lst; j <= i; ++j)
insert(j, j);
lst = i + 1;
continue;
}
if (i < n && p[i] + 1 >= p[i + 1])
continue;
int cnt = 1, ed = 0;
for (int j = lst; j < i; ++j)
if (p[j + 1] != p[j] + 1)
++cnt, ed = j;
if (cnt == 1) {
if (i - lst + 1 <= min(A, B))
insert(lst, i);
else {
for (int j = lst; j <= i; ++j)
insert(j, j);
}
lst = i + 1;
continue;
}
if (p[lst] != p[i] + 1) {
if (*max_element(p + lst, p + i + 1) - *min_element(p + lst, p + i + 1) == i - lst &&
i - lst + 1 <= B) {
for (int j = lst; j <= i; ++j)
if (j == i || p[j + 1] != p[j] + 1)
insert(lst, j), lst = j + 1;
} else
insert(lst, i), lst = i + 1;
continue;
}
if (i - lst + 1 <= B && max(ed - lst + 1, i - ed) <= A) {
insert(lst, ed), insert(ed + 1, i), lst = i + 1;
continue;
}
int X = ed - lst + 1, Y = i - ed, k = 0;
for (; k <= X; ++k)
if (max(k, X + Y - k) <= A && max(X - k, Y + k) <= B) {
if (k)
insert(lst, lst + k - 1);
insert(lst + k, i), lst = i + 1;
break;
}
if (k <= X)
continue;
for (; k <= X + Y; ++k)
if (max(k, X + Y - k) <= A && max(k - X, X + Y - (k - X)) <= B) {
insert(lst, lst + k - 1), insert(lst + k, i), lst = i + 1;
break;
}
if (k > X + Y)
legal = false;
}
if (legal) {
printf("%d\n", (int)ans.size());
for (auto it : ans)
printf("%d %d\n", it.first, it.second);
} else
puts("-1");
}
return 0;
}
Day 5 - 张宇卓
图上的游戏
有一个 \(n\) 个点的图,最初没有边,每个点有权值 \(c_i \in \{0, 1, 2 \}\) 。接下来进行 \(m\) 次加边操作,每次随机选取 \(u, v \in [1, n]\) ,在图上连 \(u \to v\) 的有向边,即随机生成一个 \(m\) 条边的有向图(可能有重边、自环)。
A 和 B 轮流操作,A 先手,每次操作可以选择一个 \(c_i = 2\) 的点,将其赋值为 \(0\) 或 \(1\) 。
无法操作时,A 的得分为满足 \([c_u = 0] \and [c_v = 1]\) 的边 \((u, v)\) 的数量,B 的得分为满足 \([c_u = 1] \and [c_v = 0]\) 的边 \((u, v)\) 的数量。
令 \(S\) 为 A 的得分减去 B 的得分,A 需要让 \(S\) 尽量大,B 需要让 \(S\) 尽量小。
对于所有 \(n' \in [1, n], m' \in [1, m]\) ,求只考虑图的前 \(n'\) 个点,加入 \(m'\) 条边后,对于所有 \(n'^{2m'}\) 种图,求 \(S\) 的总和。
\(n, m \leq 50\)
考虑对于固定的图如何算 \(S\) 。考虑将一条边的贡献拆到两个端点上,对于边 \(u \to v\) ,认为其在 \(u\) 处产生 \((-1)^{c_u}\) 的贡献,在 \(v\) 处产生 \(-(-1)^{c_v}\) 的贡献。不难发现只有端点权值不同的边会产生贡献,新的贡献为原来贡献的两倍。
不难发现一个点的贡献只与出入度的差值有关,记 \(d_u = \deg^{out}(u) - \deg^{in}(u)\) ,则 \(u\) 对答案的贡献为 \((-1)^{c_u} \times d_u\) 。因此只要将 \(c = 2\) 的点按 \(|d|\) 降序排序,则两人一定会贪心依次选取,从而不难计算 \(S\) 。
设 \(f_{i, j_1, j_2}\) 表示决策了 \(i\) 个点,分配了 \(j_1, j_2\) 的出入度的答案,具体用一个二元组 \((x, y)\) 存储,其中 \(x\) 表示方案数,\(y\) 表示贡献和。转移时降序枚举 \(x = |d|\) ,枚举 \(t = 0/1\) 表示当前分配的是出度还是入度,设有 \(k\) 个点的 \(|d| = x\) ,其中这 \(k\) 个点的入度和为 \(u\) ,预处理 \(h_{k, u}\) 表示 \(u\) 的入度分到 \(k\) 个点的方案不难转移。
注意统计答案时 \(c_i = 0 / 1\) 的点其实对答案本身没有贡献(因为正反两条边贡献抵消),答案只与 \(c_i = 2\) 的点数有关。
最后还要乘上多重集的排列数,具体的,度分配到同一点时这些度不作区分,\(d\) 相等的点不做区分,\(c_i = 2\) 的点之间不做区分,\(c_i = 0/1\) 的点之间不做区分,因为边随机。
时间复杂度 \(O(n^2 m^4)\) ,加一些剪枝跑得不慢。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e1 + 7;
int c[N], fac[N], inv[N], invfac[N], h[N][N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
struct Node {
int x, y; // x 为方案数,y 为权值和
inline Node(int _x = 0, int _y = 0) : x(_x), y(_y) {}
inline friend Node operator + (const Node a, const Node b) {
return Node(add(a.x, b.x), add(a.y, b.y));
}
inline friend Node operator * (const Node a, const Node b) {
return Node(1ll * a.x * b.x % Mod, add(1ll * a.x * b.y % Mod, 1ll * a.y * b.x % Mod));
}
} f[N][N][N], g[N][N][N];
signed main() {
freopen("game.in", "r", stdin);
freopen("game.out", "w", stdout);
prework();
scanf("%d%d", &n, &m);
f[0][0][0] = Node(1, 0);
for (int d = m; ~d; --d) {
memset(h, 0, sizeof(h)), h[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= m; ++j) {
if (!h[i - 1][j])
continue;
for (int k = 0; j + k <= m; ++k)
h[i][j + k] = add(h[i][j + k], 1ll * h[i - 1][j] * invfac[k] % Mod * invfac[d + k] % Mod);
}
for (int t = 0; t <= 1; ++t) { // 当前分配的是出度还是入度
if (!d && t)
continue; // d = 0 只要算一边就好了
for (int i = 0; i <= n; ++i)
for (int j1 = 0; j1 <= m; ++j1)
for (int j2 = 0; j2 <= m; ++j2) {
if (!f[i][j1][j2].x && !f[i][j1][j2].y)
continue;
for (int k = 0; i + k <= n; ++k) {
int j_1 = j1 + k * d * !t, j_2 = j2 + k * d * t;
Node val = f[i][j1][j2] * Node(1, 1ll * (k & 1) * sgn(i) * d % Mod);
for (int u = 0; max(j_1, j_2) + u <= m; ++u)
g[i + k][j_1 + u][j_2 + u] = g[i + k][j_1 + u][j_2 + u] +
val * h[k][u] * invfac[k];
}
}
for (int i = 0; i <= n; ++i)
for (int j1 = 0; j1 <= m; ++j1)
for (int j2 = 0; j2 <= m; ++j2)
f[i][j1][j2] = g[i][j1][j2], g[i][j1][j2] = Node();
}
}
for (int i = 1, cnt = 0; i <= n; ++i) {
scanf("%d", c + i), cnt += (c[i] == 2);
for (int j = 1; j <= m; ++j) {
int res = 0;
for (int j1 = 0; j1 <= j; ++j1)
for (int j2 = 0; j2 <= j; ++j2)
res = add(res, 1ll * f[i - cnt][j1][j2].x * f[cnt][j - j1][j - j2].y % Mod);
printf("%d ", 1ll * res * fac[cnt] % Mod * fac[i - cnt] % Mod *
fac[j] % Mod * fac[j] % Mod * inv[2] % Mod);
}
puts("");
}
return 0;
}
修理
source:QOJ8602
给定序列 \(a_{1 \sim n}\) ,\(q\) 次询问通过操作使得 \([l, r]\) 变全 \(0\) 的最少操作数。
操作定义为:初始有一个变量 \(x = 0\) ,每次可以选择执行两种操作之一:
- \(x \gets x + 1\) 。
- \(a_i \gets a_i \oplus x\) 。
\(n, q \leq 2 \times 10^5\) ,强制在线
首先可以发现,记区间最大值为 \(mx\) ,则最终 \(x \geq \mathrm{hightbit}(mx)\) ,否则消不掉最高位。
枚举 \(x \geq \mathrm{hightbit}(mx)\) ,则 \(\leq x\) 的只要消一次,\(>x\) 的要消两次,答案即为 \(\min (x + len + \sum [a_i > x])\) 。
由于 \(x \geq \mathrm{hightbit}(mx)\) ,考虑把 \(\mathrm{hightbit}(mx)\) 提取出来,只考虑 \(\geq \mathrm{hightbit}(mx)\) 的 \(a_i\) ,即将 \(a_i, x\) 都减去 \(2^{\mathrm{hightbit}(mx)}\) 。记 \(cnt\) 为这一部分的 \(a_i\) 数量,将答案式化为 \(len + 2^{\mathrm{hightbit}(mx)} + cnt - \max(\sum [a_i \leq x] - x)\) 。
注意到这个形式类似于 Hall 定理,考虑给每个 \(a_i\) 匹配一个 \(j \leq a_i\) ,则 \(cnt - \max(\sum [a_i \leq x] - x)\) 即为匹配点数。
考虑如何求区间的最大匹配,由于点权均为 \(1\) ,贪心的给每个点匹配到下标尽可能大的位置即可。
考虑提前做扫描线,维护 \([1, r]\) 的匹配点集,强制在线查询只要用主席树维护每个前缀中每个点是否匹配即可。插入 \(a_r\) 后一定会强制匹配 \(a_r\) (优先让后面的数匹配),若此时匹配不合法只要贪心删去最小的 \(l\) 即可。
维护匹配合法性同样考虑 Hall 定理,维护 \(w_i = i - \sum_{j \in S} [a_j \leq i]\) ,则匹配合法当且仅当所有 \(w\) 非负。若插入 \(a_r\) 后使得某个 \(w_i < 0\) ,则找到 \(a_x \in [1, i]\) 的最小的匹配点 \(x\) ,将其删去即可。
开一棵线段树维护 \(w\) ,只维护 \(i\) 值存在的 \(w_i\) ,不存在的 \(w_i\) 设为 \(n + 1\) ,那么修改一对匹配在线段树上就是单点修改和区间加,同时可以用这棵线段树维护每个点的匹配点。
时间复杂度 \(O((n + q) \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 7, LOGN = 19, B = 61;
vector<ll> vec[B];
ll a[N];
int b[N], id[N], rk[N], lpos[N], rpos[N];
int m[B], L[B], R[B];
int n, q, tp;
namespace ST {
int f[LOGN][N];
inline void prework() {
memcpy(f[0] + 1, b + 1, sizeof(int) * n);
for (int j = 1; j <= __lg(n); ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
}
inline ll query(int l, int r) {
int k = __lg(r - l + 1);
return max(f[k][l], f[k][r - (1 << k) + 1]);
}
} // namespace ST
namespace SGT {
const int S = 3e7 + 7;
pair<ll, int> mn[N << 2];
int match[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x) {
mn[x] = min(mn[ls(x)], mn[rs(x)]);
match[x] = min(match[ls(x)], match[rs(x)]);
}
inline void spread(int x, int k) {
mn[x].first += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}
void build(int x, int l, int r) {
mn[x] = make_pair(a[id[l]] - (1ll << b[id[l]]), l), match[x] = n + 1;
if (l == r)
return;
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
}
void modify(int x, int nl, int nr, int p, int k) {
if (nl == nr) {
match[x] = k;
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (p <= mid)
modify(ls(x), nl, mid, p, k);
else
modify(rs(x), mid + 1, nr, p, k);
pushup(x);
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
pushup(x);
}
pair<ll, int> query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return mn[x];
pushdown(x);
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return min(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
int search(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return match[x];
pushdown(x);
int mid = (nl + nr) >> 1;
if (r <= mid)
return search(ls(x), nl, mid, l, r);
else if (l > mid)
return search(rs(x), mid + 1, nr, l, r);
else
return min(search(ls(x), nl, mid, l, r), search(rs(x), mid + 1, nr, l, r));
}
} // namespace SGT
namespace SMT {
const int S = 3e7 + 7;
int rt[N][B], lc[S], rc[S], cnt[S];
int tot;
int update(int x, int nl, int nr, int p, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x], cnt[y] = cnt[x];
if (nl == nr)
return cnt[y] += k, y;
int mid = (nl + nr) >> 1;
if (p <= mid)
lc[y] = update(lc[x], nl, mid, p, k);
else
rc[y] = update(rc[x], mid + 1, nr, p, k);
return cnt[y] = cnt[lc[y]] + cnt[rc[y]], y;
}
int query(int x, int nl, int nr, int l, int r) {
if (!x)
return 0;
if (l <= nl && nr <= r)
return cnt[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(lc[x], nl, mid, l, r);
else if (l > mid)
return query(rc[x], mid + 1, nr, l, r);
else
return query(lc[x], nl, mid, l, r) + query(rc[x], mid + 1, nr, l, r);
}
} // namespace SMT
signed main() {
freopen("repair.in", "r", stdin);
freopen("repair.out", "w", stdout);
scanf("%d%d%d", &n, &q, &tp);
for (int i = 1; i <= n; ++i)
scanf("%lld", a + i), b[i] = __lg(a[i]);
ST::prework();
iota(id + 1, id + n + 1, 1);
sort(id + 1, id + n + 1, [](const int &x, const int &y) {
return a[x] == a[y] ? x < y : a[x] < a[y];
});
for (int i = 1; i <= n; ++i)
lpos[i] = (i > 1 && a[id[i - 1]] == a[id[i]] ? lpos[i - 1] : i);
for (int i = n; i; --i)
rpos[i] = (i < n && a[id[i + 1]] == a[id[i]] ? rpos[i + 1] : i);
for (int i = 1; i <= n; ++i)
rk[id[i]] = i;
fill(L, L + B, n + 1), fill(R, R + B, 0);
for (int i = 1; i <= n; ++i)
L[b[i]] = min(L[b[i]], rk[i]), R[b[i]] = max(R[b[i]], rk[i]);
SGT::build(1, 1, n);
for (int i = 1; i <= n; ++i) {
memcpy(SMT::rt[i], SMT::rt[i - 1], sizeof(int) * B);
auto update = [&](int x, int k) {
SGT::modify(1, 1, n, rk[x], k == 1 ? x : n + 1);
SGT::update(1, 1, n, lpos[rk[x]], R[b[x]], -k);
SMT::rt[i][b[x]] = SMT::update(SMT::rt[i][b[x]], 1, n, x, k);
};
update(i, 1);
auto res = SGT::query(1, 1, n, L[b[i]], R[b[i]]);
if (res.first < 0)
update(SGT::search(1, 1, n, L[b[i]], rpos[res.second]), -1);
}
ll lstans = 0;
while (q--) {
ll l, r;
scanf("%lld%lld", &l, &r);
if (tp == 2) {
l ^= lstans, r ^= lstans;
if (l > r)
swap(l, r);
}
int highbit = ST::query(l, r);
printf("%lld\n", lstans = (r - l + 1) + (1ll << highbit) + SMT::query(SMT::rt[r][highbit], 1, n, l, r));
}
return 0;
}
*人员调度 2
左右部各有 \(n\) 个点,分别有权值 \(a_{1 \sim n}, b_{1 \sim n}\) ,匹配左部点 \(i\) 与右部点 \(j\) 的权值为 \(a_i + b_j + (a_i \oplus b_j)\) 。
给出 \(m\) 组附加权值,其中第 \(i\) 组表示匹配左部点 \(x_i\) 与右部点 \(y_i\) 的权值额外加上 \(w_i\) 。
对于 \(i = 1, 2, \cdots, k\) ,求匹配数为 \(i\) 的最大权匹配。
\(n \leq 10^5\) ,\(m \leq 5 \times 10^5\) ,\(k \leq \min(n, 300)\) ,\(a_i, b_i \leq 2^{12}\) ,\(w_i \le 10^5\)
Day 7 - 刘海峰
序列变换
给定 \(a_{1 \sim n}\) ,你可以进行若干次操作:选择区间 \([l, r]\) ,对于所有 \(i \in \mathbb{N}, x \in \{ 0, 1 \}, l + 3i + x \leq r\) ,令 \(a_{l + 3i + x}\) 加上 \((-1)^x\) 。
求使得 \(a\) 全 \(0\) 的最少操作次数,或报告无解。
\(n \leq 3 \times 10^4\) ,\(|a_i| \leq 10^{10}\)
考虑构造长度为 \(n + 2\) 的新序列 \(b_i = a_{i - 2} + a_{i - 1} + a_i\) ,这里不妨假设 \(i < 1\) 和 \(i > n\) 时 \(a_i = 0\) ,则一次操作相当于:
- \(l \equiv r - 1 \pmod{3}\) :此时原序列中 \(+1, -1\) 数量相等,有 \(b_l \to b_l + 1, b_{r + 2} \to b_{r + 2} - 1\) ,不难发现 \(l, r + 2\) 在 \(\bmod 3\) 意义下相等。
- \(l \equiv r \pmod{3}\) :此时最后多了一个 \(+1\) ,有 \(b_l \to b_l + 1, b_{r + 1} \to b_{r + 1} + 1, b_{r + 2} \to b_{r + 2} + 1\) ,不难发现 \(l, r + 1, r + 2\) 在 \(\bmod 3\) 意义下两两不同。
- \(l \equiv r - 2 \pmod{3}\) :可以规约到第一种情况进行等价操作,因此无需考虑。
不难发现操作二会改变 \(b\) 的总和,因此操作二的数量是固定的。
考虑从后往前贪心,枚举 \(i = n + 2 \to 1\) ,其中扫到 \(i\) 时已经将 \(b_j (j > i)\) 均消成 \(0\) ,操作一个位置是记录一下前面各个 \(\bmod 3\) 位置需要操作的情况:
- \(b_i = 0\) :无需处理。
- \(b_i > 0\) :不断进行操作一将 \(b_i\) 消成 \(0\) 。
- \(b_i < 0\) :
- 若 \(b_{i - 1} < 0\) ,则不断用操作二(因为一次可以同时增加两个位置,显然优于操作一),直到 \(\max(b_i, b_{i - 1}) = 0\) 或操作二次数用完为止。
- 若仍然有 \(b_i < 0\) ,则用前面操作留下的左端点增加该位置,直至 \(b_i = 0\) 或残留的操作用完为止。
- 若仍然有 \(b_i < 0\) ,考虑将 \(b_{i - 1}\) 用操作一减到 \(b_i\) ,然后一起用操作二消成 \(0\) ,若操作二次数不够则无解。
时间复杂度线性。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;
ll a[N];
int T, n;
signed main() {
freopen("trans.in", "r", stdin);
freopen("trans.out", "w", stdout);
scanf("%d%d", &T, &n);
while (T--) {
ll sum = 0;
for (int i = 1; i <= n; ++i)
scanf("%lld", a + i), sum += a[i];
if (sum > 0) {
puts("-1");
continue;
}
ll ans = sum = -sum;
a[n + 2] = a[n + 1] = 0;
for (int i = n + 2; i; --i)
for (int j = 1; j < min(3, i); ++j)
a[i] += a[i - j];
ll cnt[3] = {0, 0, 0};
for (int i = n + 2; i; --i) {
if (a[i] > 0)
cnt[i % 3] += a[i], ans += a[i];
else if (a[i] < 0) {
if (i > 1 && a[i - 1] < 0) {
ll k = min(sum, min(-a[i - 1], -a[i]));
a[i] += k, a[i - 1] += k, cnt[(i + 1) % 3] += k, sum -= k;
}
if (a[i] < 0) {
ll k = min(-a[i], cnt[i % 3]);
cnt[i % 3] -= k, a[i] += k;
if (a[i] < 0) {
if (-a[i] > sum) {
ans = -1;
break;
}
ll k = -a[i];
cnt[(i + 1) % 3] += k, cnt[(i - 1) % 3] += k, ans += k, sum -= k;
}
}
}
}
printf("%lld\n", ans);
}
return 0;
}
追忆
有 \(n\) 个点,第 \(i(i \geq 2)\) 个点向 \(a_i, b_i\) 各连一条有向边,其中 \(a_i, b_i\) 在 \(1 \sim i - 1\) 中等概率随机选取。
有 \(n - 1\) 次询问,第 \(i\) 次询问 \(i + 1\) 到 \(x_i\) 的最短路。
\(n \leq 2 \times 10^6\)
首先有结论:\(x\) 能到达的点数级别为 \(O(\sqrt{x})\) 。
证明:设 \(f(x, y)\) 表示 \(x\) 只经过 \(\geq y\) 的点能到达的点数,则 \(f(x, y)\) 为 \(O(\frac{x}{y})\) 级别。这是因为每走一步点的编号期望折半,因此这些点形成高度为 \(O(\log \frac{x}{y})\) 的二叉树,因此得证。而 \(x\) 能到达的点数级别为 \(\sqrt{x} + f(x, \sqrt{x}) = O(\sqrt{x})\) ,具体的就是钦定 \(\leq \sqrt{x}\) 的点都能到达,统计 \((\sqrt{x}, x]\) 的期望点数。
因此暴力 bfs 的复杂度是 \(O(n \sqrt{n})\) 。
考虑只保留前 \(B\) 个点,并对于每个终点预处理所有点到它的距离。询问的时候只对编号 \(> B\) 的点 bfs,复杂度就是 \(O(\sum_{i = B + 1}^n f(i, B))\) ,近似为 \(O(\frac{n^2}{B})\) 。
总时间复杂度 \(O(\frac{n^2}{B} + B^{1.5})\) ,取 \(B = n^{0.8}\) 即可做到 \(O(n^{1.2})\) ,由于空间问题 \(B\) 取 \(2 \times 10^4\) ,然后开 \(O(B^2)\) 的 short
数组存 \(B^{1.5}\) 对最短路即可,需要一些卡常。
#include <bits/stdc++.h>
typedef unsigned int uint;
typedef long long ll;
using namespace std;
const int N = 2e6 + 7, B = 2.2e4 + 7;
pair<int, int> q[N];
short dis[B][B];
int a[N], b[N];
uint seed;
int n, block;
inline uint shift() {
seed ^= seed << 13, seed ^= seed >> 7, seed ^= seed << 17;
return seed;
}
inline int query(int S, int T) {
if (S == T)
return 0;
else if (T > S)
return -1;
else if (S <= block)
return dis[S][T] - 1;
int head = 1, tail = 0, ans = n + 1;
q[++tail] = {S, 0};
while (head <= tail) {
int u = q[head].first, w = q[head++].second;
if (w >= ans)
break;
else if (u == T)
return w;
else if (u > block) {
if (a[u] >= T)
q[++tail] = {a[u], w + 1};
if (b[u] >= T)
q[++tail] = {b[u], w + 1};
} else if (dis[u][T])
ans = min(ans, w + dis[u][T] - 1);
}
return ans == n + 1 ? -1 : ans;
}
signed main() {
freopen("recall.in", "r", stdin);
freopen("recall.out", "w", stdout);
scanf("%d%u", &n, &seed);
for (int i = 2; i <= n; ++i)
a[i] = shift() % (i - 1) + 1, b[i] = shift() % (i - 1) + 1;
block = min(n, B - 1);
for (int i = 1; i <= block; ++i) {
queue<int> q;
dis[i][i] = 1, q.emplace(i);
while (!q.empty()) {
int u = q.front();
q.pop();
if (!dis[i][a[u]])
dis[i][a[u]] = dis[i][u] + 1, q.emplace(a[u]);
if (!dis[i][b[u]])
dis[i][b[u]] = dis[i][u] + 1, q.emplace(b[u]);
}
}
ll ans = 0;
for (int i = 1, x; i < n; ++i) {
scanf("%d", &x);
ans ^= 1ll * (query(i + 1, x) + 2) * i;
}
printf("%lld", ans);
return 0;
}
*小方的疑惑
给定 \(a_{1 \sim n}\) 和 \(b_{1 \sim n - 1}\) ,你可以进行若干次操作:选择 \(1 \leq i < n\) 满足 \(a_{b_i} > 0\) ,将 \(a_{b_i}\) 减去 \(1\) ,并将 \(a_{i + 1}\) 加上 \(1\) 。
求能生成出的本质不同的 \(a\) 序列数量 \(\bmod (10^9 + 7)\) 。
\(n \leq 8 \times 10^3\)
Day 8 - 拼好赛(李可、李可、蒋凌宇)
平衡树
给定一棵树,点带点权。\(q\) 次询问,每次给定 \(k\) 条关键边和区间 \([l, r]\) 。定义:
- 一次操作:将一个点的点权增加 \(1\) 。
- 边的不平衡度:删去该边后两棵子树的点权和之差的绝对值。
- 树的不平衡度:所有关键边的不平衡度的最大值。
求至少 \(l\) 次、至多 \(r\) 次操作后树的不平衡度的最小值。
\(\sum k \leq n \leq 3 \times 10^5\)
首先注意到,若三条边在同一条链上,则中间那条边没有用(因为其不平衡度不可能大于左右两条边),因此这类情况中间的那条边可以删去。
对于保留的关键边,非关键边连成的连通块的点是等价的,可以将它们缩成一个点,这样构成了一个菊花图。
考虑一张菊花图,记 \(S\) 为点权和,叶子的权值为 \(s_i\) ,则边的不平衡度即为 \(|S - 2s_i|\) 。又由于 \(2 s_i > S\) 的点至多只有一个,而其不可能成为不平衡度最大的边,因此可以将绝对值去掉,将边的不平衡度视为 \(a_i = S - 2s_i\) 。
考虑一次操作的影响,显然不会操作菊花中心(这样会使得所有 \(a\) 增加 \(1\) )。加在叶子上会使得该边的 \(a_i\) 减去 \(1\) ,而其余 \(a_i\) 加上 \(1\) 。因此最优策略即为每次操作最小的 \(s_i\) 。
具体地,首先进行 \(l\) 次操作,每次操作最小的 \(s_i\) ,容易 \(O(k)\) 模拟这个过程,有一些细节要讨论。最后如果发现当前最小的 \(s_i\) 是唯一的则还可以利用剩下的 \(r - l\) 次操作增大它,因为如果不唯一仍操作会使得其他的不平衡度增加,不如不操作。
时间复杂度 \(O(\sum k \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Edge {
int u, v;
} e[N];
ll s[N];
int a[N], fa[N], in[N], out[N], id[N];
int n, q, dfstime;
void dfs(int u, int f) {
fa[u] = f, id[in[u] = ++dfstime] = u;
for (int v : G.e[u])
if (v != f)
dfs(v, u);
out[u] = dfstime;
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
freopen("balance.in", "r", stdin);
freopen("balance.out", "w", stdout);
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
scanf("%d%d", &e[i].u, &e[i].v);
G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u);
}
dfs(1, 0);
for (int i = 1; i <= n; ++i)
s[i] = s[i - 1] + a[id[i]];
for (int i = 1; i < n; ++i)
if (fa[e[i].v] == e[i].u)
swap(e[i].u, e[i].v);
while (q--) {
int k, l, r;
scanf("%d%d%d", &k, &l, &r);
vector<int> key(k);
for (int &it : key)
scanf("%d", &it), BIT::update(in[e[it].u], 1);
vector<ll> val;
for (int it : key) {
int res = BIT::query(out[e[it].u]) - BIT::query(in[e[it].u] - 1);
if (res == 1)
val.emplace_back(s[out[e[it].u]] - s[in[e[it].u] - 1]);
else if (res == k)
val.emplace_back(s[n] - (s[out[e[it].u]] - s[in[e[it].u] - 1]));
}
if (val.size() == 1)
val.emplace_back(s[n] - val[0]);
for (int it : key)
BIT::update(in[e[it].u], -1);
sort(val.begin(), val.end());
ll S = s[n] + l, mn = val[0], mn2 = 0;
int mncnt = 0;
for (int i = 0; i < val.size(); ++i) {
if (i == val.size() - 1 || l < (val[i + 1] - mn) * (i + 1)) {
mn += l / (i + 1), mncnt = i + 1 - l % (i + 1);
if (mncnt == 1)
mn2 = (i ? mn + 1 : val[1]);
break;
} else
l -= (val[i + 1] - mn) * (i + 1), mn = val[i + 1];
}
if (mncnt == 1) {
ll k = min(mn2 - mn, (ll)(r - l));
S += k, mn += k;
}
printf("%lld\n", S - mn * 2);
}
return 0;
}
上升树
给定一棵树,点带点权。需要删去一个点,最小化各个连通块内路径 LIS 的最大值。
\(n \leq 10^5\)
如果能求出每一个子树内和子树外的最长 LIS 长度,就能知道删掉某一个点的答案。
考虑如何求子树内的 LIS,只要考虑经过 \(u\) 的 LIS 即可,其他的可以直接合并取 \(\max\) 。
考虑 LIS 的二分求法:维护上升序列 \(a_{1 \sim m}\) ,每次遍历到数 \(k\) 时尝试加入,若 \(k > a_m\) 则直接插入末尾,否则找到第一个 \(\geq k\) 的位置,将其赋值为 \(k\) 。
这样就可以比较方便地合并子树信息,维护自上而下的 LIS 和自下而上的 LDS,接下来考虑三个操作:
- 合并两个上升序列:直接将对应位置取 \(\min\) 即可。
- 更新信息:枚举一个序列,二分找到另一个序列中的最优位置。注意需要枚举短序列的元素,然后在长序列上二分,这样才能保证复杂度,原理类似长链剖分。
- 单点插入:直接二分找到修改位置即可。
为了方便可以将自下而上的 LDS 转化为负数的自上而下的 LIS,这样更新信息时的 LIS 就是两个序列各自的前缀,维护首末位置只要维护序列开头的位置即可。
子树外的 LIS 并不好求,注意到删的点一定是全局最长 LIS 所在链上的一个点。于是只要用分别以这条链两个端点作为根,跑两遍 dfs 即可,这样就只需要求子树内的了。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
tuple<int, int, int> mx[N];
vector<pair<int, int> > up[N], down[N];
vector<int> path;
int a[N], lis[N];
int n;
inline void insert(vector<pair<int, int> > &vec, int k) {
auto it = lower_bound(vec.begin(), vec.end(), make_pair(k, 0));
if (it == vec.end())
vec.emplace_back(k, vec.back().second);
else {
it->first = k;
if (it != vec.begin())
it->second = prev(it)->second;
}
}
inline void merge(vector<pair<int, int> > &a, vector<pair<int, int> > &b) {
if (a.size() < b.size())
swap(a, b);
for (int i = 0; i < b.size(); ++i)
a[i] = min(a[i], b[i]);
}
inline tuple<int, int, int> query(vector<pair<int, int> > &a, vector<pair<int, int> > &b) {
bool flag = (a.size() < b.size());
if (flag)
swap(a, b);
tuple<int, int, int> res = make_tuple(-1, 0, 0);
for (int i = 0; i < b.size(); ++i) {
auto it = lower_bound(a.begin(), a.end(), make_pair(-b[i].first, 0));
if (it != a.begin())
res = max(res, make_tuple((int)(it - a.begin() + i + 1), prev(it)->second, b[i].second));
}
if (flag)
swap(a, b);
return res;
}
void dfs1(int u, int f) {
mx[u] = make_tuple(1, u, u), up[u] = {make_pair(a[u], u)}, down[u] = {make_pair(-a[u], u)};
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u), lis[u] = max(lis[u], get<0>(mx[v]));
mx[u] = max(max(mx[u], mx[v]), max(query(up[u], down[v]), query(up[v], down[u])));
insert(up[v], a[u]), merge(up[u], up[v]);
insert(down[v], -a[u]), merge(down[u], down[v]);
}
}
bool dfs2(int u, int f, int t) {
path.emplace_back(u);
if (u == t)
return true;
for (int v : G.e[u])
if (v != f && dfs2(v, u, t))
return true;
path.pop_back();
return false;
}
signed main() {
freopen("lis.in", "r", stdin);
freopen("lis.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs1(1, 0);
int s = get<1>(mx[1]), t = get<2>(mx[1]);
dfs1(s, 0), dfs1(t, 0), dfs2(s, 0, t);
int ans = n + 1;
for (int it : path)
ans = min(ans, lis[it]);
printf("%d", ans);
return 0;
}
*并行程序
有 \(n\) 个程序,所有程序共享一个全局型变量 \(x\) ,此外每个程序都有一个私有变量 \(y\) 。第 \(i\) 个程序由 \(l_i\) 条指令组成,每个指令都属于以下四种类型之一:
W
:令 \(y \gets x\) 。Z
:令 \(x \gets y\) 。+ c
:令 \(y \gets y + c\) 。- c
:令 \(y \gets y - c\) 。这些程序将会并行运行,所有变量初值均为 \(0\) 。
这些程序的指令交错执行,即所有程序的所有指令都是一个接一个地执行,对于每个时刻,每个程序满足它的指令的一个前缀以一定顺序被执行。
计算所有程序并行执行后变量 \(x\) 的最小可能值。
\(n, \sum l_i \leq 10^6\)
Day 10 - 欧阳达晟
这是第一题
有一个 \(n \times m\) 的棋盘,其中列是循环的(即第 \(0\) 列即为第 \(m\) 列,第 \(m + 1\) 列即为第 \(1\) 列)。一些格子是障碍,一些格子有棋子。
先后手轮流操作,每次可以选择一枚棋子,设其在 \((i, j)\) ,将其移动到 \((i + 1, j), (i, j - 1), (i, j + 1)\) 中的一个非障碍格子上,需要保证不存在此前的某个时刻该棋子在目标格子上。
注意不同棋子之间没有限制,多个棋子可以位于同一坐标。
无法操作者败,求先手是否存在必胜策略。
\(n, m \leq 1000\)
显然要求出每个格子的 SG 值,然后用 SG 定理求解。
当一行存在障碍时,由于转移不能成环,因此考虑用 \(0, 1, 2\) 三个状态区分格子,其中 \(0\) 表示向左,\(1\) 表示向右,\(2\) 表示未确定,不难 \(O(m)\) 求出每个状态的 SG 值,格子的 SG 值即为状态 \(2\) 的 SG 值。
考虑一行没有状态时,对于固定的点求 SG 值,同样可以通过用 \(0, 1, 2\) 三个状态区分格子,但是不同的格子求解时各个状态的 SG 值并不等价。注意到转移一定是从左边绕一圈到右边和从右边绕一圈到左边,并且只会和底下一行的 SG 值取 \(\mathrm{mex}\) 。由于 SG 值值域很小,因此可以对每个前后缀预处理初值为 \(x\) 时从左到右、从右到左依次转移后的结果,同样不难做到 \(O(m)\) 。
时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int sg[N][N][3], pre_to_l[N][3], pre_to_r[N][3], suf_to_l[N][3], suf_to_r[N][3];
char str[N][N];
int n, m, be;
int dfs1(int x, int y, int z) {
if (~sg[x][y][z])
return sg[x][y][z];
bitset<5> bit;
if (x + 1 <= n && str[x + 1][y] != '#')
bit.set(sg[x + 1][y][2]);
if (str[x][(y == 1 ? m : y - 1)] != '#' && z != 1)
bit.set(dfs1(x, (y == 1 ? m : y - 1), 0));
if (str[x][y % m + 1] != '#' && z)
bit.set(dfs1(x, y % m + 1, 1));
int mex = 0;
while (bit.test(mex))
++mex;
return sg[x][y][z] = mex;
}
inline int calc(int x, int y) {
for (int i = 0; i <= 2; ++i)
if (x != i && y != i)
return i;
}
signed main() {
freopen("first.in", "r", stdin);
freopen("first.out", "w", stdout);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", str[i] + 1);
memset(sg, -1, sizeof(sg));
for (int i = n; i; --i) {
if (count(str[i] + 1, str[i] + m + 1, '#')) {
for (int j = 1; j <= m; ++j)
if (str[i][j] != '#')
dfs1(i, j, 2);
} else {
if (i == n) {
for (int j = 1; j <= m; ++j)
sg[i][j][2] = (~m & 1);
continue;
} else if (m == 1) {
for (int j = 1; j <= m; ++j)
sg[i][j][2] = (str[i + 1][j] != '#' && !sg[i + 1][j][2]);
continue;
}
for (int j = 0; j < 3; ++j)
pre_to_l[0][j] = pre_to_r[0][j] = j;
for (int j = 1; j <= m; ++j)
for (int k = 0; k < 3; ++k) {
pre_to_l[j][k] = pre_to_l[j - 1][calc(k, sg[i + 1][j][2])];
pre_to_r[j][k] = calc(pre_to_r[j - 1][k], sg[i + 1][j][2]);
}
for (int j = 0; j < 3; ++j)
suf_to_l[m + 1][j] = suf_to_r[m + 1][j] = j;
for (int j = m; j; --j)
for (int k = 0; k < 3; ++k) {
suf_to_l[j][k] = calc(suf_to_l[j + 1][k], sg[i + 1][j][2]);
suf_to_r[j][k] = suf_to_r[j + 1][calc(k, sg[i + 1][j][2])];
}
for (int j = 1; j <= m; ++j) {
bitset<5> bit;
bit.set(suf_to_l[j + 1][pre_to_l[j - 1][2]]), bit.set(pre_to_r[j - 1][suf_to_r[j + 1][2]]);
if (str[i + 1][j] != '#')
bit.set(sg[i + 1][j][2]);
int mex = 0;
while (bit.test(mex))
++mex;
sg[i][j][2] = mex;
}
}
}
int ans = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (str[i][j] == 'B')
ans ^= sg[i][j][2];
puts(ans ? "Alice" : "Bob");
return 0;
}
这是第二题
有一个 \(2n \times 2m\) 的网格,行列编号从 \(0\) 开始。初始时 \((i, j)\) 上的数字为 \(2mi + j + 1\) ,并以 \(2 \times 2\) 为单位进行黑白交替染色,注意一个格子的颜色与其上面的数字无关。
\(q\) 次操作,操作有:
- 对每个 \(j \in [0, m - 1]\) ,交换第 \(2j\) 列和第 \(2j + 1\) 列。
- 对每个 \(j \in [0, m - 2]\) ,交换第 \(2j + 1\) 列和第 \(2j + 2\) 列。
- 对每个 \(i \in [0, n - 1]\) ,交换第 \(2i\) 行和第 \(2i + 1\) 行。
- 对每个 \(i \in [0, n - 2]\) ,交换第 \(2i + 1\) 行和第 \(2i + 2\) 行。
- 将黑色的 \(2 \times 2\) 单元格顺时针旋转,将白色的 \(2 \times 2\) 单元格逆时针旋转。
求最终网格上每个位置的数字。
\(nm, q \leq 10^6\)
记原矩阵为 \(F\) ,将其不断镜像翻转铺满整个平面,注意黑白单元格的染色依旧为间隔染色,而不是复制 \(F\) 原本的染色。
此时前四个操作的 \(j\) 均可以任意取值,因此一个数字会被交换到什么位置只跟它初始坐标奇偶性与初始坐标的颜色有关,只需模拟 \(8\) 个数字的轨迹即可求解答案。
为了方便可以维护 \(2 \times 2\) 个单元格(即 \(4 \times 4\) 个数字)的轨迹,不难发现所有 \(2 \times 2\) 个单元格的轨迹等价。
时间复杂度 \(O(nm + q)\) 。
#include <bits/stdc++.h>
using namespace std;
pair<int, int> a[4][4];
int n, m, q;
signed main() {
freopen("second.in", "r", stdin);
freopen("second.out", "w", stdout);
scanf("%d%d%d", &n, &m, &q);
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
a[i][j] = make_pair(i, j);
while (q--) {
int op;
scanf("%d", &op);
if (op == 1) {
for (int i = 0; i < 4; ++i)
swap(a[i][0], a[i][1]), swap(a[i][2], a[i][3]);
} else if (op == 2) {
for (int i = 0; i < 4; ++i) {
swap(a[i][0], a[i][3]), swap(a[i][1], a[i][2]);
a[i][0].second = (a[i][0].second - 4 + m * 4) % (m * 4);
a[i][3].second = (a[i][3].second + 4) % (m * 4);
}
} else if (op == 3) {
for (int i = 0; i < 4; ++i)
swap(a[0][i], a[1][i]), swap(a[2][i], a[3][i]);
} else if (op == 4) {
for (int i = 0; i < 4; ++i) {
swap(a[0][i], a[3][i]), swap(a[1][i], a[2][i]);
a[0][i].first = (a[0][i].first - 4 + n * 4) % (n * 4);
a[3][i].first = (a[3][i].first + 4) % (n * 4);
}
} else {
auto update1 = [](int x, int y) {
swap(a[x][y], a[x + 1][y]), swap(a[x + 1][y], a[x + 1][y + 1]), swap(a[x + 1][y + 1], a[x][y + 1]);
};
update1(0, 0), update1(2, 2);
auto update2 = [](int x, int y) {
swap(a[x][y], a[x][y + 1]), swap(a[x][y + 1], a[x + 1][y + 1]), swap(a[x + 1][y + 1], a[x + 1][y]);
};
update2(0, 2), update2(2, 0);
}
}
for (int i = 0; i < n * 2; ++i) {
for (int j = 0; j < m * 2; ++j) {
int x = (i / 4 * 4 + a[i & 3][j & 3].first) % (n * 4), y = (j / 4 * 4 + a[i & 3][j & 3].second) % (m * 4);
if (x >= n * 2)
x = n * 4 - 1 - x;
if (y >= m * 2)
y = m * 4 - 1 - y;
printf("%d ", x * m * 2 + y + 1);
}
puts("");
}
return 0;
}
这是第三题
一个 \(n \times m\) 的棋盘上有若干位置被放上棋子,需要在部分空位上放上棋子,使得存在唯一的在棋子间两两匹配的方案,满足每对棋子在同一行或同一列。
请你求出额外添加的棋子数的最小值,或报告无解。
\(n, m \leq 1000\)
对行和列建点 \(r_{1 \sim n}, c_{1 \sim m}\) ,\((i, j)\) 上有棋子则连边 \((r_i, c_j)\) ,则一组匹配方案即匹配有公共点的边。
先考虑如何判定一个局面是否合法,单独考虑每个连通块。
若连通块为一颗树,则任取一点为根,自底往上考虑每个点。设考虑到点 \(x\) 时其当前度数(不包含连向父亲的边)为 \(d_x\) ,若 \(2 \mid d_x\) ,则两两匹配这些边,否则把连向父亲的边也一起拉来两两匹配(这会使父亲的度数 \(-1\) )。
容易发现,只要有偶数条边,这样总可以找到一组匹配方案,故有唯一方案当且仅当 \(d_x \leq 2\) 且 \(d_{rt} = 0\) 或 \(d_{rt} = 0\) ,其中 \(rt\) 为选定的根。
若连通块有环,将边随意挂在两端点中的一侧,再考虑树的情况,总可以得到不同的方案(或无解),故一定不存在唯一方案。
因此合法方案一定是若干合法的树构成的森林。
回到原问题,现要将若干棵树连接成若干合法的树。
对于某棵初始不合法的树 \(T\) ,需通过连接其他树在它的部分节点上挂上一些边使其合法。假设存在一种方式,使得在该树的 \(r\) 类节点上挂 \(sr_T\) 条边,\(c\) 类节点上挂 \(sc_T\) 条边,让该树合法。区分两类点的原因为仅能在 \(r\) 和 \(c\) 类点之间连边。
对每棵树选定一个 \(sr_T\) 和 \(sc_T\) 后,记 \(Sr = \sum_T sr_T, Sc \sum_T sc_T\) ,设 \(r\) 类孤立点有 \(a_r\) 个,\(c\) 类孤立点有 \(a_c\) 个,其余合法树数量为 \(p\) ,非法树数量为 \(q\) 。由于孤立点只能连向另一侧,可以写出必要条件:
事实上该条件也是充分的,如果将一个合法树与某个非法树相连,则这条边只能用在非法树上,故可以通过这种方式构造方案。按任意顺序依次考虑每棵非法的树,把当前有的合法树用来加边(优先使用孤立点)使其合法,若发现合法树不够用,则由于还没考虑的非法树的 \(sc_T + sr_T \geq 1\) ,推出上式不成立,矛盾,故合法树一定够用。
考虑 DP,设 \(f_{u, i, j}\) 表示考虑 \(u\) 的子树,\(sc = i\) ,加边后 \(d_u = j\) 时 \(sr\) 的最小值,其中 \(j \in \{ 0, 1, 2 \}\) 。转移类似树形背包,时间复杂度 \(O((n + m)^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int siz[N], f[N][N][3], g[N][3], mn[N];
char str[N];
bool vis[N];
int n, m;
bool dfs(int u, int fa) {
vis[u] = true, siz[u] = (u >= n);
memset(f[u], inf, sizeof(f[u])), f[u][0][0] = 0;
if (u <= n)
f[u][0][1] = 1;
else
f[u][1][1] = 0;
for (int v : G.e[u]) {
if (v == fa)
continue;
if (vis[v] || !dfs(v, u))
return false;
memset(g, inf, sizeof(g));
for (int i = 0; i <= siz[u]; ++i)
for (int j = 0; j < 3; ++j)
for (int p = 0; p <= siz[v]; ++p)
for (int q = 0; q < 3; ++q)
if (j + ((q & 1) ^ 1) < 3)
g[i + p][j + ((q & 1) ^ 1)] = min(g[i + p][j + ((q & 1) ^ 1)], f[u][i][j] + f[v][p][q]);
memcpy(f[u], g, sizeof(f[u])), siz[u] += siz[v];
}
return true;
}
signed main() {
freopen("third.in", "r", stdin);
freopen("third.out", "w", stdout);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
for (int j = 1; j <= m; ++j)
if (str[j] == '#')
G.insert(i, j + n), G.insert(j + n, i);
}
memset(mn, inf, sizeof(mn)), mn[0] = 0;
int single[2] = {0, 0}, legal = 0, illegal = 0;
for (int i = 1; i <= n + m; ++i) {
if (vis[i])
continue;
if (G.e[i].empty()) {
++single[i > n];
continue;
}
if (!dfs(i, 0))
return puts("-1"), 0;
if (!min(f[i][0][0], f[i][0][2]))
++legal;
else
++illegal;
for (int j = m; ~j; --j) {
int res = inf;
for (int k = 0; k <= min(j, siz[i]); ++k)
res = min(res, mn[j - k] + min(f[i][k][0], f[i][k][2]));
mn[j] = res;
}
}
if (!illegal)
return puts("0"), 0;
int ans = inf;
for (int i = 0; i <= m; ++i)
if (max(i - single[0], 0) + max(mn[i] - single[1], 0) <= legal + illegal - 1)
ans = min(ans, i + mn[i]);
printf("%d\n", ans == inf ? -1 : ans);
return 0;
}
Day 11 - 拼好赛(蒋凌宇、胡晓虎、刘恒熙)
硬币
这是一道交互题。
有 \(n\) 堆硬币,第 \(i\) 堆硬币有 \(a_i\) 枚。其中有一堆全是假硬币,其余堆都是真硬币。真硬币质量为 \(5\) ,假硬币质量为 \(6\) 。
每次可以从每堆中各选出若干个硬币 \(p_{0 \sim n - 1}\) ,其中 \(0 \leq p_i \leq a_i\) ,查询这些硬币的质量和。
用最少的称重次数找到假币是哪一堆。
\(n \leq 10^6\)
一次询问可以知道假币询问的 \(p_i\) 的值,因此考虑每次将候选答案集合尽可能多的按 \(p_i\) 分组。
具体地,先按 \(a\) 排序,每次从 \(\leq a_i\) 的 \(p\) 中选备选次数最小的即可,这可以通过优先队列维护。
记 \(cnt_x\) 为 \(x\) 的操作次数,操作上界为:
下面证明这个上界同时也是下界。对于任意 \(x \geq 2\) 原问题比 \(\sum_{i = 1}^{x - 1} cnt_i\) 个 \(x\) 更难,而这个新问题只能是 \(x\) 叉树,因此答案 \(\geq \log_x ( \sum_{i = 1}^{x - 1} cnt_i )\) 。
#include <bits/stdc++.h>
#include "coins.h"
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;
int solve(vector<int> a) {
int n = a.size();
for (int &it : a)
it = min(it, n);
vector<int> id(n);
iota(id.begin(), id.end(), 0);
sort(id.begin(), id.end(), [&](const int &x, const int &y) {
return a[x] < a[y];
});
while (id.size() > 1) {
vector<int> qry(n);
priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > q;
ll sum = 0;
for (int i = 0; i < id.size(); ++i) {
for (int j = i ? a[id[i - 1]] + 1 : 0; j <= a[id[i]]; ++j)
q.emplace(0, j);
auto mn = q.top();
q.pop(), sum += (qry[id[i]] = mn.second), ++mn.first, q.emplace(mn);
}
ll res = weigh(qry) - sum * 5;
vector<int> now;
for (int it : id)
if (qry[it] == res)
now.emplace_back(it);
id = now;
}
return id[0];
}
厵神
有一个无穷大的网格,初始时 \(a_{i, j} = [1 \leq i \leq n][1 \leq j \leq m]\) 。
接下来每一个时刻网格的权值都会扩张,具体的方式为 \(a'_{i, j} = a_{i - 1, j} + a_{i, j - 1} + a_{i + 1, j} + a_{i, j + 1}\) ,并在扩张结束后将所有 \(i \in \{0, n + 1 \}, j \in [1, m]\) 和 \(i \in [1, n], j \in \{ 0, m + 1 \}\) 处的权值清零。
若在 \(t\) 时刻后将 \((x, y)\) 清零,求 \(t'\) 时刻后 \((x', y')\) 处的权值 \(\bmod 1004535809\) 的值。
\(n, m \leq 10^9\) ,\(t' \leq 10^5\)
先将问题转化为 \(n \times m\) 的网格中从 \((x', y')\) 出发,四联通地移动 \(t'\) 次的方案数,其中第 \(t' - t\) 次移动后不能位于 \((x, y)\) ,后者可以转化为从 \((x', y')\) 出发移动 \(t - t'\) 次移动到 \((x, y)\) 的方案数乘上从 \((x, y)\) 出发移动 \(t'\) 次的方案数。
下面讨论从 \((x, y)\) 出发移动 \(t\) 次的方案数,定点到定点的求法是类似的。不难发现两维坐标独立,可以分开讨论,最后合并答案即可。问题转化为从 \(a\) 出发,每次可以将坐标 \(\pm 1\) ,求方案数。下文令 \(t = t'\) 。
如果直接暴力模拟,可以做到 \(O(nt)\) 。
这个问题的形式是经典的格路计数问题,即从 \((0, x)\) 开始走,\((x, y)\) 只能走到 \((x + 1, y \pm 1)\) ,不能碰到 \(y = 0\) 和 \(y = n\) 的方案数。但是终点并不固定,考虑将其转化为任意走的方案数减去第一次走出 \([1, n]\) 的方案数,枚举第一次走出去的时间,则答案形如 \(\sum f_{i - 1} 2^{t - i}\) ,\(f\) 即为固定终点为 \(1\) 或 \(n\) 的答案,时间复杂度 \(O(\frac{t^2}{n})\) 。
考虑平衡复杂度,取 \(T = \sqrt{n}\) ,当 \(T \leq t\) 时暴力模拟,否则直接反射容斥,时间复杂度 \(O(n \sqrt{t})\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1004535809;
const int N = 2e5 + 7;
int fac[N], inv[N], invfac[N], pw[N], ipw[N];
int n, m, x, xx, y, yy, t, tt;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
pw[0] = ipw[0] = 1;
for (int i = 1; i < N; ++i)
pw[i] = 2ll * pw[i - 1] % Mod, ipw[i] = 1ll * ipw[i - 1] * inv[2] % Mod;
}
inline int C(int n, int m) {
return n < 0 || m < 0 || m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
namespace Method1 {
inline vector<int> solve1(int n, int be, int t) {
vector<int> f(n + 1), ans(t + 1);
f[be] = 1, ans[0] = 1;
for (int i = 1; i <= t; ++i) {
vector<int> g(n + 1);
for (int j = 1; j <= n; ++j)
ans[i] = add(ans[i], g[j] = add(j > 1 ? f[j - 1] : 0, j < n ? f[j + 1] : 0));
swap(f, g);
}
return ans;
}
inline vector<int> solve2(int n, int be, int ed, int t) {
vector<int> f(n + 1), ans(t + 1);
f[be] = 1, ans[0] = f[ed];
for (int i = 1; i <= t; ++i) {
vector<int> g(n + 1);
for (int j = 1; j <= n; ++j)
g[j] = add(j > 1 ? f[j - 1] : 0, j < n ? f[j + 1] : 0);
swap(f, g), ans[i] = f[ed];
}
return ans;
}
} // namespace Method1
namespace Method2 {
inline int calc(int n, int m, int l, int r) {
int nn = (n + m) / 2, mm = (n - m) / 2;
n = nn, m = mm;
auto flip = [](int &x, int &y, int k) {
swap(x, y), x += k, y -= k;
};
int ans = C(n + m, m), x = n, y = m;
while (x >= 0 && y >= 0) {
flip(x, y, l), ans = dec(ans, C(x + y, y));
flip(x, y, r), ans = add(ans, C(x + y, y));
}
x = n, y = m;
while (x >= 0 && y >= 0) {
flip(x, y, r), ans = dec(ans, C(x + y, y));
flip(x, y, l), ans = add(ans, C(x + y, y));
}
return ans;
}
inline vector<int> solve2(int n, int be, int ed, int t) {
if (ed < be)
swap(ed, be);
vector<int> ans(t + 1);
for (int i = (ed - be) & 1; i <= t; i += 2)
ans[i] = calc(i, ed - be, -be, n + 1 - be);
return ans;
}
inline vector<int> solve1(int n, int be, int t) {
vector<int> f = solve2(n, be, 1, t), g = solve2(n, be, n, t), ans(t + 1);
for (int i = 1; i <= t; ++i)
ans[i] = add(ans[i - 1], 1ll * add(f[i - 1], g[i - 1]) * ipw[i] % Mod);
ans[0] = 1;
for (int i = 1; i <= t; ++i)
ans[i] = dec(pw[i], 1ll * ans[i] * pw[i] % Mod);
return ans;
}
} // namespace Method2
inline int solve1(int x, int y, int t) {
vector<int> f = (n <= sqrt(t) ? Method1::solve1(n, x, t) : Method2::solve1(n, x, t)),
g = (m <= sqrt(t) ? Method1::solve1(m, y, t) : Method2::solve1(m, y, t));
int ans = 0;
for (int i = 0; i <= t; ++i)
ans = add(ans, 1ll * f[i] * g[t - i] % Mod * fac[t] % Mod * invfac[i] % Mod * invfac[t - i] % Mod);
return ans;
}
inline int solve2(int bx, int by, int ex, int ey, int t) {
vector<int> f = (n <= sqrt(t) ? Method1::solve2(n, bx, ex, t) : Method2::solve2(n, bx, ex, t)),
g = (m <= sqrt(t) ? Method1::solve2(m, by, ey, t) : Method2::solve2(m, by, ey, t));
int ans = 0;
for (int i = abs(bx - ex); i <= t - abs(by - ey); ++i)
ans = add(ans, 1ll * f[i] * g[t - i] % Mod * fac[t] % Mod * invfac[i] % Mod * invfac[t - i] % Mod);
return ans;
}
signed main() {
freopen("gen.in", "r", stdin);
freopen("gen.out", "w", stdout);
prework();
scanf("%d%d%d%d%d%d%d%d", &n, &m, &x, &xx, &y, &yy, &t, &tt);
printf("%d", dec(solve1(xx, yy, tt), t > tt ? 0 : 1ll * solve1(x, y, t) * solve2(x, y, xx, yy, tt - t) % Mod));
return 0;
}
*区间压缩
对于一个由区间构成的序列 \(a = ([l_1, r_1], \cdots, [l_n, r_n])\) ,定义其宽度为最小的 \(k\) 满足:存在一个由区间构成的序列 \(b = ([s_1, t_1], \cdots, [s_n, t_n])\) 满足:
- 对于所有 \(1 \leq i \leq n\) ,有 \(1 \leq s_i \leq t_i \leq k\) 。
- 对于所有 \(1 \leq i < j \leq n\) ,\([l_i, r_i]\) 与 \([l_j, r_j]\) 相交当且仅当 \([s_i, t_i]\) 与 \([s_j, t_j]\) 相交。
给定长度为 \(n\) 的序列 \(c\) ,\(q\) 次询问,每次给定参数 \(x, y\) ,然后按照以下方式计算出序列 \(a\) :初始序列 \(a\) 为空,对 \(c\) 中的每个元素 \([l_i, r_i]\) ,若 \([l_i, r_i] \cap [x, y] \neq \emptyset\) ,则在 \(a\) 添加 \([l_i, r_i] \cap [x, y]\) ,即 \([\max(l_i, x), \min(r_i, y)]\) 。
对序列 \(a\) 求其所有子序列的宽度和 \(\bmod 998244353\) 。
\(n, q \leq 5 \times 10^5\)