XXI Open Cup. Grand Prix of Korea

command_block

2021-05-20 20:13:42

Personal

> 感谢 @jiangly 大神指导 **Link** : [CF gym](https://codeforces.com/gym/102759) Open Cup 好! ## [DS] A ## [??] B Cactus Competition $\color{blue}\bigstar$ **题意** : 给出两个序列 $A_{1\sim n},B_{1\sim m}$。 构造一个 $n\times m$ 的矩阵 $H$,其中 $H_{i,j}=A_i+B_j$。 从 $H_{1,s}$ 出发前往 $H_{n,t}$ ,其中只能经过权值非负的位置(包括起点和终点),且只能向右或向下行走。 求有多少组 $H_{s,1}\rightarrow H_{t,m}$ 是可达的。 $n,m\leq 2\times 10^5$ ,时限$\texttt{2s}$。 ------------ 先考虑如何判定 $H_{1,1}\rightarrow H_{n,m}$ 是否可达。 初看本题,没有什么好的切入点,尝试发掘一些简单的性质。 - 若 $\min\{A\}+\max\{B\}<0$ ,则说明有一行($\min\{A\}$ 的那一行)全为负。 - 若 $\max\{A\}+\min\{B\}<0$ ,则说明有一列($\min\{B\}$ 的那一列)全为负。 若满足上述两个条件之一,则说明 $H_{1,1}\rightarrow H_{n,m}$ 不可达。下面讨论它们不满足的情况。 - $\min\{A\}+\max\{B\}\geq 0$ ,则说明有一列($\max \{B\}$ 的那一列)全非负。 - $\max\{A\}+\min\{B\}\geq 0$ ,则说明有一行($\max \{A\}$ 的那一行)全非负。 我们发现了一个有趣的性质,在本题中,“存在一行全为负”否定后可以导出“存在一列全非负”。 ![](https://cdn.luogu.com.cn/upload/image_hosting/jw9w9vpd.png) 画出这非负的一行一列,记为 $H_{i,\_},H_{\_,j}$ ,如图 $A$。 考虑构造一种方案,从左上角先来到 $H_{i,j}$ ,再前往 $H_{n,m}$。 若左上方黄色区域内存在一行**与**一列均为负,显然无解。否则,黄色区域内必然存在一行**或**一列非负。如图 $B$。 这样,就将左上角区域缩小了,不断重复,即可到达起点。类似地,也可以到达终点。 根据上面的观察,只需要排除下列四种阻断情况即可 : ![](https://cdn.luogu.com.cn/upload/image_hosting/5lbz7bh4.png) 四种阻断均不存在 $\Longleftrightarrow$ 可以从 $H_{1,1}$ 到达 $H_{n,m}$。 接下来考虑如何计算答案。 对于全负的行,会将问题分成若干段子问题(也可以看做提供下界)。下面我们不再关心这类限制。 枚举 $s$ ,考虑有多少 $t$ 满足 $H_{s,1}\rightarrow H_{t,m}$ : - $\max_{i\in[s,t]}A_i+\min\{B\}\geq 0$ 找出第一个 $i\geq s$ 使得 $A_i+\min\{B\}\geq 0$。则 $i\leq t$ 的都满足条件。 综上,蓝色和红色隔断为每个起点 $H_{s,1}$ 限制了一个终点的区间。 对于图中绿色和橙色的阻断,相当于直接除掉了一些起点和终点。 - 绿色,左上角阻断(橙色右下角阻断类似) 对于起点 $H_{s,1}$,若存在 $H_{x,y}$ 满足 $A_x+B_i<0\ (1\leq i\leq y)$ 且 $A_i+B_y<0\ (s\leq i\leq x)$ ,则将 $H_{s,1}$ 删除。 枚举 $x$ ,考虑所有 $H_{x,\_}$ 的贡献。 找到最小的 $j$ 使得 $A_x+B_j\geq 0$ ($B$ 从大到小排序后前缀编号 $\min$),则前缀 $A_x+B_{1\sim j-1}$ 都是负的。 为了尽量向上延伸,只需找到 $B_{1\sim j-1}$ 中最小的一个,记为 $B_y$。 然后找出一个最大的 $i\leq x$ 满足 $A_i+B_y\geq 0$ (单调栈 + 二分),则区间 $[i+1,x]$ 之间的起点都被删除。 先去除不合法的起点终点(可以用差分),然后若干次区间求和即可。 复杂度 $O\big((n+m)\log n\big)$。 注意不要把 $n,m$ 写反…… ```cpp #include<algorithm> #include<cstdio> #define MaxN 200500 using namespace std; int a[MaxN],b[MaxN],pl[MaxN],pr[MaxN],p2[MaxN],px[MaxN],p3[MaxN]; bool cmp(int A,int B){return b[A]<b[B];} int n,m,stk[MaxN],top,o1[MaxN],o2[MaxN],tl[MaxN],tr[MaxN]; int main() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++)scanf("%d",&a[i]); for (int i=1;i<=m;i++)scanf("%d",&b[p2[i]=i]); for (int i=1;i<=m;i++) pl[i]=(i==1||b[i]<b[pl[i-1]]) ? i : pl[i-1]; for (int i=m;i;i--) pr[i]=(i==m||b[i]<b[pr[i+1]]) ? i : pr[i+1]; sort(p2+1,p2+m+1,cmp); for (int i=1;i<=m;i++)px[i]=b[p2[i]]; p3[m+1]=m+1; for (int i=m;i;i--)p3[i]=min(p2[i],p3[i+1]); for (int x=1;x<=n;x++){ while(top&&a[x]>a[stk[top]])top--; stk[++top]=x; int j=p3[lower_bound(px+1,px+m+1,-a[x])-px]; if (j==1)continue; int y=pl[j-1]; if (a[stk[1]]+b[y]<0){o1[1]++;o1[x+1]--;continue;} int l=1,r=top,mid; while(l<r){ mid=(l+r+1)>>1; if (a[stk[mid]]+b[y]>=0)l=mid; else r=mid-1; }o1[stk[l]+1]++;o1[x+1]--; } for (int i=1;i<=n;i++)o1[i]+=o1[i-1]; for (int i=1;i<=n;i++)o1[i]=!o1[i]; p3[m+1]=0; for (int i=m;i;i--)p3[i]=max(p2[i],p3[i+1]); top=0; for (int x=n;x;x--){ while(top&&a[x]>a[stk[top]])top--; stk[++top]=x; int j=p3[lower_bound(px+1,px+m+1,-a[x])-px]; if (j==m)continue; int y=pr[j+1]; if (a[stk[1]]+b[y]<0){o2[x]++;continue;} int l=1,r=top,mid; while(l<r){ mid=(l+r+1)>>1; if (a[stk[mid]]+b[y]>=0)l=mid; else r=mid-1; }o2[x]++;o2[stk[l]]--; } for (int i=1;i<=n;i++)o2[i]+=o2[i-1]; for (int i=1;i<=n;i++)o2[i]=o2[i-1]+(!o2[i]); int minb=b[1],maxb=b[1]; for (int i=2;i<=m;i++) {minb=min(minb,b[i]);maxb=max(maxb,b[i]);} for (int s=n,t=n+1;s;s--){ if (a[s]+minb>=0)t=s; tl[s]=t; } for (int s=n,t=n;s;s--){ if (a[s]+maxb<0)t=s-1; tr[s]=t; } long long ans=0; for (int s=1;s<=n;s++) if (o1[s]&&tl[s]<=tr[s])ans+=o2[tr[s]]-o2[tl[s]-1]; printf("%lld",ans); return 0; } ``` ## [DP] C Economic One-way Roads **题意** :给出一张 $n$ 个点的无向连通图。 将所有道路定向。每条道路定于两个方向都有各自的花费。 最终要使得定向后的图为强连通图,求最小花费。 $n\leq 18$ ,时限$\texttt{5s}$。 ------------ - **强连通图的耳分解** 有向图 $G=(V,E)$ 强连通当且仅当可以通过以下方法构造: 任选一个点 $s$,令 $S=\{s\}$。 重复以下过程直到 $S=V$。 任选两个点 $u,v\in S$。$u$ 和 $v$ 可以相同。 选择 $k\pod{k\ge 0}$ 个不同的点 $w_1,w_2,\ldots,w_k\not\in S$。 连接 $u\to w_1\to w_2\to\cdots\to w_k\to v$,并将 $w_1,w_2,\ldots ,w_k$ 加入 $S$。 其中,路径 $u\to w_1\to w_2\to\cdots\to w_k\to v$ 被称作“耳”。 考虑状压 $\rm DP$ ,记 $dp[S]$ 表示耳分解已经含有点集 $S$ ,$S$ 内部的最小花费。 然后枚举一条路径,路径上的边的费用是固定的,其余的边选择费用较小的方向。 然而这需要子集 $\rm DP$ ,复杂度为 $O\big(3^n{\rm poly}(n)\big)$ ,无法通过。 考虑类似轮廓线的思路,不要一次加入整条路径,而是逐个点加入。 记 $f[S][u][v]$ 表示已经加入了点集 $S$ ,还需要加入一条 $u\rightarrow v$ 的路径,最小的代价。转移时枚举 $u$ 的下一个点。 当 $u=v$ 时,则可以贡献到 $dp[S]$。对于 $dp[S]$ ,任选两个点 $u,v\in S$ 作为路径端点,转移到 $f[S][u][v]$。 时间复杂度 $O(n^32^n)$。 $f$ 数组所需的空间过大,按 $s$ 中 $1$ 的个数滚动数组,可以将空间复杂度优化至 $O\big(\binom{n}{n/2}n^2\big)$ ```cpp ``` ## [DS] E Chemistry **题意** : 给出一张 $n$ 个点 $m$ 条边的无向图。 求有多少个区间点集 $[l,r]$ 的导出子图为一条链(不一定要以标号为序)。 $n,m\leq 2.\times 10^5$ ,时限$\texttt{5s}$。 ------------ 某个点集的导出子图是一条链的充要条件 : - 没有环 - 边数等于点数减一(达到上界) - 没有三度点 从左到右枚举右端点,考虑每个左端点对应的区间是否合法。 无环 :用 LCT + 双指针维护。这会去掉一个前缀。 无三度点 :维护每个点的度数,双指针。这也会去掉一个前缀。 边数等于点数减一 : 对于位置 $l$ ,记 $c_l$ 表示点集 $[l,r]$ 的导出子图的边数。 若新加一条边 $(r,u)$ ,则将 $c_{1\sim u}$ 加一。 最终我们要询问有多少 $c_{l}=r-l$ 的位置,只需用线段树维护 $c_l-l$ ,然后区间查询最大值与最大值个数即可。 复杂度 $O(n\log n)$。 ```cpp #include<algorithm> #include<cstdio> #include<vector> #define pb push_back #define MaxN 250500 using namespace std; struct LCT{ struct Node{ int l,r,f;bool fl; inline void rev() {swap(l,r);fl^=1;} }a[MaxN]; inline bool nrt(int u) {return a[a[u].f].l==u||a[a[u].f].r==u;} inline void ladd(int u){ if (a[u].fl){ a[a[u].l].rev(); a[a[u].r].rev(); a[u].fl=0; } } void rot(int u) { int fa=a[u].f,gf=a[fa].f; ladd(fa);ladd(u); if (a[gf].l==fa)a[gf].l=u; if (a[gf].r==fa)a[gf].r=u; a[a[fa].f=u].f=gf; if (a[fa].l==u){ a[a[fa].l=a[u].r].f=fa; a[u].r=fa; }else { a[a[fa].r=a[u].l].f=fa; a[u].l=fa; } } void splay(int u){ ladd(u); while(nrt(u)){ int fa=a[u].f,gf=a[fa].f; if (nrt(fa)&&(a[fa].l==u)==(a[gf].l==fa)) rot(fa); rot(u); } } void access(int u){ int sav=u; for (int v=0;u;v=u,u=a[u].f) {splay(u);a[u].r=v;} splay(sav); } void makrt(int u){access(u);a[u].rev();} int findrt(int u){ access(u); while(a[u].l){ladd(u);u=a[u].l;} splay(u);return u; } void spilt(int u,int v){makrt(u);access(v);} void link(int u,int v){makrt(u);a[u].f=v;} void cut(int x,int y){spilt(x,y);a[x].f=a[y].l=0;} bool chk(int u,int v){return findrt(u)==findrt(v);} }T; struct Node{ int x,c,tg; inline void ladd(int t){tg+=t;x+=t;} }a[MaxN<<2]; inline void up(int u){ int l=u<<1,r=u<<1|1; a[u].x=max(a[l].x,a[r].x); a[u].c=((a[l].x==a[u].x) ? a[l].c : 0) +((a[r].x==a[u].x) ? a[r].c : 0); } int n; void build(int l=1,int r=n,int u=1) { if (l==r){a[u].x=l;a[u].c=1;return ;} int mid=(l+r)>>1; build(l,mid,u<<1); build(mid+1,r,u<<1|1); up(u); } inline void ladd(int u){ if (a[u].tg){ a[u<<1].ladd(a[u].tg); a[u<<1|1].ladd(a[u].tg); a[u].tg=0; } } int wfl,wfr,wfc; void add(int l=1,int r=n,int u=1) { if (wfl<=l&&r<=wfr) {a[u].ladd(wfc);return ;} int mid=(l+r)>>1;ladd(u); if (wfl<=mid)add(l,mid,u<<1); if (mid<wfr)add(mid+1,r,u<<1|1); up(u); } int qry(int l=1,int r=n,int u=1) { if (wfl<=l&&r<=wfr) return a[u].x==wfc ? a[u].c : 0; int mid=(l+r)>>1,ret=0;ladd(u); if (wfl<=mid)ret=qry(l,mid,u<<1); if (mid<wfr)ret+=qry(mid+1,r,u<<1|1); return ret; } vector<int> g[MaxN]; void cut(int u,int r){ for (int i=0;i<g[u].size();i++) if (u<g[u][i]&&g[u][i]<=r)T.cut(u,g[u][i]); } int m,d[MaxN]; void cut2(int u,int r){ for (int i=0;i<g[u].size();i++) if (u<g[u][i]&&g[u][i]<=r)d[g[u][i]]--; } int main() { scanf("%d%d",&n,&m); for (int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); g[u].pb(v);g[v].pb(u); }for (int i=1;i<=n;i++)sort(g[i].begin(),g[i].end()); build(); long long ans=0; for (int r=1,tl=1,tl2=1;r<=n;r++){ for (int i=0;i<g[r].size();i++){ int v=g[r][i];if (v>r)continue; while(T.chk(r,v))cut(tl++,r-1); if (tl<=v)T.link(r,v); while(tl2<=v&&(d[v]==2||d[r]==2))cut2(tl2++,r); if (tl2<=v){d[v]++;d[r]++;} wfl=1;wfr=g[r][i];wfc=1;add(); }wfl=max(tl,tl2);wfr=wfc=r;if (wfl<=wfr)ans+=qry(); }printf("%lld\n",ans); return 0; } ``` ## [??] F Interval Graph **题意** 给出 $n$ 个区间,第 $i$ 个区间为 $[l_i,r_i]$ ,且权值为 $w_i$。 构造一张 $n$ 个顶点的无向图,其中顶点 $u$ 对应区间 $u$。 当且仅当对应的区间对具有非空交集时,两个顶点之间存在一条边。 要删除一些点,使得该图无环。求需要删除的最小点权和。 $n\leq 2.5\times 10^5$ ,时限$\texttt{3s}$。 ------------ 观察发现,图无环等价于每个点至多被区间覆盖两次。 这是经典的费用流问题,可见 [P3358 最长k可重区间集问题](https://www.luogu.com.cn/problem/P3358) - 对于区间 $[l_i,r_i]$ 从 $l_i$ 向 $r_i+1$ 连边,容量为 $1$ ,权值为 $w_i$。 - 对于每个位置 $\overline{x}$ ,从 $\overline{x}$ 向 $\overline{x+1}$ 连边,容量为 $+\infty$ - 且连边 $S\rightarrow \overline{-\infty},\overline{+\infty}\rightarrow T$ ,容量均为 $+\infty$ 这样,一条容量为 $1$ 的流就代表选出一组不交的区间集合。 求容量为 $2$ 的最大费用流即可得到“可以保留的最大点权和”。于是需要求两次最长路。 注意到该图是个 $\rm DAG$ ,利用原式对偶算法,为点 $\overline{u}$ 加势能 $-10^{12}*u$ ,则所有的边就都是负的了,可以使用 $\rm Dijkstra$。 复杂度 $O(n\log n)$。 ```cpp ``` ## [DS] I Query On A Tree 17 $\blacktriangle$ **题意** :给出一棵 $n$ 个点的有根树,初始时每个点的点权为 $0$。 执行 $q$ 次操作,每次操作为下列两种之一 : - 将 $u$ 子树内的点权 $+1$ - 将 $u$ 到 $v$ 的简单路径上的点权 $+1$ 。 记 $u$ 的点权为 $A_u$ ,每次操作后,找出一个顶点 $t$ 使得 $\sum\limits_{u=1}^nA_u\times dis(u,t)$ 最小。 若有多个满足条件的点,输出深度最小的,可以证明这样的点是唯一的。 $n,q\leq 10^5$ ,时限$\texttt{2s}$。 ------------ 显然, $t$ 是树的带权重心。 记所有点的点权和为 $S$ ,则最浅带权重心的子树点权和一定严格大于 $S/2$。 将所有顶点按照 $\rm dfs$ 序写出,点 $u$ 重复 $A_u$ 次。 重心的子树是该序列上的一个区间,且长度大于序列长度的一半,。 于是,这个区间一定包含序列最中间的元素(若区间长度为偶数,则可以任取一个) 用重链剖分加线段树维护点权。每次用线段树上二分找到序列最中间的元素,并从该顶点出发向上倍增,找到第一个子树点权和 $>S/2$ 的数即可。 复杂度 $O(n+q\log^2 n)$。 ```cpp #include<algorithm> #include<cstdio> #include<vector> #define pb push_back #define ll long long #define MaxN 100500 using namespace std; vector<int> g[MaxN]; struct TNode{int f,tf,p;}b[MaxN]; int siz[MaxN],dep[MaxN],f[20][MaxN]; void pfs1(int u) { siz[u]=1; for (int i=0,v;i<g[u].size();i++) if (!siz[v=g[u][i]]){ dep[v]=dep[f[0][v]=b[v].f=u]+1; pfs1(v); siz[u]+=siz[v]; if (siz[v]>siz[b[u].p]) b[u].p=v; } } int dfn[MaxN],out[MaxN],tim,tp[MaxN]; void pfs2(int u,int tf) { b[u].tf=tf;dfn[u]=++tim; if (b[u].p)pfs2(b[u].p,tf); for (int i=0;i<g[u].size();i++) if (!b[g[u][i]].tf) pfs2(g[u][i],g[u][i]); out[u]=tim; } struct Node{ int len,tg;ll x; inline void ladd(int c) {tg+=c;x+=1ll*len*c;} }a[MaxN<<2]; inline void up(int u) {a[u].x=a[u<<1].x+a[u<<1|1].x;} int n,wfl,wfr;ll ret,tot,wfk; void build(int l=1,int r=n,int u=1) { a[u].len=r-l+1; if (l==r)return ; int mid=(l+r)>>1; build(l,mid,u<<1); build(mid+1,r,u<<1|1); } inline void ladd(int u){ if (a[u].tg){ a[u<<1].ladd(a[u].tg); a[u<<1|1].ladd(a[u].tg); a[u].tg=0; } } void add(int l=1,int r=n,int u=1) { if (wfl<=l&&r<=wfr) {a[u].ladd(1);tot+=a[u].len;return ;} int mid=(l+r)>>1;ladd(u); if (wfl<=mid)add(l,mid,u<<1); if (mid<wfr)add(mid+1,r,u<<1|1); up(u); } void qry(int l=1,int r=n,int u=1) { if (wfl<=l&&r<=wfr){ret+=a[u].x;return ;} int mid=(l+r)>>1;ladd(u); if (wfl<=mid)qry(l,mid,u<<1); if (mid<wfr)qry(mid+1,r,u<<1|1); } int kth(int l=1,int r=n,int u=1) { if (l==r)return l; int mid=(l+r)>>1;ladd(u); ll ls=a[u<<1].x; if (wfk>ls){wfk-=ls;return kth(mid+1,r,u<<1|1);} return kth(l,mid,u<<1); } void padd(int u,int v) { while(b[u].tf!=b[v].tf){ if (dep[b[u].tf]<dep[b[v].tf])swap(u,v); wfl=dfn[b[u].tf];wfr=dfn[u];add(); u=b[b[u].tf].f; }wfl=dfn[u];wfr=dfn[v]; if (wfl>wfr)swap(wfl,wfr);add(); } void subadd(int u) {wfl=dfn[u];wfr=out[u];add();} ll subqry(int u) {ret=0;wfl=dfn[u];wfr=out[u];qry();return ret;} int calc(int u) { for (int k=16;k>=0;k--){ int v=f[k][u]; if (!v)continue; if (subqry(v)*2<=tot)u=v; }return (subqry(u)*2<=tot) ? f[0][u] : u; } int q; int main() { scanf("%d",&n); for (int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); g[u].pb(v);g[v].pb(u); }dep[1]=1;pfs1(1);pfs2(1,1); for (int j=1;j<=16;j++) for (int i=1;i<=n;i++) f[j][i]=f[j-1][f[j-1][i]]; for (int i=1;i<=n;i++)tp[dfn[i]]=i; build(); scanf("%d",&q); for (int i=1,op,u,v;i<=q;i++){ scanf("%d%d",&op,&u); if (op==1)subadd(u); else {scanf("%d",&v);padd(u,v);} wfk=(tot+1)/2; printf("%d\n",calc(tp[kth()])); }return 0; } ``` ## [DS] L Steel Slicing 2 $\blacktriangle$ **题意** : 给出一张纸片,宽为 $n$。其中第 $i$ 单位宽度向上延伸 $l_i$ ,向下延伸 $r_i$。 ![](https://espresso.codeforces.com/1575809816c5b3a960f6d650581cee1227d7bc82.png) 要求对该纸片切若干刀,每一刀都恰好将每一个纸片切成两份,要求切完后所有纸片都是矩形,求最小的刀数。 $n\leq 2.5\times 10^5$ ,时限$\texttt{1s}$。 ------------ 显然,所有刀一定水平切或者竖直切。 又能发现,当图中不存在凹顶点时,则代表所有纸片都是凸多边形。又因为刀法是水平的,所以就都是矩形。 进一步观察,若还有凹顶点,则必定存在一刀减少一个凹顶点的方案。此外,一刀至多减少两个凹顶点。 ![](https://cdn.luogu.com.cn/upload/image_hosting/8mu2k3jn.png) 于是,我们只需最大化切掉两个凹顶点的刀数(称这样的刀是好的)。若总凹顶点个数为 $c$ ,切掉两个凹顶点的刀数为 $d$ ,则总刀数为 $c-d$。 将所有能好的刀痕都画出来,如图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/n4qgzuu7.png) 其中,第 $i$ 列与第 $i+1$ 列之间的(竖着的)刀痕是好的,当且仅当 $l_i\neq l_{i+1},r_i\neq r_{i+1}$。 此外,若 $j-i>1,\ l_i=l_j$ 且 $\min_{k=i+1}^{j-1}l_k>l_i$ ,则存在一刀从 $l_i$ 切到 $l_j$ 是好的。 $r$ 的一侧类似。可以利用笛卡尔树求出所有横着的好刀痕,共 $O(n)$ 条。 接下来,我们要选取若干刀痕实际切下。不难发现,一个刀痕集合合法,当且仅当没有两个刀痕在非端点处相交。 刀痕的相交关系可以被描述为二分图,接下来就是要求该二分图的最大独立集大小。可以转化为求最大匹配。 不难发现,每条横刀能匹配的竖刀是一个区间。且这些区间不会部分相交,只会包含或相离。 于是,有简单的贪心策略 : 从左往右考虑每条竖刀,选择一条右端点尽量靠左的横刀来匹配。 ```cpp ```