杂题选做
\(CF1839E\) Decreasing Game
考虑两个数的情况。显然,若两数不等,先手胜;否则,后手胜。
不妨直接猜结论 : 如果能找出一个集合,使得集合中元素的和恰为总和的一半, 则后手胜; 否则先手胜。
充分性很显然,每次先手选择一个数,后手只要在另一个集合中也选一个数即可。 这样两个集合减少的值相同;
需要证明的是如果找不出这样一个集合,那么是否经过若干次操作后,仍然找不出。
若一次操作两人分别选择了 \(x\) 和 \(y\) 且 \(x \leq y\), 假设经过这次操作后可以找出一个集合,使得其元素和为总和一半,其中 \(y - x\) 所在集合的其他数总和为 \(s1\),
另一个集合总和为 \(s2\),即 \((y - x) + s1 = s2\), 移项后发现 \(y + s1 = x + s2\), 矛盾。
因此结论是成立的。
用动态规划求解一个背包问题即可。
时间复杂度 \(O(N ^ 3)\)
\(CF1748E\)
首先发现,一个序列合法当且仅当大小关系和原序列一致。
建出笛卡尔树后解一个简单的动态规划 , 即可。
时间复杂度 \(O(NM)\)
\(CF1858E\)
不妨考虑如果不支持回退怎么做。
考虑这些 \(+\) 和 \(-\) 的操作其实可以抽象为一个树形结构。 对于每个点而言,根到其路径上的数可以还原原序列是什么样子。
假设上次操作结束后所在的点为 \(u\), 如果添加一个数, 那么新建一个点 \(v\) 作为 \(u\) 的儿子即可。如果将序列长度减少 \(k\), 那么相当于往父亲方向跳了 \(k\) 次。
所以可以通过倍增实现求 \(k\) 级祖先。
而如果有回退怎么办呢?直接维护一个栈,栈里存储每次操作后当前点的位置。 每次回退弹出栈顶即可。
现在唯一的问题是如何求答案?
最简单的方式是将所有询问离线, 用一次 \(DFS\) 可以很方便求出每个点到根路径上不同的数的个数。
这样做可以轻松通过 \(E1\), 而无法通过 \(E2\), 因为 \(E2\) 强制在线。
不难发现这个祖先-后代的关系其实可以用可持久化线段树维护。 对于每个添加操作,只需要在线段树上查一查,看看能不能产生贡献即可。
这样做时间复杂度是 \(O(NlogN)\) 的, 足够通过本题。
然而我们有更优秀的线性做法 :
考虑一个数对答案有贡献,当且仅当它首次出现。
维护一个长度为 \(n\) 的数组 \(a\), 和一个长度为 \(N\) 的数组 \(A\) 其中 \(n \leq N\),并且 \(\forall i \leq n, a_{i} = A_{i}\)。另外记 \(occ_{i}\) 表示 \(i\) 这个数第一次
出现是在什么位置。我们只需要保证对于每个 \(a_{i}\),\(occ_{a_{i}}\) 的值是正确的。
如果新添加一个数 \(x\),如果 \(occ_{x} > n\) 或者 \(a_{occ_{x}} \neq x\),那么可以更新 \(occ_{x}\) 的值,并将答案加一。
如果将序列长度减少 \(k\), 那么直接将 \(n\) 减去 \(k\) 。
对于回退操作, 我们像刚才描述的一样, 用堆栈记录更新操作。 每次取栈顶即可。
而对于求答案, 维护一个数组 \(ans\), 要求的就是 \(ans_{n}\)
时间复杂度 \(O(N)\)
放一下代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MN = 2e6 + 5;
struct node {
int *x, val;
node(int *x, int val) : x(x), val(val) {}
} ;
int foo[MN], ans[MN], a[MN], tim;
char c[2];
stack < vector < node > > s;
int main() {
int q; scanf("%d", &q);
while (q--) {
scanf("%s", c);
if (c[0] == '+') {
vector < node > cng;
int x; scanf("%d", &x);
if (foo[x] > tim || a[foo[x]] != x) {
cng.emplace_back(node(&foo[x], foo[x]));
foo[x] = tim + 1;
cng.emplace_back(node(&ans[tim + 1], ans[tim + 1]));
ans[tim + 1] = ans[tim] + 1;
} else {
cng.emplace_back(node(&ans[tim + 1], ans[tim + 1]));
ans[tim + 1] = ans[tim];
}
cng.emplace_back(node(&a[tim + 1], a[tim + 1]));
a[tim + 1] = x;
cng.emplace_back(node(&tim, tim));
++tim;
s.push(cng);
} else if (c[0] == '-') {
vector < node > cng;
int x; scanf("%d", &x);
cng.emplace_back(node(&tim, tim));
tim -= x;
s.push(cng);
} else if (c[0] == '!') {
vector < node > v = s.top(); s.pop();
for (node e : v)
*e.x = e.val;
} else {
printf("%d\n", ans[tim]);
fflush(stdout);
}
}
return 0;
}
\(CF1856D\)
考虑分治。
对于一个区间 \([l, r]\),将其一分为二,求出两部分的最值,并记它们出现的位置为 \(a\) 和 \(b\)。
如果 \(a < b\) , 那么不难发现 \(b\) 是整个区间最大的数, 且 \([a, b]\) 逆序对数量和 \([a, b - 1]\) 逆序对数量相同。
而如果 \(a > b\),那么 \([a, b]\) 逆序对数量至少比 \([a, b - 1]\) 逆序对数量大 \(1\)。
于是在知道了 \(a\) 和 \(b\) 的值后, 只要通过两次询问就可以知道 \([l, r]\) 最值的出现位置。
记 \(f(n)\) 表示 长度为 \(n\) 的区间,总共要花多少次询问。
\(f(n) = 2f(\frac{n}{2}) + 2(r - l)^2\)
可以通过归纳证明 \(f(n) \leq 4n^2\)
时间复杂度 \(O(NlogN)\)
\(CF1856E\)
首先发现每个点是独立的。
故总和最大值就是最大值的和。
考虑每个点的子树,不难发现其要么全部比根小,要么全部比其大。 否则一定不是最优的 (很容易证明)。
一个简单做法是,求出所有子树的大小,然后用 \(bitset\) 优化背包问题。
最后要求的是一个类似于 \(X(S-X)\) 的东西。
然而这样做的时间复杂度是 \(O(\frac{N^3}{w})\) 的,只能通过 \(E1\)
如何通过 \(E2\) 呢?
考虑到不同的子树大小至多 \(\sqrt{n}\) 种,可以求多重背包
这样时间复杂度是 \(O(\frac{n\sqrt{n}logn}{w})\) 的。
注意要特判重儿子大小超过一半的情况,否则复杂度会退化。
\(CF1860D\)
对于一个合法的序列,其最小交换次数为与原序列不同的位置个数的一半。
根据这个性质动态规划。 具体实现时用滚动数组优化一下空间。
时间复杂度 \(O(N^4)\)
\(CF1854B\)
考虑一个 \(O(N ^ 2)\) 的动态规划解法。
记 \(dp_{i, j}\) 表示前 \(i\) 个数,是否能到达 \(j\)
不难发现这个 \(DP\) 可以直接用 \(bitset\) 优化。
时间复杂度 \(O(\frac{N ^ 2}{w})\)
代码很简单
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MN = 2e5 + 5;
int N, A[MN];
LL S[MN];
bitset < MN > dp, new_dp, AND;
int main() {
scanf("%d", &N);
LL TOT = 0;
for (int i = 1; i <= N; ++i) {
scanf("%d", &A[i]);
S[i] = S[i - 1] + (LL) A[i];
}
for (int i = 1; i <= N - 1; ++i) AND.set(i);
dp.set(1);
for (int i = 1; i <= N; ++i) {
new_dp = dp | ((dp & AND) << A[i]);
dp = new_dp;
AND.reset(i);
}
LL ANS = 0;
for (int i = 2 * N; i >= N; --i)
if (dp[i]) ANS = S[N] - i + 1;
for (int i = 1; i <= N; ++i)
if (dp[i]) ANS = max(ANS, S[i] - i + 1);
printf("%lld\n", ANS);
return 0;
}
\(CF702F\)
先考虑一个简单做法。
将物品以 \(P\) 为第一关健字, \(C\) 为第二关键字排序。
那么对于每个客户,只需要从前往后扫描每个物品,如果能购买则购买。
这样做的时间复杂度是 \(O(N^2)\) 级别的。 不能通过本题。
因为没有强制在线,不妨换个思路,考虑每个物品对询问的影响。
考虑对于一个物品,我们可以找出每个超过 \(C\) 的询问,将它们减去 \(C\), 并给对应的答案 \(+1\)。
直接这样做仍然是 \(O(N ^ 2)\) 的。 然而与第一种做法不同,我们可以用数据结构加速这个过程。
不妨维护一个非旋 \(treap\),将每个询问的值都加进去。
每次只需通过分裂操作,分裂出 $ < c$ 和 $ \geq c$ 的两棵树。对后者的根打上标记。 然后再把两棵树合并起来。
求答案只需要深度优先遍历 \(treap\) 进行标记下传即可。
然而这样做会出现一个问题,两棵非旋 \(treap\) 合并的前提条件是其中一棵树的任意权值小于或大于另一棵树的所有权值。
解决这个问题有一个很巧妙的方法 :
不妨考虑分裂出三棵树, 值域分别为 \([0, c), [c, 2c), [2c, +\infty]\)。
第一棵树显然什么都不用做。
而对于第二棵树, 不妨直接将它们全部减去 \(c\), 然后暴力插入第一棵树中。
然后,给第三棵树打上标记,将它和第一棵树合并。
为什么这样做时间复杂度是正确的?
可以观察到: 若一个数被暴力插入到一棵树中,那么这个数的值至少减少了一半。 所以每个数最多被暴力插入 \(O(logV)\) 次。
故总时间复杂度 \(O(NlogN)\)
代码如下
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int MX = 1e9;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
inline int rnd() { return rng() % MX; }
const int MN = 3e5 + 5;
int root, K, sz, C[MN], A[MN], B[MN], N, ord[MN], ans[MN];
inline bool cmp(int x, int y) {
return B[x] == B[y] ? A[x] < A[y] : B[x] > B[y];
}
struct fhq_node {
int rnk;
int VAL, ANS;
int tagA, tagV;
int lc, rc;
int home;
} treap[MN];
inline int new_node(int val, int home) {
int u = ++sz;
treap[u].rnk = rand();
treap[u].VAL = val;
treap[u].tagA = treap[u].tagV = 0;
treap[u].lc = treap[u].rc = 0;
treap[u].home = home;
return u;
}
inline void pushdown(int now) {
int lc = treap[now].lc, rc = treap[now].rc,
vA = treap[now].tagA, vV = treap[now].tagV;
if (vA != 0) {
treap[lc].ANS += vA; treap[rc].ANS += vA;
treap[lc].tagA += vA; treap[rc].tagA += vA;
}
if (vV != 0) {
treap[lc].VAL += vV; treap[rc].VAL += vV;
treap[lc].tagV += vV; treap[rc].tagV += vV;
}
treap[now].tagA = 0;
treap[now].tagV = 0;
return;
}
inline void fhq_split(int now, int val, int &x, int &y) {
if (!now)
x = y = 0;
else {
pushdown(now);
if (val < treap[now].VAL) {
y = now;
fhq_split(treap[now].lc, val, x, treap[y].lc);
} else {
x = now;
fhq_split(treap[now].rc, val, treap[x].rc, y);
}
}
}
inline int fhq_merge(int p, int q) {
if (!p || !q) return p + q;
pushdown(p), pushdown(q);
if (treap[p].rnk < treap[q].rnk) {
treap[p].rc = fhq_merge(treap[p].rc, q);
return p;
} else {
treap[q].lc = fhq_merge(p, treap[q].lc);
return q;
}
}
inline void fhq_insert(int u, int &root) {
int x, y;
fhq_split(root, treap[u].VAL, x, y);
root = fhq_merge(fhq_merge(x, u), y);
return;
}
inline void brute_insert(int v, int &u) {
if (!v) return;
pushdown(v);
brute_insert(treap[v].lc, u); brute_insert(treap[v].rc, u);
treap[v].lc = treap[v].rc = 0;
fhq_insert(v, u);
return;
}
inline void traverse(int u) {
if (!u) return;
pushdown(u);
traverse(treap[u].lc); traverse(treap[u].rc);
ans[treap[u].home] = treap[u].ANS;
return;
}
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d%d", &A[i], &B[i]);
for (int i = 1; i <= N; ++i) ord[i] = i;
sort(ord + 1, ord + 1 + N, cmp);
scanf("%d", &K);
for (int i = 1; i <= K; ++i) {
scanf("%d", &C[i]);
int u = new_node(C[i], i);
fhq_insert(u, root);
}
for (int i = 1, x, y, z; i <= N; ++i) {
fhq_split(root, A[ord[i]] - 1, x, y);
fhq_split(y, 2 * A[ord[i]] - 1, y, z);
if (y) {
treap[y].ANS += 1;
treap[y].VAL -= A[ord[i]];
treap[y].tagA += 1;
treap[y].tagV -= A[ord[i]];
}
if (z) {
treap[z].ANS += 1;
treap[z].VAL -= A[ord[i]];
treap[z].tagA += 1;
treap[z].tagV -= A[ord[i]];
}
brute_insert(y, x);
root = fhq_merge(x, z);
}
traverse(root);
for (int i = 1; i <= K; ++i)
printf("%d ", ans[i]);
printf("\n");
return 0;
}
\(CF1863F\)
考虑 \([l, r]\) 这个区间的异或和 \(k\)。
如果 \(k = 0\), 那么可以对这个区间进行任意切割。
如果 \(k \neq 0\), 考虑 \(k\) 的最高位 \(b\)。切割 \([l, r]\) 这个区间会使两个子序列一个异或和第 \(b\) 为为 \(0\), 另一个为 \(1\)。 \(1\) 的那一半显然会被保留下来。
考虑按照区间长度从大到小进行动态规划, 直接令 \(f_{l, r}\) 表示是否能切割出 \([l, r]\) 这个区间。 此外记录 \(lmask_{i}\) 和 \(rmask_{i}\) 表示 \(i\) 开始/结束的区间, 异或和哪些位为 \(1\) 是合法的。
当处理到 \([l, r]\) 这个区间时, 如果 \(k\) (异或和)和 \(lmask_{l}\) 或者 \(rmask_{r}\) 的 \(AND\) 值为正数, 那么说明这个区间是可行的,\(f_{l, r} = 1\), 否则 \(f_{l, r} = 0\),随后我们更新 \(lmask\) 和 \(rmask\) 的值,细节见代码。
时间复杂度 \(O(N^2)\)
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int MX = 1e9;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
inline int rnd() { return rng() % MX; }
const int MN = 100005;
int N, ans[MN], P[MN], Q[MN];
LL A[MN], L[MN], R[MN], pre[MN];
inline int HBIT(LL s) {
return 63 - __builtin_clzll(s);
}
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%lld", &A[i]);
for (int i = 1; i <= N; ++i) pre[i] = pre[i - 1] ^ A[i];
for (int i = 1; i <= N; ++i) P[i] = Q[i] = L[i] = R[i] = 0;
for (int len = N; len >= 1; --len) {
for (int i = 1, j = len; j <= N; ++i, ++j) {
LL s = pre[j] ^ pre[i - 1];
int dp_val = 0;
if (j - i >= N - 1 || (L[i] & s) || (R[j] & s) || P[i] || Q[j])
dp_val = 1;
if (i == j) ans[i] = dp_val;
if (dp_val) {
if (!s) {
P[i] = 1;
Q[j] = 1;
} else {
L[i] |= 1LL << HBIT(s);
R[j] |= 1LL << HBIT(s);
}
}
}
}
for (int i = 1; i <= N; ++i)
printf("%d", ans[i]);
printf("\n");
}
return 0;
}