XX Open Cup Grand Prix of Moscow【杂题】
传送门:link。
*A Alice and Bob
给定 \(n\) 个点 \(m\) 条边的 DAG,每个点染红/蓝色,求在每个点放 0/1 个棋子使得以下游戏中小红赢的方案数\(\bmod 998244353\)。
小红和小蓝轮流操作,每次选择一个与其对应颜色且有棋子的点,选择一个出边连向的点,移动一个棋子。
\(n\le 300\)。
这是一个不平等博弈,套板子即可。设放在点 \(u\) 上的棋子所对应的游戏值为 \(d_u\),
则当 \(u\) 为红色时 \(d_u=\max(\{0\}\cup\{d_v+1:(u,v)\in E\})\),否则 \(d_u=\min(\{0\}\cup\{d_v-1:(u,v)\in E\})\)。
总共的和就是 \(\sum d_u\),因为 \(d_u\in[-n,n]\),所以直接搞个背包即可,时间复杂度 \(O(n^3)\)。
B Brackets
对于长为 \(2n\) 的括号序列 \(S\),给定 \([1,2n]\cap\N\) 的一组匹配,要求每组匹配的对应位置的字符相同。
求满足条件且合法的字典序最小的 \(S\),需判断无解。
\(n\le 2\cdot 10^5\)。
猜结论+贪心。
对每组匹配按较小位置排序,按顺序考虑能不能填 (
。
给 (
赋权值 \(1\),)
赋权值 \(-1\)。
结论:若还没填的位置全都填 )
的时候最大后缀和非正且原问题有解,那么就是合法的。
证明就是调整法,看不太懂,自闭了。
#include<bits/stdc++.h>
using namespace std;
const int N = 200003, M = 1<<20;
template<typename T>
void read(T &x){
int ch = getchar(); x = 0;
for(;ch < '0' || ch > '9';ch = getchar());
for(;ch >= '0' && ch <= '9';ch = getchar()) x = x * 10 + ch - '0';
}
int n, p[N<<1], sec[N], sum[M], suf[M];
bool vis[N], ans[N];
void build(int x = 1, int L = 1, int R = n){
sum[x] = L-R-1; if(L == R) return;
int mid = L+R>>1;
build(x<<1, L, mid);
build(x<<1|1, mid+1, R);
}
void upd(int p, int v, int x = 1, int L = 1, int R = n){
if(L == R){sum[x] = v; suf[x] = max(v, 0); return;}
int mid = L+R>>1, ls = x<<1, rs = x<<1|1;
if(p <= mid) upd(p, v, ls, L, mid);
else upd(p, v, rs, mid+1, R);
sum[x] = sum[ls] + sum[rs];
suf[x] = max(suf[rs], suf[ls] + sum[rs]);
}
int main(){
read(n); n <<= 1; build();
for(int i = 1;i <= n;++ i){read(p[i]); sec[p[i]] = i;}
for(int i = 1;i <= n;++ i) if(!vis[p[i]]){
int j = sec[p[i]]; vis[p[i]] = true;
upd(i, 1); upd(j, 1);
if(suf[1] > 0){
ans[p[i]] = true;
upd(i, -1); upd(j, -1);
}
} int tmp = 0;
for(int i = 1;i <= n;++ i){
tmp += ans[p[i]] ? -1 : 1;
if(tmp < 0){puts("("); return 0;}
} if(tmp){puts("("); return 0;}
for(int i = 1;i <= n;++ i) putchar(ans[p[i]] ? ')' : '(');
}
-D Deja Vu
给定长为 \(n\) 的正整数序列 \(x_1,x_2,\cdots,x_n\),要求支持 \(q\) 次操作:
- \(\texttt{1 i y}\):令 \(x_i:=y\)。
- \(\texttt{2 l}\):求 \(\min\{d\mid l\le a<b<c<d\land x_a<x_b<x_c<x_d\}\),需判断空集。
\(n,q\le 5\cdot 10^5\),\(x_i,y\le 10^9\)。
好像很恶心,咕了
E Easiest Sum
给定长为 \(n\) 的整数序列 \(a_1,\cdots,a_n\) 和正整数 \(k\)。
每次操作你可以选择 \(p\in[1,n]\),令 \(a_p:=a_p-1\)。
定义 \(g(i)\) 表示操作 \(i\) 次之后最大子段和的最小值。
求 \((\sum_{i=1}^kg(i))\bmod 998244353\)。
\(n\le 10^5\),\(-10^8\le a_i\le 10^8\),\(k\le 10^{13}\)。
容易发现 \(g\) 会有一堆一大段相同,所以转置一下,考虑求 \(c(x)\) 表示使所有子段和 \(\le x\) 的最小操作次数。
设 \(p_i\) 表示对 \(1,2,\cdots,i\) 的操作次数之和,则 \(p_i\ge p_{i-1}\),\(p_j-p_i\ge\sum_{k=i+1}^ja_i-x\pod{i<j}\),要让 \(p_n\) 尽量小。
这是一个差分约束形式,最暴力的方法就是造一个有向图跑最长路:
- \(i\rightarrow j\) 连权值为 \(\sum_{k=i}^{j-1}a_k-x\) 的边(\(1\le i<j\le n+1\))
- \(i\rightarrow i+1\) 连权值为 \(0\) 的边(\(1\le i\le n\))
然后就可以看出来 \(c(x)=\max_k(t_k-xk)\),其中 \(t_k\) 表示 \(k\) 个互不相交的段的和最大值,这个是经典问题。
随着 \(x\) 减少取到最大值的 \(k\) 也是增加的,所以拿这一堆直线切出的就是一个凸包。
具体实现有点麻烦,分界点就是 \(t_{k+1}-t_k\),左开右闭的每一段是一堆线段形成等差数列的形式,最后剩下的散块要特判。
然后发现 pushup 写漏了,pushdown 忘调用了
#include<bits/stdc++.h>
#define MP make_pair
#define PB emplace_back
#define fi first
#define se second
using namespace std;
typedef long long LL;
typedef pair<LL, int> pli;
typedef pair<LL, pair<int, int> > plii;
pli operator - (const pli &a){return MP(-a.fi, a.se);}
plii operator - (const plii &a){return MP(-a.fi, a.se);}
pli operator + (const pli &a, const LL &b){return MP(a.fi + b, a.se);}
plii operator + (const pli &a, const pli &b){return MP(a.fi + b.fi, MP(a.se, b.se));}
const int N = 100010, M = 1<<18, mod = 998244353, inv2 = mod+1>>1;
LL qmo(LL x){return (x % mod + mod) % mod;}
template<typename T>
void read(T &x){
int ch = getchar(); x = 0; bool f = false;
for(;ch < '0' || ch > '9';ch = getchar()) f |= ch == '-';
for(;ch >= '0' && ch <= '9';ch = getchar()) x = x * 10 + ch - '0';
if(f) x = -x;
}
int n, tot; LL k, sum[M], f[M]; bool tag[M];
pli mnp[M], mxp[M], mns[M], mxs[M]; plii mn[M], mx[M];
void prev(int x){
swap(mnp[x], mxp[x]); swap(mns[x], mxs[x]); swap(mn[x], mx[x]);
mnp[x] = -mnp[x]; mxp[x] = -mxp[x]; mns[x] = -mns[x]; sum[x] = -sum[x];
mxs[x] = -mxs[x]; mn[x] = -mn[x]; mx[x] = -mx[x]; tag[x] ^= 1;
}
void pdown(int x){if(tag[x]){prev(x<<1); prev(x<<1|1); tag[x] = false;}}
void pup(int x){
mnp[x] = min(mnp[x<<1], mnp[x<<1|1] + sum[x<<1]);
mxp[x] = max(mxp[x<<1], mxp[x<<1|1] + sum[x<<1]);
mns[x] = min(mns[x<<1|1], mns[x<<1] + sum[x<<1|1]);
mxs[x] = max(mxs[x<<1|1], mxs[x<<1] + sum[x<<1|1]);
mn[x] = min(min(mn[x<<1], mn[x<<1|1]), mns[x<<1] + mnp[x<<1|1]);
mx[x] = max(max(mx[x<<1], mx[x<<1|1]), mxs[x<<1] + mxp[x<<1|1]);
sum[x] = sum[x<<1] + sum[x<<1|1];
}
void build(int x, int L, int R){
if(L == R){
read(sum[x]); if(sum[x] < 0) f[tot--] = sum[x];
mn[x] = mx[x] = MP(sum[x], MP(L, L));
mnp[x] = mxp[x] = mns[x] = mxs[x] = MP(sum[x], L);
return;
} int mid = L+R>>1; build(x<<1, L, mid);
build(x<<1|1, mid+1, R); pup(x);
}
void upd(int x, int L, int R, int l, int r){
if(l <= L && R <= r){prev(x); return;}
int mid = L+R>>1; pdown(x);
if(l <= mid) upd(x<<1, L, mid, l, r);
if(mid < r) upd(x<<1|1, mid+1, R, l, r);
pup(x);
}
int main(){
read(n); tot = n; build(1, 1, n); read(k);
sort(f+tot+1, f+n+1, greater<LL>());
for(int i = 1;i <= n && mx[1].fi > 0;++ i){
f[i] = mx[1].fi; upd(1, 1, n, mx[1].se.fi, mx[1].se.se);
} f[n+1] = -2e13; LL used = 0, cur = f[1], ans = qmo(-cur);
for(int i = 1;i <= n;++ i){
LL nxt = f[i+1];
if(used + (cur - nxt) * i >= k){
nxt = cur - (k - used) / i; used += (cur - nxt) * i;
ans = (ans + qmo(cur+nxt+1) * qmo(cur-nxt) % mod * inv2 % mod * i + qmo(nxt) * qmo(k-used+1)) % mod;
break;
} used += (cur - nxt) * i;
ans = (ans + qmo(cur+nxt+1) * qmo(cur-nxt) % mod * inv2 % mod * i) % mod;
cur = nxt;
} printf("%lld\n", ans);
}
F Funny Salesman
给定 \(n\) 个点的树,边带权,设 \(d(u,v)\) 表示 \(u\) 到 \(v\) 的简单路径上边权的最大值,求在所有长为 \(n\) 的排列 \(p\) 中,\(\sum_{i=2}^n2^{d(p_i,p_{i-1})}\) 的最大值。
\(n\le 10^5\),\(w\in[0,30]\cap\N\)。
这个边权是 \(2^w\) 的性质好像 p 用没有,建 Kruskal 重构树然后贪心就完事了!
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long LL;
const int N = 100003, M = N<<1;
template<typename T>
void read(T &x){
int ch = getchar(); x = 0;
for(;ch < '0' || ch > '9';ch = getchar());
for(;ch >= '0' && ch <= '9';ch = getchar()) x = x * 10 + ch - '0';
}
int n, fa[M], val[M], siz[M], ls[M], rs[M]; LL ans;
struct Edge {
int u, v, w;
bool operator < (const Edge &o) const {return w < o.w;}
} e[N];
int getf(int x){return x == fa[x] ? x : fa[x] = getf(fa[x]);}
int main(){
read(n);
for(int i = 1;i <= n;++ i){fa[i] = i; siz[i] = 1;}
for(int i = 1;i < n;++ i){read(e[i].u); read(e[i].v); read(e[i].w);}
sort(e + 1, e + n);
for(int i = n+1;i < (n<<1);++ i){
ls[i] = getf(e[i-n].u); rs[i] = getf(e[i-n].v);
if(siz[ls[i]] > siz[rs[i]]) swap(ls[i], rs[i]);
fa[ls[i]] = fa[rs[i]] = fa[i] = i; val[i] = e[i-n].w;
siz[i] = siz[ls[i]] + siz[rs[i]];
} int u = (n<<1)-1, k = n-1;
while(u > n){
if(k <= (siz[ls[u]]<<1)){ans += (LL)k<<val[u]; break;}
ans += (LL)siz[ls[u]]<<val[u]+1; k -= siz[ls[u]]<<1; u = rs[u];
} printf("%llu\n", ans);
}
G Graph Coloring
给定 \(n\) 个点的竞赛图,给边 \(14\) 染色,满足 \(\forall i\rightarrow j\rightarrow k\),两条边的颜色不同。
\(3\le n\le 3000\)。
看到 \(\binom{14}7>3000\) 想到....忘了叫啥名字了。
给每个点 \(u\) 赋颜色集合的不同 \(7\) 元子集 \(S_u\),\(i\rightarrow j\) 的颜色是 \(S_i\backslash S_j\) 里的一个颜色。然后发现跟出题人心灵相通了
#include<bits/stdc++.h>
using namespace std;
const int N = 3003;
int n, m, a[N]; char str[N];
int main(){
scanf("%d", &n);
for(int i = 0;m < n;++ i) if(__builtin_popcount(i) == 7) a[m++] = i;
for(int i = 1;i < n;++ i){
scanf("%s", str);
for(int j = 0;j < i;++ j)
putchar(__builtin_ctz(str[j] == '1' ? (a[i] & ~a[j]) : (a[j] & ~a[i])) + 'a');
putchar('\n');
}
}
*H Hidden Graph
这是一道交互题
给定正整数 \(n\),交互库有正整数 \(k\) 和一张 \(n\) 个点的无向图,满足所有导出子图的最小度数 \(\le k\)。
你可以向交互库询问至多 \(2nk+n\) 次,每次询问给出点集 \(S\),交互库会返回给你 \(S\) 的导出子图中的一条边或告诉你 \(S\) 是独立集。求这张图。
\(k<n\),\(nk\le 2000\)。
关键 trick:所有导出子图的最小度数 \(\le k\Rightarrow\) 可以 \(k+1\) 染色。
考虑增量计算,维护 \(k+1\) 个独立集,若当前加入点 \(i\),那么将 \(i\) 跟每个独立集拼一拼,询问出所有边。
至多额外花费 \(n(k+1)\) 次询问,于是总询问次数 \(\le n(2k+1)\)。
*I Insects
初始时给定平面上 \(n\) 个黑点 \((a_i,b_i)\)。
定义第 \(i\) 个白点与第 \(j\) 个黑点之间有连边当且仅当 \(x_i\ge a_j\land y_i\ge b_j\)。
要求支持 \(m\) 次询问,每次加一个白点 \((x_i,y_i)\),求最小点覆盖。
\(n,m\le 10^5\),\(0\le a_i,b_i,x_i,y_i\le 10^5\)。
不妨设白点是左部点,黑点是右部点。
定理 1 (Konig Theorem) 二分图的最大匹配与最小点覆盖大小相同。
proves
显然最小点覆盖大小 \(\ge\) 最大匹配大小。于是只需构造一组解即可。
对于一组最大匹配 \(M\),定义匹配边的方向从右向左,非匹配边的方向从左向右。
在这个有向图上,从左边未被匹配覆盖的点开始 DFS,设访问到的点集是 \(Z\),左右部点的集合是 \(L,R\)。
定义 \(L_+=L\cap Z\),\(L_-=L\cap(V-Z)\),\(R_+=R\cap Z\),\(R_-=R\cap(V-Z)\)。
则 \(L_-\cup R_+\) 是点覆盖,因为 \(L_+\) 与 \(R_-\) 之间没有边。
考虑证明 \(|L_-\cup R_+|=|M|\)。由定义可得 \(L_-\subseteq V(M)\),可以证明 \(R_+\subseteq V(M)\),因为若 \(v\in R_+\) 但 \(v\notin V(M)\),则出发点到 \(v\) 的路径是一条增广路,与 \(|M|\) 的最大性相矛盾。且每条匹配边的两端点同时 \(\in Z\),得证。
推论 2 \(L_+\cup R_-\) 是最大独立集。
引理 3 \(L_+\cup R_+\) 的最大匹配覆盖 \(R_+\)。
引理 4 \(L_-\cup R_-\) 的最大匹配覆盖 \(L_-\)。
定义最优匹配是覆盖左部点编号字典序最小的最大匹配。
引理 5 第 \(i\) 次询问的答案即为 \([1,i]\) 与最优匹配的交的大小。
引理 6 设 \(A_k\) 是左部的前 \(k\) 个点,考虑 \(A_k\cup R\) 的导出子图,设 \(A_-,A_+,R_-,R_+\) 如上定义,\(M_+\) 是 \(A_+\cup R_+\) 的最优匹配,则存在一组 \(A_k\cup R\) 的最优匹配是 \(M_+\) 的超集。
proves
根据引理 4,\(A_-\cup R_-\) 的最大匹配都是最优的。
所以将 \(A_-\cup R_-\) 与 \(A_+\cup R_+\) 的最优匹配直接合并就可以得到最大匹配,并且找不到更优的,所以是最优匹配。得证。
考虑分治,设函数 \(f(L,R,x)\) 表示当前在求 \(L\cup R\) 的最优匹配,但 \(L\) 的前 \(|L|-x\) 个点已经考虑过了,只需要考虑后面的 \(x\) 个点。
先考虑求出这 \(x\) 个点中前 \(\lfloor x/2\rfloor\) 个点的最优匹配。设 \(A\) 是 \(L\) 的前 \(|L|-\lceil x/2\rceil\) 个点,如上求出 \(A_-,A_+,R_-,R_+\)。
只需分别求出 \(A_+\cup R_+\) 和 \(A_-\cup R_-\) 的最优匹配,根据引理 3,\(R_+\) 会被 \(A_+\) 匹配完,根据引理 6,\(A_+\) 中未被匹配的点不会再被匹配,所以调用 \(f(A_+,R_+,t)\) 之后就可以把 \(A_+,R_+\) 删掉,其中 \(t\) 是 \(A_+\) 与 \(L\) 的后 \(x\) 个点的交大小。显然 \(t\le\lfloor x/2\rfloor\)。
然而 \(R_-\) 中剩下的点可能还有用处,所以不能删。
对于剩下的点,如上求出 \(L_-,L_+,R_-,R_+\),同理可得调用 \(f(L_+,R_+,t)\),其中 \(t\) 是 \(L_+\) 与 \(L\) 的后 \(\lceil x/2\rceil\) 个点的交的大小。显然 \(t\le\lceil x/2\rceil\)。
根据引理 4,\(L_-\) 中的所有点都可以被匹配上。
最后 \(x=1\) 的时候可以直接用暴力方法跑两次最大匹配,看看 \(L\) 的最后一个点是不是必要的。
于是我们只需要更快地做最大匹配和 dfs 增广树。
前者可以直接贪心,将黑点和白点都按水平序排序,按顺序枚举白点,选择能选的黑点中纵坐标最大的。可以搞一个 set 维护。
后者同样排序,用线段树维护黑点的纵坐标的区间最小值,每次询问的时候就可以直接查了。
因为分治不超过 \(\log n\) 层,并且每一层的 \(L,R\) 集合不交,所以总复杂度 \(O(n\log^2n)\)。
代码好像有丶难写