比赛链接
官方题解
Problem A. Unusual Competitions
显然,当且仅当左右括号的个数不相等,答案为 − 1 -1 −1 。否则,将左右括号分别看做 + 1 , − 1 +1,-1 +1,−1 ,画出前缀和的折线图,不难发现翻转 x x x 轴下方的部分是最优的。
时间复杂度 O ( N ) O(N) O(N) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
char s[MAXN];
int main() {
int n; read(n);
scanf("%s", s + 1);
int cur = 0, ans = 0, last = 0;
for (int i = 1; i <= n; i++) {
if (s[i] == '(') {
cur += 1;
if (cur == 0) ans += i - last;
} else {
cur -= 1;
if (cur == -1) last = i - 1;
}
}
if (cur != 0) puts("-1");
else cout << ans << endl;
return 0;
}
Problem B. Present
考虑分别求出答案的每一位。
若我们希望求出答案在
2
i
2^i
2i 处的值,可以将所有数对
2
i
+
1
2^{i+1}
2i+1 取模,并排序。
此时,两个数
x
,
y
x,y
x,y 的和对答案产生贡献,当且仅当
x
+
y
∈
[
2
i
,
2
i
+
1
)
∪
[
2
i
+
1
+
2
i
,
2
i
+
2
)
x+y\in[2^i,2^{i+1})\cup[2^{i+1}+2^i,2^{i+2})
x+y∈[2i,2i+1)∪[2i+1+2i,2i+2) 。
用前缀和或双指针均可解决。
时间复杂度 O ( V + N L o g V ) O(V+NLogV) O(V+NLogV) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 4e5 + 5;
const int MAXV = 1 << 25;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n, a[MAXN];
bool getans(int p) {
static int sum[MAXV];
int bit = (1 << (p + 1)) - 1, goal = 1 << p;
for (int i = 0; i <= bit; i++)
sum[i] = 0;
for (int i = 1; i <= n; i++)
sum[a[i] & bit]++;
for (int i = 1; i <= bit; i++)
sum[i] += sum[i - 1];
bool ans = false; int last = 0;
for (int i = 0; i <= bit; i++) {
int cnt = sum[i] - last;
if (cnt != 0) {
if (((i + i) & bit) >= goal) ans ^= 1ll * cnt * (cnt - 1) / 2 % 2 == 1;
int l = (goal - i + bit + 1) & bit, r = bit - i, tmp = 0;
if (cnt & 1) {
if (l <= r) {
if (r > i) tmp = sum[r] - sum[max(l - 1, i)];
} else {
tmp = sum[bit] - sum[max(l - 1, i)];
if (r > i) tmp += sum[r] - sum[i];
}
}
if (tmp & 1) ans ^= true;
}
last = sum[i];
}
return ans;
}
int main() {
read(n);
for (int i = 1; i <= n; i++)
read(a[i]);
int ans = 0;
for (int p = 0; p <= 24; p++) {
bool flg = getans(p);
if (flg) ans ^= 1 << p;
}
cout << ans << endl;
return 0;
}
Problem C. Instant Noodles
删去右侧的孤点,它们显然对答案没有影响。
为左侧的每一个点随机一个
64
64
64 位无符号整型的权值
v
i
v_i
vi 。
令右侧的每一个点的权值
b
i
b_i
bi 为左侧所有与其相邻的点
v
i
v_i
vi 的异或和。
那么,可以认为,当且仅当右侧两个点的 b i b_i bi 相同,它们始终会在 N ( S ) N(S) N(S) 中一起出现,或一起不出现。因此,这样的两个点可以被缩成一个点来看待。
可以证明,答案即为缩点后各个点 c i c_i ci 的 GCD 。
单组数据时间复杂度 O ( M + N L o g N ) O(M+NLogN) O(M+NLogN) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
typedef long long ll;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
ll a[MAXN]; ull b[MAXN], v[MAXN];
map <ull, ll> mp; int n, m;
ull rnd() {
ull ans = 0;
for (int i = 0; i <= 7; i++) {
ans <<= 8;
ans ^= rand() & 255;
}
return ans;
}
int main() {
srand('X' + 'Y' + 'X');
int T; read(T);
while (T--) {
read(n), read(m);
for (int i = 1; i <= n; i++) {
read(a[i]), b[i] = 0;
v[i] = rnd();
}
for (int i = 1; i <= m; i++) {
int x, y; read(x), read(y);
b[y] ^= v[x];
}
mp.clear();
for (int i = 1; i <= n; i++)
if (b[i] != 0) mp[b[i]] += a[i];
ll ans = 0;
for (auto x : mp)
ans = __gcd(ans, x.second);
printf("%lld\n", ans);
}
return 0;
}
Problem D. Reality Show
考虑将序列倒置,我们希望找出一个不降的子序列,同时最大化其关于二进制下进位的权值。
考虑一个朴素的动态规划,记
d
p
i
,
j
,
k
dp_{i,j,k}
dpi,j,k 表示考虑了序列的前
i
i
i 位,当前的二进制数为
k
×
2
j
k\times 2^j
k×2j 。
转移时考虑放弃当前的元素,选择当前的元素,或进位,不难得出
O
(
1
)
O(1)
O(1) 的转移。
观察上述 DP ,可以发现,从 d p i , j , k dp_{i,j,k} dpi,j,k 到 d p i + 1 , j , k dp_{i+1,j,k} dpi+1,j,k ,产生变动的位置只有 O ( N + M ) O(N+M) O(N+M) 个,因此可以去掉第一维,仅修改这些位置,滚动数组的同时优化时间复杂度。
时间复杂度 O ( N L o g N × ( N + M ) ) O(NLogN\times (N+M)) O(NLogN×(N+M)) ,可优化至 O ( N ( N + M ) ) O(N(N+M)) O(N(N+M)) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 4005;
const int INF = 1e9 + 7;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n, m, l[MAXN], s[MAXN], c[MAXN], dp[MAXN][MAXN];
int main() {
read(n), read(m);
for (int i = 1; i <= n; i++)
read(l[i]);
for (int i = 1; i <= n; i++)
read(s[i]);
reverse(l + 1, l + n + 1);
reverse(s + 1, s + n + 1);
for (int i = 1; i <= n + m; i++)
read(c[i]);
for (int i = 1; i <= n + m; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = -INF;
for (int i = 1; i <= n; i++) {
int cur = l[i];
for (int j = n; j >= 0; j--) {
if (dp[cur][j] == -INF) continue;
int tmp = dp[cur][j] - s[i] + c[cur];
for (int k = cur + 1, t = j; t & 1; k++, t >>= 1)
tmp += c[k];
for (int k = cur, t = j + 1; t != 0; k++, t >>= 1)
chkmax(dp[k][t], tmp);
}
for (int j = 1; j <= n + m; j++) {
chkmax(dp[j][0], dp[j - 1][0]);
chkmax(dp[j][0], dp[j - 1][1]);
}
}
int ans = -INF;
for (int i = 1; i <= n + m; i++)
for (int j = 0; j <= n; j++)
chkmax(ans, dp[i][j]);
cout << ans << endl;
return 0;
}
Problem E. Median Mountain Range
考虑权值范围在
{
0
,
1
}
\{0,1\}
{0,1} 内的问题。
结构
0
,
0
0,0
0,0 和结构
1
,
1
1,1
1,1 是稳定的,因此答案应当为最长的
0
,
1
0,1
0,1 交替的段除以
2
2
2 。
那么,原问题中,考虑选取中间值 M i d Mid Mid ,令 ≥ M i d \geq Mid ≥Mid 的元素为 1 1 1 , < M i d <Mid <Mid 的元素为 0 0 0 ,操作进行的轮数即为所有中间值中,使得范围在 { 0 , 1 } \{0,1\} {0,1} 内的问题操作轮数最多的轮数。
由此,我们需要一个结构,支持将一个元素从
0
0
0 修改为
1
1
1 ,同时维护当前最长的
0
,
1
0,1
0,1 交替的段的长度,并且求出修改后,最终状态新增了那些
1
1
1 。
以下代码通过维护稳定位置的方式实现了这一算法。
时间复杂度 O ( N L o g N ) O(NLogN) O(NLogN) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int ans[MAXN];
struct SegmentTree {
struct Node {
int lc, rc;
int sum;
} a[MAXN * 2];
int n, root, size;
void update(int root) {
a[root].sum = a[a[root].lc].sum + a[a[root].rc].sum;
}
void build(int &root, int l, int r) {
root = ++size;
if (l == r) {
a[root].sum = 1;
return;
}
int mid = (l + r) / 2;
build(a[root].lc, l, mid);
build(a[root].rc, mid + 1, r);
update(root);
}
void init(int x) {
n = x;
build(root, 1, n);
}
void modify(int root, int l, int r, int ql, int qr, int v) {
if (l == ql && r == qr) {
if (a[root].sum == 0) return;
if (l == r) {
a[root].sum = 0;
ans[l] = v;
return;
}
int mid = (l + r) / 2;
modify(a[root].lc, l, mid, l, mid, v);
modify(a[root].rc, mid + 1, r, mid + 1, r, v);
update(root);
return;
}
int mid = (l + r) / 2;
if (mid >= ql) modify(a[root].lc, l, mid, ql, min(mid, qr), v);
if (mid + 1 <= qr) modify(a[root].rc, mid + 1, r, max(mid + 1, ql), qr, v);
update(root);
}
void modify(int l, int r, int v) {
modify(root, 1, n, l, r, v);
}
} ST;
set <int> Break;
int n, col[MAXN];
struct DataStructure {
int cnt[MAXN];
set <int> Max;
void inc(int x) {
cnt[x]++;
if (cnt[x] == 1) Max.insert(x);
}
void dec(int x) {
cnt[x]--;
if (cnt[x] == 0) Max.erase(x);
}
int query() {
auto tmp = Max.end(); tmp--;
return (*tmp);
}
} D;
void inc(int pos, int val) {
if (pos == 0 || pos == n) {
col[pos] += 2;
if (pos == 0) {
auto tmp = Break.lower_bound(pos);
auto suf = tmp; suf++;
if (col[*suf] == 2) ST.modify((*tmp) + 1, *suf, val);
else ST.modify((*tmp) + 1, ((*tmp) + (*suf)) / 2, val);
} else {
auto tmp = Break.lower_bound(pos);
auto pre = tmp; pre--;
if (col[*pre] == 2) ST.modify((*pre) + 1, *tmp, val);
else ST.modify(((*pre) + (*tmp)) / 2 + 1, *tmp, val);
}
} else if (col[pos] == 0) {
col[pos] += 1;
auto tmp = Break.lower_bound(pos);
auto pre = tmp, suf = tmp; pre--, suf++;
D.dec((*tmp) - (*pre)), D.dec((*suf) - (*tmp)), D.inc((*suf) - (*pre));
if (col[*pre] == 2 && col[*suf] == 2) ST.modify((*pre) + 1, *suf, val);
else if (col[*pre] == 2) ST.modify((*pre) + 1, ((*pre) + (*suf)) / 2, val);
else if (col[*suf] == 2) ST.modify(((*pre) + (*suf)) / 2 + 1, *suf, val);
Break.erase(tmp);
} else {
col[pos] += 1;
auto tmp = Break.insert(pos).first;
auto pre = tmp, suf = tmp; pre--, suf++;
D.inc((*tmp) - (*pre)), D.inc((*suf) - (*tmp)), D.dec((*suf) - (*pre));
if (col[*suf] == 2) ST.modify((*tmp) + 1, *suf, val);
else ST.modify((*tmp) + 1, ((*tmp) + (*suf)) / 2, val);
if (col[*pre] == 2) ST.modify((*pre) + 1, *tmp, val);
else ST.modify(((*pre) + (*tmp)) / 2 + 1, *tmp, val);
}
}
int main() {
read(n);
static int p[MAXN], a[MAXN];
for (int i = 1; i <= n; i++) {
read(a[i]);
p[i] = i;
}
sort(p + 1, p + n + 1, [&] (int x, int y) {return a[x] > a[y]; });
for (int i = 0; i <= n; i++)
Break.insert(i);
for (int i = 1; i <= n; i++)
D.inc(1);
int Max = 1; ST.init(n);
for (int i = 1; i <= n; i++) {
inc(p[i], a[p[i]]), inc(p[i] - 1, a[p[i]]);
if (a[p[i]] != a[p[i + 1]]) chkmax(Max, D.query());
}
printf("%d\n", (Max - 1) / 2);
for (int i = 1; i <= n; i++)
printf("%d ", ans[i]);
printf("\n");
return 0;
}
Problem F. Assigning Fares
考虑用其两侧点
c
i
c_i
ci 的大小关系描述边的方向。那么,对于至少有一条路径经过的边,我们需要决定其方向,使得每一条路径上边的方向是一致的。
由此,对于任意一条路径,一旦确定其中一条边的方向,自然可以确定整条路径的方向。可以用带权并查集处理边之间方向相等和相反的关系,若出现某条边与自己方向相反,则答案显然为
−
1
-1
−1 。
考虑用二分答案来解决剩余的最优化问题,令当前二分的答案为
k
k
k 。
注意在本题中,只有存在公共点的边直接存在边之间方向相等和相反的关系,由此,可以考虑设计子树 DP 来判断答案是否可行。令
d
p
i
dp_i
dpi 表示确定
c
i
<
c
f
a
t
h
e
r
i
c_i<c_{father_i}
ci<cfatheri 的情况下,
c
i
c_i
ci 可以取到的最小值。
需要说明的是,若
i
i
i 的父边反向,可以考虑取反整个
i
i
i 的子树,取
c
i
=
k
+
1
−
d
p
i
c_i=k+1-dp_i
ci=k+1−dpi 。
考虑 DP 的转移,由于我们指定了
i
i
i 的父边的方向,因此,一部分子树连向
i
i
i 的边的方向是固定的,这部分子树将会将
d
p
i
dp_i
dpi 限定在一个形如
[
v
,
k
]
[v,k]
[v,k] 的范围内。另一部分子树连向
i
i
i 的边的方向与
i
i
i 的父边的方向没有直接关联,但仍然有可能相互关联,考虑将相互关联的子树进行分组。可以发现,每一组子树将会把
d
p
i
dp_i
dpi 限定在一个形如
[
l
,
r
]
∪
[
k
+
1
−
r
,
k
+
1
−
l
]
[l,r]\cup[k+1-r,k+1-l]
[l,r]∪[k+1−r,k+1−l] 的范围内。
DP 的转移即为合并这些区间,并找到最小的解。我们当然可以使用线段树或是扫描线来做到这一点,但若是这样,总的时间复杂度将会多出一个
O
(
L
o
g
N
)
O(LogN)
O(LogN) 。
线性转移的关键在于注意到 **
[
l
,
r
]
∪
[
k
+
1
−
r
,
k
+
1
−
l
]
[l,r]\cup[k+1-r,k+1-l]
[l,r]∪[k+1−r,k+1−l] 或是一个完整的区间,或是两个关于
k
+
1
2
\frac{k+1}{2}
2k+1 对称的区间。**换言之,若
[
l
,
r
]
∪
[
k
+
1
−
r
,
k
+
1
−
l
]
[l,r]\cup[k+1-r,k+1-l]
[l,r]∪[k+1−r,k+1−l] 中间存在缺口,则这个缺口一定包含
k
+
1
2
\frac{k+1}{2}
2k+1 。那么,这些缺口的并是一个区间。
因此,我们只需要维护区间的交,和缺口的并,即可做到线性转移。
同时,在转移的过程中,我们也可以找到被取反的子树,从而求出具体方案。
时间复杂度 O ( N L o g N ) O(NLogN) O(NLogN) 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
typedef long long ll;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
int n, m; vector <int> a[MAXN];
namespace LowestCommonAncestor {
const int MAXN = 5e5 + 5;
const int MAXLOG = 20;
int depth[MAXN], father[MAXN][MAXLOG];
void work(int pos, int fa) {
depth[pos] = depth[fa] + 1;
father[pos][0] = fa;
for (int i = 1; i < MAXLOG; i++)
father[pos][i] = father[father[pos][i - 1]][i - 1];
for (unsigned i = 0; i < a[pos].size(); i++)
if (a[pos][i] != fa) work(a[pos][i], pos);
}
int lca(int x, int y) {
if (depth[x] < depth[y]) swap(x, y);
for (int i = MAXLOG - 1; i >= 0; i--)
if (depth[father[x][i]] >= depth[y]) x = father[x][i];
if (x == y) return x;
for (int i = MAXLOG - 1; i >= 0; i--)
if (father[x][i] != father[y][i]) {
x = father[x][i];
y = father[y][i];
}
return father[x][0];
}
int climb(int x, int y) {
for (int i = 0; y != 0; i++)
if (y & (1 << i)) {
y ^= 1 << i;
x = father[x][i];
}
return x;
}
}
namespace UnionFind {
const int MAXN = 1e6 + 5;
int f[MAXN];
void init(int n) {
for (int i = 1; i <= n * 2; i++)
f[i] = i;
}
int find(int x) {
if (f[x] == x) return x;
else return f[x] = find(f[x]);
}
void merge(int x, int y) {
x = find(x);
y = find(y);
f[x] = y;
}
void addedge(int x, int y, bool z) {
if (z == false) {
merge(x, y);
merge(x + n, y + n);
} else {
merge(x, y + n);
merge(x + n, y);
}
if (find(x) == find(x + n)) {
puts("-1");
exit(0);
}
}
pair <int, bool> query(int x) {
int tmp = find(x);
if (tmp <= n) return make_pair(tmp, false);
else return make_pair(tmp - n, true);
}
}
bool rev[MAXN];
int k, timer, p[MAXN];
int dp[MAXN], res[MAXN];
void getans() {
for (int i = 1; i <= n; i++) {
int pos = p[i];
if (!rev[pos]) res[pos] = dp[pos];
else res[pos] = k + 1 - dp[pos];
for (auto x : a[pos])
rev[x] ^= rev[pos];
}
}
bool check(int mid) {
k = mid;
for (int i = n; i >= 1; i--) {
int pos = p[i];
vector <int> points;
static bool trev[MAXN];
static pair <int, int> inter[MAXN];
using namespace UnionFind;
for (auto x : a[pos]) {
pair <int, bool> tmp = query(x);
inter[tmp.first] = make_pair(0, 0);
}
pair <int, bool> cur = query(pos);
points.push_back(cur.first);
inter[cur.first] = make_pair(1, k);
for (auto x : a[pos]) {
pair <int, bool> tmp = query(x);
if (inter[tmp.first].first == 0) {
points.push_back(tmp.first);
inter[tmp.first] = make_pair(1, k);
}
if (tmp.second == cur.second) chkmax(inter[tmp.first].first, dp[x] + 1);
else chkmin(inter[tmp.first].second, k - dp[x]);
}
pair <int, int> ban = make_pair(k + 1, 0);
int l = inter[cur.first].first, r = inter[cur.first].second;
for (auto x : points) {
pair <int, int> a = inter[x];
pair <int, int> b = make_pair(k + 1 - a.second, k + 1 - a.first);
if (a.first > a.second) return false;
if (a > b) swap(a, b);
chkmax(l, a.first);
chkmin(r, b.second);
if (a.second + 1 < b.first) {
chkmin(ban.first, a.second + 1);
chkmax(ban.second, b.first - 1);
}
}
if (l > r) return false;
if (l < ban.first || l > ban.second) dp[pos] = l;
else if (ban.second + 1 <= r) dp[pos] = ban.second + 1;
else return false;
for (auto x : points) {
if (inter[x].first <= dp[pos] && inter[x].second >= dp[pos]) trev[x] = false;
else trev[x] = true;
}
for (auto x : a[pos]) {
pair <int, bool> tmp = query(x);
rev[x] = trev[tmp.first] ^ (tmp.second != cur.second);
}
}
return true;
}
int subt[MAXN];
void build(int pos, int fa) {
p[++timer] = pos;
for (auto x : a[pos])
if (x != fa) {
build(x, pos);
subt[pos] += subt[x];
}
for (unsigned i = 0; i < a[pos].size(); i++)
if (a[pos][i] == fa) {
swap(a[pos][i], a[pos][a[pos].size() - 1]);
a[pos].pop_back();
break;
}
if (subt[pos]) {
UnionFind :: addedge(fa, pos, false);
}
}
int main() {
read(n), read(m);
for (int i = 1; i <= n - 1; i++) {
int x, y; read(x), read(y);
a[x].push_back(y);
a[y].push_back(x);
}
UnionFind :: init(n);
LowestCommonAncestor :: work(1, 0);
for (int i = 1; i <= m; i++) {
using namespace LowestCommonAncestor;
int x, y; read(x), read(y);
int z = lca(x, y), p, q;
if (x != z) {
subt[x]++;
subt[p = climb(x, depth[x] - depth[z] - 1)]--;
}
if (y != z) {
subt[y]++;
subt[q = climb(y, depth[y] - depth[z] - 1)]--;
}
if (x != z && y != z) {
UnionFind :: addedge(p, q, true);
}
}
build(1, 0);
int l = 1, r = n;
while (l < r) {
int mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l);
check(l), getans();
for (int i = 1; i <= n; i++)
printf("%d ", res[i]);
printf("\n");
return 0;
}