[NOI Online #1 提高组]
A
首先从 \(t = 2\) 的特殊部分分出发。
不难发现这个操作是很不直观的,于是可以考虑对于每个操作 \((u, v)\) 在 \(u, v\) 之间连一条无向边。
显然连通块之间要分开考虑,对于同一个联通块,一次操作连通块内权值之和没有改变。
于是可以令 \(c_i = a_i - b_i\),那么 \(a\) 序列变到 \(b\) 序列等价于 \(c\) 序列变为全 \(0\)。
由此可见一个连通块内全变为 \(0\) 的必要条件是连通块内权值总和为 \(0\),继续观察发现这是充要条件。简单证明如下:
从最简单特殊的连通图-树出发,可以发现叶子节点若要消去无论如何需要恰好进行确定的一些操作。
那么可以先将子树内全部清 \(0\),最后就只剩下了根节点。因为权值总和为 \(0\),因此根节点此时权值必为 \(0\)。
对于一般的联通图,任意选择一颗 \(dfs\) 树即可。
这启示我们在解决联通图的判定性问题时,往往可以从最简单的树出发,然后考虑简单环,然后基环树...
于是可以直接使用并查集维护这个过程,复杂度 \(\mathcal{O(Tn \log n)}\)。
回到原问题,不难发现通过上述的做法(证明)可以发现:用操作二连边起来的同意连通块内权值可以互相转移,于是可以考虑先将这个联通块缩起来方便下面的考虑。
之后我们同样在缩起来的连通块之间用操作一连边。
同样从最简单特殊的树出发,还是从叶子节点开始考虑。
那么每个点最终对根节点的贡献均为 \((-1) ^ {dep} \times c_i\),若最终根节点为 \(0\) 即可满足条件。
若可以换根,则充要条件应为:任选一个根向下黑白染色,黑色点权和与白色点权和相等。
这启示着我们要往二分图的方面思考,不难证明:二分图合法当且仅当两部权值之和相等。
那么对于一个非二分图,一定可以将所有点权运送到奇环上然后消去,但前提是点权之和必须为偶数。
总结一下本题做法:
-
首先按照操作二连边缩点后再按照操作一连边。
-
单独考虑每个连通块。若为二分图,判定条件为:两部权值相等;否则判定条件为:点权之和为偶数。
复杂度 \(\mathcal{O(Tn \log n)}\)。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define Next(i, u) for (int i = h[u]; i; i = e[i].next)
const int N = 1e5 + 5;
struct edge { int v, next;} e[N << 1];
struct node { int u, v;} r[N];
int T, n, m, u, v, F, ans, opt, tot, cnt, w[3], a[N], b[N], h[N], co[N];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
namespace dsu {
int w[N], fa[N], sz[N];
void reset() { rep(i, 1, n) fa[i] = i, w[i] = a[i] - b[i], sz[i] = 1;}
int find(int x) { return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);}
void Merge(int x, int y) {
int a = find(x), b = find(y);
if(a == b) return ;
if(sz[a] > sz[b]) swap(a, b);
fa[a] = b, w[b] += w[a], sz[b] += sz[a];
}
}
void add(int u, int v) {
e[++tot].v = v, e[tot].next = h[u], h[u] = tot;
e[++tot].v = u, e[tot].next = h[v], h[v] = tot;
}
void dfs(int u, int col) {
w[col] += dsu :: w[u], co[u] = col;
Next(i, u) {
int v = e[i].v;
if(!co[v]) dfs(v, 3 - col);
else if(co[v] != 3 - col) F = true;
}
}
int main () {
T = read();
while (T--) {
n = read(), m = read();
rep(i, 1, n) a[i] = read();
rep(i, 1, n) b[i] = read();
cnt = 0, ans = 1, dsu :: reset();
rep(i, 1, m) {
opt = read(), u = read(), v = read();
if(opt == 1) r[++cnt] = (node){u, v};
else dsu :: Merge(u, v);
}
tot = 0, memset(h, 0, sizeof(h));
memset(co, 0, sizeof(co));
rep(i, 1, cnt) add(dsu :: find(r[i].u), dsu :: find(r[i].v));
rep(i, 1, n) if(dsu :: find(i) == i && !co[i]) {
F = w[1] = w[2] = 0, dfs(i, 1);
if(!F) ans &= (w[1] == w[2]);
else ans &= !((w[1] + w[2]) & 1);
}
printf(ans ? "YES\n" : "NO\n");
}
return 0;
}
B
可以发现,冒泡排序的本质是第 \(i\) 轮时将 \(n - i + 1\) 向后挪直至排好。
因此每一轮冒泡排序后考虑以 \(i\) 为结尾的逆序对个数 \(f_i\),相当于将 \(f\) 向左移一位后将非 \(0\) 的位置减 \(1\)。
因此 \(k\) 轮冒泡排序后逆序对的数量本质上是 \(\sum\limits_{i > k} f_i(f_i > k) - k \times \sum\limits_{i > k} [f_i > k]\)。
注意到一定有 \(f_i < k(i \le k)\),所以等价于求 \(\sum f_i(f_i > k) - k \times \sum [f_i > k]\)。
使用权值树状数组维护即可,修改只需要单点修改。
复杂度 \(\mathcal{O((n + m) \log n)}\)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 2e5 + 5;
int n, m, x, opt, p[N], a[N];
struct FT {
#define lowbit(x) (x & (-x))
int c[N];
void add(int p, int k) { if(!p) return ; for (int i = p; i <= n; i += lowbit(i)) c[i] += k;}
int query(int p) { int ans = 0; for (int i = p; i; i -= lowbit(i)) ans += c[i]; return ans;}
}T[3];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
signed main () {
n = read(), m = read();
rep(i, 1, n) {
p[i] = read(), a[i] = T[0].query(n) - T[0].query(p[i]);
T[0].add(p[i], 1), T[1].add(a[i], 1), T[2].add(a[i], a[i]);
}
while (m--) {
opt = read(), x = read();
if(opt == 1) {
T[1].add(a[x], -1), T[1].add(a[x + 1], -1);
T[2].add(a[x], -a[x]), T[2].add(a[x + 1], -a[x + 1]);
if(p[x] > p[x + 1]) --a[x + 1];
else ++a[x];
swap(p[x], p[x + 1]), swap(a[x], a[x + 1]);
T[1].add(a[x], 1), T[1].add(a[x + 1], 1);
T[2].add(a[x], a[x]), T[2].add(a[x + 1], a[x + 1]);
}
else {
if(x >= n) { puts("0"); continue ;}
int cnt = T[1].query(n) - T[1].query(x);
printf("%lld\n", T[2].query(n) - T[2].query(x) - cnt * x);
}
}
return 0;
}
C
首先考虑一下 \(k = 1\) 的特殊情况,可以猜想尽可能让大的数字相乘是更优的。
下面的证明是学的 EI 的方法:
单纯从代数的角度发现没有出路,于是可以考虑将这个东西数形结合,直观地表示出来。
通过乘积可以联想到面积,但两个数的乘积不好直接表示但可知:\(\frac{a_i ^ 2 + a_j ^ 2 - (a_i - a_j) ^ 2}{2} = a_i \times a_j\) 于是原问题等价于求 \(\sum a_i ^ 2 - \frac{\sum\limits (a_i - a_{i + 1}) ^ 2}{2}\) 的最大值,前者是一个定值,只需最小化后者即可。
不难发现,后者可以被看作是 \((a_i, a_i) \sim (a_{i + 1}, a_{i + 1})\) 组成的正方形。
于是问题进一步转化为从环上 \(p_1\) 开始走完一圈,每次加入 \((a_{p_i}, a_{p_i}) \sim (a_{p_{i + 1}}, a_{p_{i + 1}})\) 构成正方形的面积,最小化所有正方形面积之和。
下面我们定义 \(a - b\) 为从 \(a\) 中去除 \(b\) 的部分,\((a_i, a_j)\) 表示 \((a_i, a_i) \sim (a_j, a_j)\) 构成的正方形。
仔细分析可以发现因为经过某个点一次之后必定回来,所以 \(\forall i, (a_i, a_{i + 1})\) 至少要被算入两次。
同时,因为点不能重复到达,所以若存在某一次经过了 \((a_i, a_{i + 1})\) 则必定会经过 \((a_i, a_{i + 2})\),因此 \((a_i, a_{i + 2}) - (a_i, a_{i + 1}) - (a_{i + 1}, a_{i + 2})\) 至少会被算入一次。
此时可以发现,上面的贪心恰好达到了上述分析的下界。
那么对于 \(k > 1\) 的情况,仔细分析会发现若从 \(1\) 出发后经过一定的此时会回到 \(1\) 开始循环,这样的次数是满足 \(kx \equiv 0\pmod{n}\) 的最小整数解 \(x\),不难发现为 \(\frac{n}{\gcd(n, k)}\),于是这张图等价于被分成了 \(\gcd(n, k)\) 个大小为 \(\frac{n}{\gcd(n, k)}\) 环,因此本质不同的图只有 \(d(n)\) 个。
于是现在的问题就转化为合理分配权值给 \(x(x \mid n)\) 个长度为 \(\frac{n}{x}\) 的环,使得每个环内部进行 \(k = 1\) 的贪心后总权值最大。
基于观察上面贪心证明的观察可以发现:
-
不论如何分配,\(\sum a_i ^ 2\) 始终是一个定值,于是只需最小化每个环内 \(\frac{\sum\limits (a_i - a_{i + 1}) ^ 2}{2}\) 的值。
-
基于 \(1\) 及上述贪心证明可知,删除一个环中的最大值或最小值一定更优。
-
基于 \(1\) 及上述贪心证明可知,将一个一个不大于最大值或不小于最小值的值加入一定会使得答案更优。
因此我们可以发现,两个环的分配值域一定不会相交,否则可以交换端点(满足上述 \(2, 3\) 性质)使得答案更优。
总结一下本题做法:
-
本质不同的方案只有 \(d(n)\) 种,\(\forall k\) 会将环分成 \(\gcd(n, k)\) 个大小为 \(\frac{n}{\gcd(n, k)}\) 的 \(k = 1\) 的特殊情形。
-
最优的分配权值进入每个环的方案一定是 \(a_1 \sim a_{\frac{n}{\gcd(n, k)}}, a_{\frac{n}{\gcd(n, k)} + 1} \sim a_{\frac{n}{\gcd(n, k)} \times 2} \cdots\) 依次分配进每个环。
-
对于每个环内 \(k = 1\) 的特殊情况,一定是按照 \(a_1 \cdots a_{n - 1}, a_n, a_{n - 2} \cdots a_2(a_1 \le a_2 \le \cdots \le a_n)\) 的方式放置最优。
接下来对于每个 \(d \mid n\) 模拟上述流程即可,复杂度 \(\mathcal{O(nd(n))}\)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 2e5 + 5;
int n, m, k, a[N], ans[N];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int Gcd(int a, int b) { return !b ? a : Gcd(b, a % b);}
int solve(int k) {
int len = n / k, ans = 0;
rep(i, 1, k) {
ans += a[(i - 1) * len + 1] * a[(i - 1) * len + 2];
ans += a[i * len] * a[i * len - 1];
rep(j, 1, len - 2) ans += a[(i - 1) * len + j] * a[(i - 1) * len + j + 2];
}
return ans;
}
signed main () {
n = read(), m = read();
rep(i, 1, n) a[i] = read(), ans[0] += a[i] * a[i];
sort(a + 1, a + n + 1);
rep(i, 1, n / 2) if(n % i == 0) ans[i] = solve(i);
rep(i, 1, m) {
k = read();
printf("%lld\n", !k ? ans[k] : ans[Gcd(k, n)]);
}
return 0;
}