Codeforces Round #715 (Div. 1)
Codeforces Round #715 (Div. 1)
Codeforces Round #715 (Div. 1)
A
三个串中必定存在两个串 \(A,B\),满足\(max(min(number\ of\ 0\ in\ A,number\ of\ 0\ in\ B),min(number\ of\ 1\ in\ A,number\ of\ 1\ in\ B))\ge n\)
可以反证如果不存在就很离谱
也就是说我们构造一个 \(S\),使得 \(A,B\) 是 \(S\) 的子序列,可以共用的部分 \(\ge n\),那么最终的长度 \(\leq 3\times n\)
B
其实每个符合条件的序列可以被划分为若干段,以 \(i\) 为横坐标,\(a_i\) 为纵坐标,就是这样:
每一段都是斜率为 \(-1\) 的线段且相邻段首尾相接。那么如果长度和切割点一定,这个序列是唯一的。这就是一个简单的组合数。然后可以像进制转换那样去确定每一个切割点...
#include <bits/stdc++.h>
using namespace std;
#define int long long
void read (int &x) {
char ch = getchar(); int f = 0; x = 0;
while (!isdigit(ch)) { if (ch == '-') f = 1; ch = getchar(); }
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
if (f) x = -x;
} const int N = 1e5 + 5, lim = 1e5, mx = 1e18;
int n, k, p[N], in[N], num[N];
signed main() {
int T; read (T);
int now = 1;
for (int i = 1; i <= lim; ++i) {
num[i] = now; now *= 2;
if (now > mx) {
for (int j = i + 1; j <= lim; ++j) num[j] = mx * 5;
break;
}
} num[0] = 1;
while (T--) {
read (n), read (k);
if (k > num[n]) { puts ("-1"); continue; }
int p = 1, sum = 0, la = 0;
for (int i = 1; i <= n; ++i) {
sum += num[n - i];
if (sum == k) {
for (int j = i; j > la; --j) printf ("%lld ", j);
for (int j = n; j > i; --j) printf ("%lld ", j); break;
} else if (sum > k) {
sum -= num[n - i];
for (int j = i; j > la; --j) printf ("%lld ", j); la = i;
}
} puts ("");
}
}
C
补图中只要一条边有权值 \(X\) 就可以满足异或为 \(0\) 的要求。先处理补图的连通性。如果有某条边不影响连通性,就把权值赋到这条边上,不用算入答案(不会出现在生成树中),然后把确定的边加入执行 \(Kruscal\) 的过程。否则(一棵树)要把 \(X\) 算入答案,然后先把确定边加入,再类似次小生成树处理一个替换调整的过程,可以跳 \(LCA\) 或再弄一个并查集来维护
如何处理补图的连通性?肯定存在一个点 \(p\),度数 \(\leq \frac{m}{n}\),那么补图中不与 \(p\) 直接相连的点 \(\leq \frac{m}{n}\),对于这些点暴力判断,复杂度即为线性。也可以用一种边删边处理的方法解决。
D
把 \((i,a_i)\) 当成边,如果整个排列构成一个环,那么只需要随便选一个点,然后不断把当前点上的值归位即可,就像这样:
我们将其称之为“太阳线”,一个端点引出的线段显然没有交。
现在考虑多个环的情况,可以交换两个不同环上的两个点来合并成一个环。但交换会连出一些新的线,有可能会和“太阳线”相交。为了避免这种情况,我们先选定“中心点”,将其它所有点进行极角排序,对两个相邻点执行并查集的过程:如果在一个环中跳过,否则这两个环以这两点为媒介相连。容易发现这样构造不会和“太阳线”相交。
为了使极角排序简单顺利进行,不妨选最左点。
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); int f = 0; x = 0;
while (!isdigit(ch)) { if (ch == '-') f = 1; ch = getchar(); }
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
if (f) x = -x;
} const int N = 2020;
int n, p, fa[N], a[N], m, id[N], x[N], y[N]; double k[N];
int get (int x) {
return x == fa[x] ? x : fa[x] = get (fa[x]);
}
void merge (int x, int y) { fa[get (x)] = get (y); }
bool cmp (int a, int b) { return k[a] < k[b]; }
struct ans { int x, y; } o[N * 10]; int cnt;
void work (int x, int y) {
o[++cnt] = {x, y}, swap (a[x], a[y]);
}
signed main() {
read (n); x[0] = 1e9;
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int i = 1; i <= n; ++i) {
read (x[i]), read (y[i]);
read (a[i]), merge (i, a[i]);
if (i != a[i] && x[i] < x[p]) p = i;
}
if (!p) return puts ("0"), 0;
for (int i = 1; i <= n; ++i)
if (i != a[i] && i != p) id[++m] = i;
for (int i = 1; i <= m; ++i) {
k[id[i]] = 1.0 * (y[id[i]] - y[p]) / (x[id[i]] - x[p]);
}
sort (id + 1, id + m + 1, cmp);
for (int i = 1; i < m; ++i) {
int fx = get (id[i]), fy = get (id[i + 1]);
if (fx == fy) continue;
work (id[i], id[i + 1]), fa[fx] = fy;
}
while (a[p] != p) work (p, a[p]);
printf ("%d\n", cnt);
for (int i = 1; i <= cnt; ++i)
printf ("%d %d\n", o[i].x, o[i].y);
return 0;
}
E
每次操作会选一个 \(x\) 和 \(son(x)\) 中权值最小的点 \(y\) 交换且 \(a_x<a_y\),那么交换后 \(a_y\) 依旧是最小的,那么操作不改变兄弟节点间的相对大小。借此可以确定树的 \(dfs\) 序
可以把操作分为若干轮,第 \(i\) 轮把数字 \(i\) 从根节点一路往最小的儿子向下走,直到走不了。由此可以得出当前进行的操作次数。当前正在进行的是 \(a_1-1\) 轮,由此可以得出当前进行的操作次数为 \(1\) 到 \(a_1-1\) 数字所在节点的深度之和。剩下的问题是如何判断无解。
对于数字 \([1,a_1-2]\),操作已经结束,每个点所在的位置一定(可自行yy其规律,即代码中的 \(st\) 数组)
对于 \(a_1-1\) ,它的目标节点应当在当前所在节点的子树里,且它能够一直向上到根(即路径上权值都比它大)
对于 \([a_1,n]\) 所在的位置,它们的爸爸都得比自己小
#include <bits/stdc++.h>
using namespace std;
#define int long long
void read (int &x) {
char ch = getchar(); int f = 0; x = 0;
while (!isdigit(ch)) { if (ch == '-') f = 1; ch = getchar(); }
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
if (f) x = -x;
} const int N = 3e5 + 5, M = N << 1;
int n, s, a[N], w[N], tot, dfn[N], t[N], d[N], fa[N], o;
vector<pair<int, int> > g[N];
int cnt, h[N], nxt[M], to[M]; int tp, st[N];
void add (int u, int v) {
to[++cnt] = v, nxt[cnt] = h[u], h[u] = cnt;
}
#define pb push_back
void dfs (int u, int la) {
dfn[u] = ++tot, t[tot] = u, d[u] = d[la] + 1, fa[u] = la;
for (int i = h[u], v; i; i = nxt[i])
if ((v = to[i]) != la) g[u].push_back ({a[v], v});
sort (g[u].begin(), g[u].end());
for (auto i : g[u]) dfs (i.second, u); st[++tp] = u;
}
void YES () {
printf ("YES\n%lld\n", s);
for (int i = 1; i <= n; ++i) printf ("%lld ", dfn[i]);
}
void NO () { puts ("NO"), exit (0); }
void goup (int p) {
int x = w[p];
while (fa[x]) {
if (a[x] > a[fa[x]]) NO ();
swap (a[x], a[fa[x]]), x = fa[x];
}
x = st[p];
while (x) { if (x == w[p]) return; x = fa[x]; }
NO ();
}
signed main() {
read (n);
for (int i = 1; i <= n; ++i) read (a[i]), w[a[i]] = i;
for (int i = 1, u, v; i < n; ++i)
read (u), read (v), add (u, v), add (v, u);
dfs (1, 0); o = a[1];
for (int i = 1; i < o; ++i) s += d[w[i]] - 1;
for (int i = 1; i < o - 1; ++i) if (w[i] != st[i]) NO ();
if (o > 1) goup (o - 1);
for (int i = 1; i <= n; ++i)
if (a[fa[i]] > a[i] && a[i] >= o - 1) NO ();
return YES (), 0;
}
F
考虑对于一个固定的 \(k\) 求出答案。先按照题意暴力的连边 \((x,y)\) 表示限制 \(a_x<a_y\)。有这些个推导:
1、如果 \(u,v\) 之间存在长度 \(\ge 2\) 的路径,边 \((u,v)\) 无用
2、对于一个 \(x\),必须的边 \((x,r),r>x\) 最多只有一条,左边亦然。因为如果有两条 \((x,r1),(x,r2),x<r1<r2\),\(x\) 和 \(r2\) 同在的区间也包含 \(r1\),那么 \(r1,r2\) 间也有一条边或路径,根据结论 \(1\),\((x,r1)(x,r2)\) 中有一条边无用
3、如何找到这个唯一的 \(l,r\) 呢?在所有包含 \(x\) 的区间中找到右端点 \(R\) 最大的,\(r\) 即为区间 \((x,R]\) 中 \(a_x\) 的后继。\(l\) 亦然。
4、但结论 \(2\) 中唯一的 \((x,r)\) 也不一定必要,可能存在路径 \((x,\dots,t,r)\) 并根据结论 \(1\) 去掉这条边。如何判断?容易发现这个 \(t\) 必定 \(<x\),这次我们找到包含 \(r\) 的左端点 \(L\) 最小的区间,如果存在 \(p\),满足 \(L\leq p<x,a_x<a_p<a_r\),那么这个 \(p\) 可以作为 \(t\) 而使 \((x,r)\) 失效
以上四点可以让我们解决一个固定的 \(k\) 了:先用 \(3\) 求出所有 \(l,r\),再用 \(4\) 判断哪些 \(l,r\) 无用,剩下的即为答案。但通过这种方法求出所有 \(k\) 对应的答案似乎太慢了。
In fact,稍稍改进一下结论和做法就可以较快速地求出所有答案。我们维护的 \(l,r\) 就是向左向右的后继,不妨顺便把前驱也计算出来,把这四条边称为 “判定边”。结论 \(4\) 引理:一条边 \((x,y)\) 不会被删除当且仅当它同时是 \(x,y\) 的 “判定边”。可以自行证明其充要性。每个点的 “判定边” 的另一端单调,最多改变 \(O(n)\) 次,可以在 \(O(n^2)/O(nq)\) 时间内维护(直接遍历 每个区间为 \(nq\),先把不用更改的区间扔掉再遍历为 \(n^2\),差别不大),可以通过本题。
而官方题解的优秀做法就是处理一下每条边作为 “判定边” 的时间段然后 \(set\)+莫队 一顿操作弄到 \(O(n\sqrt{q}\ logn)\)。反正特别麻烦,所以只写了非官方做法(下面的代码常数较大)
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); x = 0; while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 25005;
int n, m, res, a[N], L[N], R[N], la[N][2], ra[N][2];
void work (int l, int r) {
for (int i = l; i <= r; ++i) {
for (int j = R[i] + 1; j <= r; ++j) {
int t = ra[i][0];
if (a[j] < a[i] && a[j] > a[t])
res += (la[j][1] == i) - (la[t][1] == i), ra[i][0] = j;
t = ra[i][1];
if (a[j] > a[i] && a[j] < a[t])
res += (la[j][0] == i) - (la[t][0] == i), ra[i][1] = j;
R[i] = j;
}
for (int j = L[i] - 1; j >= l; --j) {
int t = la[i][0];
if (a[j] < a[i] && a[j] > a[t])
res += (ra[j][1] == i) - (ra[t][1] == i), la[i][0] = j;
t = la[i][1];
if (a[j] > a[i] && a[j] < a[t])
res += (ra[j][0] == i) - (ra[t][0] == i), la[i][1] = j;
L[i] = j;
}
}
}
signed main() {
read (n), read (m); a[n + 1] = n + 1;
for (int i = 1; i <= n; ++i) read (a[i]);
for (int i = 1; i <= n; ++i)
R[i] = L[i] = i, la[i][1] = ra[i][1] = n + 1;
for (int i = 1, l, r; i <= m; ++i) {
read (l), read (r);
work (l, r); printf ("%d\n", res);
}
return 0;
}