本文搬运自本人的知乎文章。
https://zhuanlan.zhihu.com/p/588162564
好久没有在补题之后写题解的习惯了。
但是最近感觉有些题目的思路即使在题目通过后仍然难以理清,因此觉得需要写些东西帮助自己整理思路,另外也方便以后翻看积累到的技巧。
J. Strange Sum
题目链接
题意
给定序列 \(a_1, a_2,\cdots,a_n\)。
我们需要选择若干元素(可以不选),满足如果选择了 \(a_i\),那么序列中所有长度为 \(i\) 的区间中都最多只有两个元素被选择。
最大化选择的元素的和。
Solution
显然,考虑到假设我们选择的编号最大的元素为 \(a_p\),那么区间 \([1, p]\) 中也应该最多两个元素。
所以,整个序列中我们最多选择两个元素。
因此,假设序列中最大、次大的两个数分别为 \(b, c\),那么答案就是 \(\max\{0, b,b + c\}\)。
Code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
sort(a.begin(), a.end(), greater<int>());
int b = a[0], c = a[1];
cout << max({0, b, b + c}) << endl;
return 0;
}
L. Tree
题目链接
题意
给定一棵以 \(1\) 为根的有根树,将这棵树划分为若干点集,满足每个点集中,要么任意一个点对的两个点都有祖先关系,要么任意一个点对的两个点都没有祖先关系。
最小化点集的数量。
Solution
这个题目感觉做法会很多……
我比赛现场的时候卡了好久,后来借用长链剖分的思路才想出来,这个思路可能有点复杂了。
每个点集要么是一个链,要么是不同子树中的许多点。
显然,如果我们想要取一个链作为集合,那么只有把这个链一直取到叶子才是最优的。
那么我们考虑把这棵树做长链剖分,假设我们得到了 \(p\) 条长链,每条长链的长度为 \(lp_i\)。
假设我们一开始全都用第二类集合来划分,那么答案显然是整棵树最大的深度。这个答案也可以看做是将所有长链的底端对其以后水平放置,同一行的点划分进一个点集的结果,也就是这些长链的最大长度。(显然同一行中的点不存在祖先关系)
考虑贪心选择一些长链作为第一个集合,显然因为第二类集合的数量应该是“剩余长链的最大长度”,所以选择最长的长链必然最优。
将 \(lp_i\) 从大到小排序,枚举我们选择了前 \(i\) 条长链作为第一类集合,那么剩余的长链作为第二类集合的集合数量是 \(lp_{i + 1}\),因此答案就是 \(\max_{i=0}^p\{i + lp_{i + 1}\}\)。
时间复杂度 \(O(n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e6 + 7;
int n;
vector<int> g[N], lp;
int len[N], son[N];
void dfs1(int x) {
for (int y : g[x]) {
dfs1(y);
if (len[y] > len[son[x]]) son[x] = y;
}
len[x] = len[son[x]] + 1;
}
void dfs2(int x, int l = 0) {
if (!son[x]) lp.push_back(l);
else {
dfs2(son[x], l + 1);
for (int y : g[x]) if (y != son[x]) dfs2(y, 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
cin >> n;
lp.clear();
for (int x = 1; x <= n; ++x) son[x] = len[x] = 0, g[x].clear();
for (int i = 2, x; i <= n; ++i) cin >> x, g[x].push_back(i);
dfs1(1);
dfs2(1, 1);
sort(lp.begin(), lp.end(), greater<int>());
int p = lp.size(), ans = p;
for (int i = 0; i < p; ++i) ans = min(ans, lp[i] + i);
cout << ans << '\n';
}
return 0;
}
B. Cells Coloring
题目链接
题意
给定一个 \(n\times m\) 的方格,有一些格子上有障碍,剩余的格子可以被染色。
我们要选择一个颜色总数 \(k\),将每个无障碍的格子染成 \(0, 1, \cdots, k\) 中的一个颜色,并满足每行每列没有相同的非 \(0\) 颜色。
假设颜色后颜色为 \(0\) 的格子的个数为 \(z\) 个,你需要最小化 \(ck + dz\)。\(c, d\) 为题目提供的参数。
Solution
感觉是很基础的网络流题。
很容易看出来的结论是,如果我们染色后,每行每列的有色格子的数量的最大值是 \(x\),那么只需要 \(k = x\) 即可满足「每行每列没有相同的非 \(0\) 颜色」的限制。
(显然这样的网格需要满足的限制一定是一个 \(x \times x\) 的网格满足的限制的子集,而 \(x\times x\) 的网络一定存在只需要 \(x\) 中颜色的染色方案)
那么问题转化为,选择一些非障碍格子染色,令 \(k\) 为每行每列中被染色格子的最大数量,\(z\) 为未被染色的无障碍格,最小化 \(ck+dz\)。
考虑枚举 \(k\),使每行每列选择的格子数不超过 \(k\) 的情况下,最小化 \(z\) 也就是未被染色的格子数量。
这是一个很基本的最大流模型,源点向行连接容量为 \(k\) 的边,列向汇点连接容量 \(k\) 的边,如果一个格子可以被染色,那么这个格子的行向对应的列连接容量为 \(1\) 的边。
最大流的结果就是在限制下能够被染色的格子的最大数量,使用无障碍的格子的数量减去对应的结果即可得到 \(z\)。
如果每次都重新建图跑最大流,时间复杂度应为 \(O(n^3\sqrt n)\),未必能通过本题。
(然而我直接每次重新建图暴力最大流就跑得很快了,下面的这个优化对我的程序也没什么作用……)
一个常用的优化是,顺序枚举 \(k\),那么和所有边的容量只会增加,只需要在残量网络上增加容量,然后在上一次的基础上继续跑 Dinic 或者 ISAP 即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define fec(i, x, y) (int i = head[x], y = g[i].to; i; i = g[i].ne, y = g[i].to)
#define dbg(...) fprintf(stderr, __VA_ARGS__)
#define fi first
#define se second
using ll = long long; using ull = unsigned long long; using pii = pair<int, int>;
template <typename A, typename B> bool smax(A &a, const B &b) { return a < b ? a = b, 1 : 0; }
template <typename A, typename B> bool smin(A &a, const B &b) { return b < a ? a = b, 1 : 0; }
constexpr int N = 250 * 2 + 2 + 7;
constexpr int M = 250 * 250 + 250 * 2 + 7;
constexpr int INF = INT_MAX;
int n, m, S, T, nod;
char s[N][N];
int sc[N], cl[N];
struct Edge {int to, ne, f;} g[M << 1]; int head[N], tot = 1;
void addedge(int x, int y, int z) { g[++tot].to = y; g[tot].f = z; g[tot].ne = head[x]; head[x] = tot; }
void adde(int x, int y, int z) { addedge(x, y, z); addedge(y, x, 0); }
int dis[N], gap[N], cur[N], q[N];
void bfs() {
int hd = 0, tl = 0;
q[++tl] = T, ++gap[dis[T] = 1];
while (hd < tl) {
int x = q[++hd];
for fec(i, x, y) if (!dis[y] && g[i^1].f) dis[y] = dis[x] + 1, ++gap[dis[y]], q[++tl] = y;
}
}
int dfs(int x, int a) {
if (x == T || !a) return a;
int flow = 0, f;
for (int &i = cur[x]; i; i = g[i].ne)
if (dis[x] == dis[g[i].to] + 1 && (f = dfs(g[i].to, std::min(a, g[i].f)))) {
g[i].f -= f, g[i ^ 1].f += f;
a -= f, flow += f;
if (!a) return flow;
}
--gap[dis[x]];
if (!gap[dis[x]]) dis[S] = nod + 1;
++gap[++dis[x]];
return flow;
}
int ISAP() {
static int ans = 0;
bfs();
while (dis[S] <= nod) memcpy(cur + 1, head + 1, sizeof(*head) * nod), ans += dfs(S, INF);
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int c, d, mxk = 0, sum = 0;
cin >> n >> m >> c >> d;
for (int i = 1; i <= n; ++i) {
cin >> (s[i] + 1);
for (int j = 1; j <= m; ++j) s[i][j] = s[i][j] == '.', sc[i] += s[i][j], cl[j] += s[i][j], smax(mxk, cl[j]);
smax(mxk, sc[i]), sum += sc[i];
}
S = n + m + 1, T = nod = S + 1;
for (int i = 1; i <= n; ++i) adde(S, i, 0);
for (int i = 1; i <= m; ++i) adde(i + n, T, 0);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) if (s[i][j]) adde(i, j + n, 1);
ll ans = LLONG_MAX;
for (int k = 0; k <= mxk; ++k) {
if (k) {
memset(gap + 1, 0, sizeof(*gap) * nod);
memset(dis + 1, 0, sizeof(*dis) * nod);
for (int i = 1; i <= n; ++i) ++g[i * 2].f;
for (int i = 1; i <= m; ++i) ++g[(i + n) * 2].f;
}
smin(ans, (ll)c * k + (ll)d * (sum - ISAP()));
}
cout << ans << '\n';
return 0;
}
A. Bridge
题目链接
题意
有 \(n\) 条链,每条链上有依次连接的 \(1\cdots m+1\) 编号的点,\((a, b)\) 表示 \(a\) 链上的 \(b\) 号点。
两个操作:
- 在 \((a, b)\) 和 \((a + 1, b)\) 之间建一个双向桥。保证每个点只会和一个桥相连。
- 从 \((a, 1)\) 出发。如果当前点 \((x, y)\) 和本次操作内没有走过的一座桥连接,那么就走过桥。否则走到 \((x, y + 1)\)。达到任意一条链的 \(m+1\) 点就停下,输出最后停在的链的编号。
Solution
这题很容易看出来就是 LCT 简单应用了,但是代码有一些细节并不好写……
惊喜的是比赛现场的时候竟然交了一发就 AC 了,要是像之前数据结构题那样交个好几次那这一场就寄了。
首先,「本次操作内没有走过的一座桥」的限制,是用来保证不会在一座桥之间来回走,因此一次操作中,不可能重复经过一个点多次。
同时,因为每个点只会和一座桥相连,因此走过桥以后,下一步一定是在链上前进。因此我们如果把这两部看成一步,我们直接从桥的一个端点,到达走过桥以后继续走可以到的下一个点。显然,从这样的角度,只要一座桥 \((a, b) \leftrightarrow (a', b)\) 诞生了,我们可以认为一座有向桥是建立在了 \((a, b)\) 和 \((a', b+1)\) 之间的,因而在转化后的图中,这样的的 \((a, b)\) 是不可能再走到 \((a, b + 1)\) 了。因此,我们可以认为每一个点的后继是唯一确定的,因此整个图实际上是 \(n\) 棵树。
动态维护每一个点的后继显然可以用 LCT,但是直接维护树的话,点数的规模显然是我们不能接受的。我们考虑把这个图压缩一下。
在一条链上,如果没有建立过桥的点,那么它们只能继续走下去,没有其余选择。所以我们可以把每个点合并到右边的第一个有桥的点上,即,假设链 \(a\) 上有桥的点分别为 \(p_{a, 1}, p_{a,2}, \cdots, p_{a, k}\)(令 \(p_{a, k+1} = m + 1\)),那么我们可以把 \(p_{a, i} + 1\) 到 \(p_{a, i + 1}(0 \leq i \leq k)\) 中的点都是用一个点 \(v_{a, i}\) 来代表。我们对于每条链使用一个 set 维护现在压缩后的点,可以 \(\log\) 查询一个点被压缩到哪个点了。
然后我们来看操作 \(1\)。对于需要新建的桥 \((a, b) \leftrightarrow (a', b)\),如果 \((a, b), (a', b)\) 不在各自被压缩后的点集中,那么就加进压缩后的点集,同时需要在 LCT 上维护变化。具体的,如果 \((a, b)\) 这个点原先被压缩进了 \(v\) 点,其代表区域为 \([l, r]\),那么我们需要新建一个点 \(u\) 来代表 \([b + 1, r]\),那么这时我们需要修改 \(v\) 点的代表范围为 \([l, b]\)。这里我们仍然用 \(v\) 来代表 \((a, b)\) ,因为如果修改 \((a, b)\) 所属的压缩点的编号,就需要修改原先 \(v\) 的孩子的父亲,这个过程难以维护。然后,设原来的 \(v\) 的父节点是 \(fa\),我们需要将 \(v\) 和 \(fa\) 之间的边断开,然后连接 \(v\to u\),\(u\to fa\)。
新建完压缩的点以后,对于我们需要新建的桥 \((a, b) \leftrightarrow (a', b)\),我们找到这两个点对应的压缩点 \(v_1, v_2\),找到 \(u, v\) 在各自的链上后一个点(其实就是 LCT 上的父节点)\(f_1, f_2\),然后断开 \(v_1\to f_1\) 和 \(v_2\to f_2\),连接 \(v_1\to f_2\) 和 \(v_2\to f_1\)。
对于询问,只需要找到每一个链的起始点所在子树的根即可。
需要注意的是,因为我们维护的是有根树,所以不能在 LCT 中使用 makeroot
操作,因此某些 link
, cut
的写法可能需要修改。
Code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 3e5 + 7;
#define lc c[0]
#define rc c[1]
struct Node { int c[2], fa; bool rev; } t[N];
int S[N];
bool isroot(int o) { return t[t[o].fa].lc != o && t[t[o].fa].rc != o; }
bool idtfy(int o) { return t[t[o].fa].rc == o; }
void connect(int fa, int o, int d) { t[fa].c[d] = o, t[o].fa = fa; }
void pushup(int o) { }
void pushdown(int o) {
if (!t[o].rev) return;
t[o].rev = 0;
if (t[o].lc) swap(t[t[o].lc].lc, t[t[o].lc].rc), t[t[o].lc].rev ^= 1;
if (t[o].rc) swap(t[t[o].rc].lc, t[t[o].rc].rc), t[t[o].rc].rev ^= 1;
}
void rotate(int o) {
int fa = t[o].fa, pa = t[fa].fa, d1 = idtfy(o), d2 = idtfy(fa), b = t[o].c[d1 ^ 1];
t[o].fa = pa, !isroot(fa) && (t[pa].c[d2] = o);
connect(fa, b, d1), connect(o, fa, d1 ^ 1);
pushup(fa), pushup(o);
}
void splay(int o) {
int x = o, tp = 1;
S[tp] = x;
while (!isroot(x)) S[++tp] = x = t[x].fa;
while (tp) pushdown(S[tp--]);
while (!isroot(o)) {
int fa = t[o].fa;
if (isroot(fa)) rotate(o);
else if (idtfy(o) == idtfy(fa)) rotate(fa), rotate(o);
else rotate(o), rotate(o);
}
}
void access(int o) {
for (int x = 0; o; x = o, o = t[o].fa)
splay(o), t[o].rc = x, pushup(o);
}
int findrt(int x) {
access(x), splay(x);
while (pushdown(x), t[x].lc) x = t[x].lc;
return splay(x), x;
}
void mkrt(int x) {
access(x), splay(x);
t[x].rev ^= 1, swap(t[x].lc, t[x].rc);
}
int findfa(int x) {
access(x), splay(x);
pushdown(x), x = t[x].lc;
while (pushdown(x), t[x].rc) x = t[x].rc;
if (x) splay(x);
return x;
}
void split(int x, int y) { mkrt(x), access(y), splay(y); }
void link(int x, int y) {
access(x), splay(x), t[x].fa = y;
}
void cut(int x, int y) {
access(x), splay(x);
t[x].lc = t[t[x].lc].fa = 0;
if (t[y].lc == x && t[x].fa == y) t[y].lc = t[x].fa = 0, pushup(y);
}
using pii = pair<int, int>;
int n, m, q, nod;
int bl[N];
set<pii> ps[N];
int get(int id, int x) {
set<pii> &s = ps[id];
pii u(x, 0);
auto p = s.upper_bound(u);
if (p->first == x) return p->second;
int old = p->second, oldv = p->first;
s.erase(p);
++nod;
bl[nod] = id;
int fa = findfa(old);
if (fa) cut(old, fa);
link(old, nod);
if (fa) link(nod, fa);
s.emplace(x, old);
s.emplace(oldv, nod);
return old;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
for (int i = 1; i <= n; ++i) ++nod, ps[i].emplace(m + 1, nod), bl[nod] = i;
for (int i = 1; i <= q; ++i) {
int opt, a, b;
cin >> opt;
if (opt == 1) {
cin >> a >> b;
int v1 = get(a, b), v2 = get(a + 1, b);
int f1 = findfa(v1), f2 = findfa(v2);
if (f1) cut(v1, f1);
if (f2) cut(v2, f2);
if (f2) link(v1, f2);
if (f1) link(v2, f1);
} else {
cin >> a;
cout << bl[findrt(ps[a].begin()->second)] << '\n';
}
}
return 0;
}
D. Contests
题目链接
题意
给定 \(m\) 个长度为 \(n\) 的排列。
有 \(q\) 次询问,给定 \(x, y\),询问最小的 \(l\),使得存在序列 \(b\),使 \(b_1 = x, b_{l+1}=y\),满足 \(b_i\) 至少在一个排列中的位置在 \(b_{i+1}\) 之前。
Solution
比赛现场上把这个题转化成了一个图……然后怎么也想不到可以倍增。
我们可以发现,\(b_i\) 和 \(b_{i+1}\) 之间的关系一定是依托在某一个排列上的。
因此我们可以将这个序列的构造过程,看作在一个个排列上跳跃的过程。每跳到一个排列上,就表示我们这一步转移需要使用这个排列。注意这里的跳跃实际上是两个步骤:从当前点 \(v\) 到达所在排列上后面的某一个点 \(u\),然后切换到另一个排列上,为下一次跳跃做准备。
很显然,在一个排列上,越靠前的位置,在后续的跳跃中,具有的选择越多,所以我们可以认为一个排列上越往前的位置越优,在跳跃的过程中,也应该尽量跳到一个排列的最前方。
而我们一旦能够跳跃到某一个排列中,在 \(y\) 之前的点上,那么我们就可以在下一步到达 \(y\)。
因此我们的目标就是,通过在排列上进行若干次跳跃,实现到达某一个排列上位于 \(y\) 之前的位置,最小化跳跃次数。
这样的题目我们根据经验应该想到倍增去维护跳跃一定步数后,能够在某个排列上跳到的最靠前的位置。
要维护这样的信息,我们需要知道我们起始位于的节点编号(我们不需要关心从哪一个排列上开始走)、最后位于的排列的编号(我们需要这个信息来判断能不能到达 \(y\))、跳跃的步数。
所以我们令 \(f[i][j][k]\) 表示跳跃了从任意一个排列上的 \(i\) 出发,最多 \(2^k\) 步,位于第 \(j\) 个排列上的最靠前的位置。
首先是预处理,当 \(k=0\) 时,也就是从 \(i\) 点出发,跳跃一步,能到达第 \(j\) 个排列上的最靠前的位置。
我们枚举我们出发时 \(i\) 所在的排列,那么从 \(i\) 排列上点 \(x\) 出发,走一步能到排列 \(j\) 的方案就应该是 \(i\) 排列上位于 \(x\) 右边的所有点 \(y\) 在 \(j\) 排列上的位置的最小值。
而 \(f[i][j][k]\) 就是从每一个排列上的 \(i\) 出发的答案的最小值。
接下来的转移非常简单,只需要枚举一个中转排列 \(l\),那么 \(f[i][j][k]\) 就是 \(f[i][j][k - 1]\),和从在 \(l\) 上 \(f[i][l][k - 1]\) 位置的点出发走 \(2^{k-1}\) 步到 \(j\) 链的最靠前位置的最小值。形式化一下,设 \(u_l\) 就是 \(f[i][l][k - 1]\) 在 \(l\) 链上对应的点,那么 \(f[i][j][k] = \min_{l} \{f[i][j][k - 1], f[u_l][j][k - 1]\}\)。
最后考虑询问怎么处理。
对于询问 \(x, y\),求出首先判断 \(x\) 能否直接接上 \(y\),如果可以答案就是 \(1\)。然后开始倍增,维护 \(p_i(1\leq i\leq m)\) 表示从 \(x\) 开始,在尚不能到达 \(y\) 的情况下,在第 \(i\) 个排列上能到达的最靠前的位置,记录 \(ans\) 表示此时走的总步数。然后从大到小枚举 \(k\),用类似前面倍增转移的方法去构造 \(g_i\) 为在 \(p\) 的基础上再走最多 \(2^k\) 步,在 \(i\) 排列上的最靠前的位置。判断构造出来的 \(g\) 能否直接到达 \(y\),如果可以就不更新 \(p\),否则用 \(g\) 替换 \(p\) 并将 \(ans\) 加上 \(2^k\)。最后的答案应该是 \(ans + 2\)。(因为 \(ans\) 中计算的是跳跃数不是经过的点数,同时 \(ans\) 求出的是最大的不能跳跃到 \(y\) 前面的步数)
时间复杂度 \(O((n+q)m^2\log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define fec(i, x, y) (int i = head[x], y = g[i].to; i; i = g[i].ne, y = g[i].to)
#define dbg(...) fprintf(stderr, __VA_ARGS__)
#define fi first
#define se second
using ll = long long; using ull = unsigned long long; using pii = pair<int, int>;
template <typename A, typename B> bool smax(A &a, const B &b) { return a < b ? a = b, 1 : 0; }
template <typename A, typename B> bool smin(A &a, const B &b) { return b < a ? a = b, 1 : 0; }
constexpr int N = 1e5 + 7;
constexpr int M = 7;
constexpr int LG = 17;
int n, m;
int a[M][N], r[M][N], f[N][M][LG];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < m; ++i)
for (int j = 1; j <= n; ++j) cin >> a[i][j], r[i][a[i][j]] = j;
for (int i = 1; i <= n; ++i) for (int j = 0; j < m; ++j) f[i][j][0] = n + 1;
for (int i = 0; i < m; ++i)
for (int j = 0; j < m; ++j) {
int p = n + 1;
for (int k = n; k; --k) smin(p, r[j][a[i][k]]), smin(f[a[i][k]][j][0], p);
}
for (int k = 1; k < LG; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 0; j < m; ++j) {
f[i][j][k] = f[i][j][k - 1];
for (int l = 0; l < m; ++l) smin(f[i][j][k], f[a[l][f[i][l][k - 1]]][j][k - 1]);
}
int q;
cin >> q;
while (q--) {
int x, y;
cin >> x >> y;
int p[M], ans = 0, has_ans = 0;
for (int j = 0; j < m; ++j) {
p[j] = r[j][x];
if (r[j][x] <= r[j][y]) { ans = -1, has_ans = 1; goto print; }
}
for (int i = LG - 1; ~i; --i) {
int g[M], flag = 0;
for (int j = 0; j < m; ++j) {
g[j] = p[j];
for (int k = 0; k < m; ++k) smin(g[j], f[a[k][p[k]]][j][i]);
if (g[j] <= r[j][y]) { flag = has_ans = 1; break; }
}
if (!flag) ans += 1 << i, memcpy(p, g, sizeof(p));
}
print:
if (has_ans) cout << ans + 2 << '\n';
else cout << "-1\n";
}
return 0;
}
H. Power of Two
题目链接
题意
给定 \(n\) 个 \(2\) 的整次幂,构造这些数的一个排列 \(d\),使得经过 \(x_0 = 0, x_i = x_{i-1}\ op_i\ d_i\) 后得到的 \(x_n\) 最大,其中 \(op_i\) 是一个 \(x\) 个 &
,\(y\) 个 |
,\(z\) 个 ^
构成的操作排列。
Solution
大讨论题……最后一小时我的队友就在开这个题,然后写到比赛结束也没有调完……
我写的代码的第一稿和最后 AC 的版本已经是天差地别了。
讨论到心态崩溃。
大概就是分成这些讨论的情况
1 如果有 &
,那么我们的目标显然是将前面的若干操作得到的结果用一个 &
清为 \(0\) 以后,使用 |
或者 ^
将每个有效位都变成 \(1\)。
1.1 如果 |
和 ^
的数量不多于有效位数的总数,那么我们只需要选择最大的 \(y + z\) 个有效位,对它们随意使用 |
或者 ^
,在此之前,需要用 &
将剩下的数都连接起来。
1.2 否则此时一定存在某一位的出现次数超过了 \(1\) 次。
1.2.1 如果存在 |
,那么我们就应该将连续的 &|
给这一位使用,剩余的有效数位随意使用 |
和 ^
都可以变成 \(1\),剩余的数和操作随意安排,放在刚刚的 &``|
前面的即可。
1.2.2 如果不存在 |
,那么
1.2.2.1 如果有至少两个 &
且至少两个出线多于一次的数位,那么只需要在这两个位上连续 &
,即可得到全 \(0\) 的状态。剩下的使用每一位一个 ^
都变为 \(1\)。
1.2.2.3 如果只有一个出现多于 \(1\) 次的位,那么设其出现次数为 \(cnt\)。
1.2.2.3.1 如果 \(cnt - 1 - x\) 是偶数,那么 ^
不改变结果,所有有效位都可以是 1。
1.2.2.3.2 如果 \(cnt - 1 - x\) 是奇数。如果将剩余的 ^
全部给这一位会导致这一位变成 0。考虑找出所有位中最小的位,在对之前那一位使用完 \(cnt - x\) 个 ^
后,对这一位使用 &
。
1.2.2.3.3 如果只有一个 &
。那么
1.2.2.3.3.1 如果存在一位的出现次数为偶数(那么除了最后一次以外,出现了奇数次),那么我们只需要在中间对这一位使用一次 &
即可,这一位最终仍为 \(1\)。
1.2.2.3.3.2 如果不存在,即所有位都出现了奇数次。那么找到最小的位,中间的 &
给这一位,最终只有这一位是 \(0\),其余都是 \(1\)。
2 如果没有 &
,显然 |
比 ^
自由很多,所以有限消耗 ^
。考虑所有出现次数为奇数的位,用 ^
连接这些位,如果一个位的所有次出现都被 ^
连接,那么这个位的结果应该是 \(1\)。
2.1 如果 ^
被消耗完,那么在后面随意用 |
连接即可。
2.2 如果 ^
没有被消耗完,那么这时所有的出现次数位奇数的数位我们不用考虑了。假设剩余 cnt 个有效数位,
2.2.1 如果 \(cnt \leq y\)(|
的个数),那么我们只需要在最后放 \(cnt\) 个 |
连接这些数位即可,前面的部分随意连接。
2.2.2 如果 \(cnt > y\),那么我们取最高的 \(y\) 个数位,它们最后应该使用 |
连接,这些位置应该是 1,剩余的有效位置就都是 \(0\) 了。
这个是我的完整的讨论过程。
代码中为了实现方便,可能和描述有所不同。
Code
#include <bits/stdc++.h>
using namespace std;
#define fec(i, x, y) (int i = head[x], y = g[i].to; i; i = g[i].ne, y = g[i].to)
#define dbg(...) fprintf(stderr, __VA_ARGS__)
#define fi first
#define se second
using ll = long long; using ull = unsigned long long; using pii = pair<int, int>;
template <typename A, typename B> bool smax(A &a, const B &b) { return a < b ? a = b, 1 : 0; }
template <typename A, typename B> bool smin(A &a, const B &b) { return b < a ? a = b, 1 : 0; }
int getnum(vector<int> &c) {
int cnt = 0;
for (int x : c) if (x) ++cnt;
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n, x, y, z;
cin >> n >> x >> y >> z;
vector<int> a(n), c(n);
for (int i = 0; i < n; ++i) cin >> a[i], ++c[a[i]];
string ans(n, '0');
string anso;
vector<int> ansn;
if (x) {
stack<int> num;
stack<char> op;
int ce = 0, c2 = 0, mnp = n;
for (int i = 0; i < n; ++i) ce += c[i] > 0, c2 += c[i] > 1, c[i] && smin(mnp, i);
if (ce >= y + z) {
for (int i = n - 1; ~i; --i) if (c[i]) {
if (y) --y, op.push('|');
else if (z) --z, op.push('^');
else break;
--c[i], num.push(i), ans[i] = '1';
}
} else {
assert(c2);
if (y) {
int p = -1;
for (int i = 0; i < n; ++i) if (c[i] > 1) p = i;
--c[p], num.push(p), ans[p] = '1', --y, op.push('|');
for (int i = n - 1; ~i; --i) if (c[i] && i != p) {
if (y) --y, op.push('|');
else if (z) --z, op.push('^');
else assert(0);
--c[i], num.push(i), ans[i] = '1';
}
} else {
for (int i = n - 1; ~i; --i) if (c[i]) {
if (y) --y, op.push('|');
else if (z) --z, op.push('^');
else assert(0);
--c[i], num.push(i), ans[i] = '1';
}
if (x > 1 && c2 > 1) {
int p1 = find_if(c.begin(), c.end(), [](const int &x) { return x > 0; }) - c.begin();
int p2 = find_if(c.begin() + p1 + 1, c.end(), [](const int &x) { return x > 0; }) - c.begin();
--c[p1], num.push(p1), --x, op.push('&');
--c[p2], num.push(p2), --x, op.push('&');
} else if (c2 == 1) {
int p = -1;
for (int i = 0; i < n; ++i) if (c[i] > 0) p = i;
if ((c[p] - x) & 1) {
assert(num.top() == mnp);
num.pop(), op.pop();
++z, ++c[mnp], ans[mnp] = '0';
--c[mnp], num.push(mnp), op.push('&'), --x;
}
} else if (x == 1) {
int p = find_if(c.begin(), c.end(), [](const int &x) { return x & 1; }) - c.begin();
if (p < n) --c[p], num.push(p), --x, op.push('&');
else {
assert(num.top() == mnp);
num.pop(), op.pop();
++z, ++c[mnp], ans[mnp] = '0';
--c[mnp], num.push(mnp), op.push('&'), --x;
}
}
}
}
for (int i = n - 1; ~i; --i) while (c[i]) {
if (x) --x, op.push('&');
else if (y) --y, op.push('|');
else if (z) --z, op.push('^');
else assert(0);
--c[i];
num.push(i);
}
while (!op.empty()) anso.push_back(op.top()), op.pop();
while (!num.empty()) ansn.push_back(num.top()), num.pop();
} else {
bool odd_enough = 0;
for (int i = n - 1; ~i; --i) if (c[i] & 1) while (c[i]) {
if (z) --z, anso.push_back('^');
else goto odd_skip;
ans[i] = '1', ansn.push_back(i);
--c[i];
}
odd_enough = 1;
odd_skip:
if (!odd_enough) {
for (int i = n - 1; ~i; --i) while (c[i]) {
if (y) --y, anso.push_back('|');
else assert(0);
ans[i] = '1', ansn.push_back(i);
--c[i];
}
} else {
stack<int> num;
stack<char> op;
for (int i = n - 1; ~i; --i) if (c[i]) {
if (y) --y, op.push('|');
else break;
--c[i], num.push(i), ans[i] = '1';
}
for (int i = n - 1; ~i; --i) while (c[i]) {
if (y) --y, op.push('|');
else if (z) --z, op.push('^');
--c[i], num.push(i);
}
while (!op.empty()) anso.push_back(op.top()), op.pop();
while (!num.empty()) ansn.push_back(num.top()), num.pop();
}
}
end:
reverse(ans.begin(), ans.end());
cout << ans << '\n';
for (int i = 0; i < n; ++i) cout << anso[i];
cout << "\n";
for (int i = 0; i < n; ++i) cout << ansn[i] << " \n"[i == n - 1];
}
return 0;
}
K. Streets
题目链接
题意
给定 \(n\) 条垂直的直线,每条直线坐标 \(x_i\),权值 \(a_i\)。
给定 \(m\) 条水平的直线,每条直线坐标 \(y_i\),权值 \(b_i\)。
显然任意两条水平直线和两条垂直直线可以构成一个矩形,我们定义一个矩形的花费为矩形四个边在各自所在直线上的长度乘直线的权值的和。
有 \(T\) 次询问,每次询问一个 \(c\),求出花费不超过 \(c\) 的矩形的面积最大值。
Solution
首先对于垂直线 \(i, j\) 和水平线 \(l, r\),他们构成的矩形的花费及应该满足的条件是
\[(x_j - x_i)(b_l + b_r) + (a_i + a_j)(y_r - y_l) \leq c \]因为直线权值无关面积,因而如果直线的坐标之差固定,只有权值之和最小的两条直线的组合有意义。考虑到直线的坐标比较小,因此有意义的直线组合也比较少,只有 \(10^5\) 级别。所以我们通过直线组合的方向考虑会更容易。我们假设我们这样的直线组合的数量为 \(V\)。
我们可以维护坐标差为 \(i\) 的垂直直线的最小权值和 \(a_i\),以及坐标差为 \(l\) 的水平直线的最小权值和 \(b_l\)。(注意这里的 \(a, b\) 数组的定义和题目中不一样)
那么上面的式子可以化作
\[i\cdot b_l + a_il \leq c \]因为询问次数很少,我们考虑对于每次询问枚举垂直直线组 \(i\),那么满足要求的水平直线组一定有
\[b_l \leq \frac{c - a_il}{i} = \frac ci - \frac{a_i}il \]可以发现,如果我们把一条水平线 \((l, b_l)\) 看成二维平面中的一个点,那么满足限制的水平线坐标就位于一条直线的下方。因为我们需要最大化的面积等于 \(il\),所以我们只需要这条水平线下面的最大的 \(l\),即我们的任务变成了,求出二维平面中,位于一条直线的下面的点的横坐标最大值。
一个比较容易想到的思路是,考虑二分一个横坐标 \(p\),那么只需要判断直线下面是否有直线 \(x = p\) 右边的点。容易想到这种条件等价于横坐标大于等于 \(p\) 的点的构成的凸包和直线是否有交点。
快速求出横坐标大于等于 \(p\) 的点构成的凸包并不容易,可以使用线段树将我们需要的区间分割,每个线段树区间维护区间内的点构成的凸包,直线一次判断和区间的凸包是否相交也是可以的。
实际上因为直线截距为正斜率为负,只需要维护一个下凸壳就可以了,判断下凸壳和直线相交可以在凸壳上二分,因为下凸壳的斜率一定单调增,二分求出下凸壳的斜率和直线的斜率最接近的边即可,判断两条边的公共端是否在直线下方,即可求出直线和凸包是否存在交点。
具体的,二分中,如果经过 \((l, b_l)\) 和 \(l', b_{l'}(l > l')\) 直线的斜率小于等于 \(-\frac{a_i}i\),那么就有
\[\frac{b_l - b_{l'}}{l - l'} \leq -\frac{a_i}i \]\[i(b_l - b_{l'}) + a_i(l - l') \leq 0 \]\[i\cdot b_l +a_il \leq i\cdot b_{l'} + a_il' \]也就是,这个条件可以等价于,\(i\) 和 \(l\) 产生的花费小于 \(i\) 和 \(l'\) 产生的花费,这样的比较我们就可以规避浮点运算了。
这样的算法的复杂度为 \(O(TV\log n\log^2V)\)。其中两次二分各贡献一个 \(\log\),线段树贡献一个 \(\log\)。
这样的复杂度显然是无法通过的。这个算法有很多种优化方法,我觉得比较好懂的优化是这样的。
首先我们需要最大化的答案是 \(il\),因此如果我们倒序枚举 \(i\),那么 \(l\) 只有变大,才有更新答案的可能。因为我们知道 \(l\) 对于花费的限制是有单调性的,如果 \(l\) 不满足限制,那么 \(l\) 右边的所有点都不可能满足限制。
因此我们倒序枚举 \(i\) 的过程中,维护 \(p\) 表示我们本来应该在二分中求出的横坐标 \(l\)。每次 \(i\) 变化时,如果我们发现现在的 \(p\) 不满足限制,我们就跳过 \(i\),因为我们将 \(p\) 变小不可能对答案产生更新。如果发现现在的 \(p\) 满足限制了,我们可以逐渐增大 \(p\),直到不满足限制位置,在 \(p\) 变化的期间,记录好对答案可能产生的更新。这样,我们就可以避免一次二分了。
随后我们可以发现,如果我们按照 \(l\) 从右向左倒序求出凸包,在求出凸包的过程中,对于新加入的 \(l\),我们会在单调栈中弹出若干点,然后压入一个点,此时的凸包就是横坐标在 \(l\) 右边的点构成的凸包。如果我们记录下来的被我们弹出的点,那么这个过程是可以恢复的。在求完凸包后,再从右向左扫过去,只需要把单调栈的栈顶弹出,把这一个 \(l\) 出弹出的点再压入,就能还原出在 \(l\) 右边的点构成的凸包,而且总计的操作次数和建凸包是相通的。
把这个方法和上面的 \(p\) 逐渐增加的思路结合,我们就不必使用线段树来维护凸包了。随着 \(p\) 的增大,我们也只需要把 \(p\) 所在的位置的凸包还原出来。
我们这样处理的复杂度就降低到了 \(O(Tn\log V)\)。可以通过本题。
Code
#include <bits/stdc++.h>
using namespace std;
#define fec(i, x, y) (int i = head[x], y = g[i].to; i; i = g[i].ne, y = g[i].to)
#define dbg(...) fprintf(stderr, __VA_ARGS__)
#define fi first
#define se second
using ll = long long; using ull = unsigned long long; using pii = pair<int, int>;
template <typename A, typename B> bool smax(A &a, const B &b) { return a < b ? a = b, 1 : 0; }
template <typename A, typename B> bool smin(A &a, const B &b) { return b < a ? a = b, 1 : 0; }
constexpr int N = 5000 + 7;
constexpr int M = 1e5 + 7;
constexpr int INF = INT_MAX;
int n, m, T, na, nb, tp;
pii a[M], b[M], h[M];
vector<pii> del[M];
void init(int n, pii *a, int &na) {
static int x[N], v[N], c[M];
int mxx = 0;
for (int i = 1; i <= n; ++i) cin >> x[i], smax(mxx, x[i]);
for (int i = 1; i <= n; ++i) cin >> v[i];
for (int i = 0; i <= mxx; ++i) c[i] = INF;
for (int i = 1; i <= n; ++i)
for (int j = i; j <= n; ++j) smin(c[x[j] - x[i]], v[i] + v[j]);
for (int i = 0; i <= mxx; ++i) if (c[i] < INF) a[++na] = pii(i, c[i]);//, cerr << i << ' ' << c[i] << endl;
}
pii operator - (pii a, pii b) { return pii(a.fi - b.fi, a.se - b.se); }
ll cross(pii a, pii b) { return (ll)a.fi * b.se - (ll)a.se * b.fi; }
void getHell() {
for (int i = nb; i; --i) {
while (tp > 1 && cross(b[i] - h[tp], h[tp] - h[tp - 1]) <= 0) del[i].push_back(h[tp--]);
h[++tp] = b[i];
}
}
ll calc(pii a, pii b) { return (ll)a.fi * b.se + (ll)a.se * b.fi; }
bool check(vector<pii> &h, pii cur, ll c) {
int l = 1, r = h.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (calc(cur, h[mid]) <= calc(cur, h[mid + 1])) r = mid;
else l = mid + 1;
}
return calc(cur, h[l]) <= c;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> m >> T;
init(n, a, na), init(m, b, nb);
getHell();
while (T--) {
ll ans = 0, c;
cin >> c;
vector<pii> h2(h, h + tp + 1);
for (int i = na, p = 1; i; --i) {
while (p <= nb && check(h2, a[i], c)) {
smax(ans, (ll)a[i].fi * b[p].fi);
assert(h2.back() == b[p]);
h2.pop_back();
copy(del[p].rbegin(), del[p].rend(), back_inserter(h2));
++p;
}
}
cout << ans << '\n';
}
return 0;
}
标签:Contest,int,ICPC,--,fa,num,2022,push,op
From: https://www.cnblogs.com/hankeke303/p/18154715/icpc2022xian