题解 【AT4440 [AGC028F] Reachable Cells】

command_block

2021-10-18 15:53:05

Solution

> 本题的 $O(n^2\log n)$ 题解。 **题意** : 给出一个 $n\times n$ 的矩阵 $A$,每个格子是障碍或 $1\sim 9$ 的数字。 称格子 $u$ 能到达格子 $v$ 当且仅当: - $u\neq v$ - $A_u,A_v$ 均不为障碍。 - 通过反复向右或向下移动到相邻的空单元格,可以从单元格 $u$ 到达单元格 $v$ ,且不经过障碍。 求出 $\sum_{u,v}[u\text{能到达}v]A_uA_v$,其中 $A$ 代表格子上的数码。 $n\leq 1500$ ,时限$\texttt{9s}$。 ------------ 无法理解出题人怎么想到这种东西的…… F2 $O(n^3)$ 草标是坏文明…… 本文只考虑所有数字都为 $1$ 的情况。对于一般情况,简单地由“集合大小”改为“权值和”即可。 考虑分治,每次计算跨越分界线的贡献。 若能 $O(n^2)$ 计算,则原问题可以 $O(n^2\log n)$ 计算。 ## Part1 观察性质,问题的转化 设分界线为第 $m$ 行。 考虑每次计算上半部分**某一行**为起点的贡献和,记为第 $c$ 行。 对于 $(c,i)$ ,记 $S_i$ 表示可达的第 $m$ 行的集合,记 $U=\bigcup_{i=1}^mS_i$ 。 - **性质①** : $\min S_i,\max S_i$ 随 $i$ 单调不减。(若存在) **证明** : 只证 $\min$ ,$\max$ 类似。 假设有 $i,j$ 满足 $i<j,\ \min S_i>\min S_j$ 。 由 $i\rightarrow \min S_i,\ j\rightarrow \min S_j$ ,有图: ![](https://cdn.luogu.com.cn/upload/image_hosting/atkyy3rr.png) 路径一定会交叉,因此 $i$ 必然可以前往 $\min S_j<\min S_i$ ,矛盾。 记 $l_i=\min S_i,\ r_i=\max S_i$ - **性质②** : 记 $U=\bigcup_{i=1}^mS_i$ (第 $c$ 行能到达的所有分界点),则 $S_i=[l_i,r_i]\cap U$。 **证明** : 显然有 $S_i\subseteq [l_i,r_i]\cap U$ ,只需证 $S_i\supseteq [l_i,r_i]\cap U$ ,即 $p\in [l_i,r_i]\cap U\Rightarrow p\in S_i$。 (反证)对于 $p\in [l_i,r_i]\cap U$ ,假设 $p\notin S_i$ ,找出 $j\neq i$ 满足 $p\in S_j$ 。有图: ![](https://cdn.luogu.com.cn/upload/image_hosting/x1pm2hgz.png) 路径一定会交叉,因此 $i$ 必然可以前往 $p$ ,矛盾。 记 $T_i$ 为分界点 $(m,i)$ 能到达的下半部点集。 $(c,i)$ 能到达的下半部点集可以写作 : $$\bigcup\limits_{j\in U\cup[l_i,r_i]}T_j$$ 从小到大考虑各个 $i$ ,由于 $l_i,r_i$ 单调不降,问题可以转化为 : - 从右侧加入一个 $T_i$ - 从左侧删除一个 $T_i$ - 查询目前 $T$ 的并集大小。 假设目前的区间为 $[l,r]$ ,对于 $i\in[l,r]$ 记 $H_i=T_i\cap\Big(\neg\bigcup\limits_{j\in U\cup(i,r]}T_j\Big)$ ,即目前 $T_i$ 所独有,而后面的 $T$ 没有的部分。 在左侧删除时,并集大小减去 $H_l$。 在右侧加入时,考虑如何更新 $H$ 。 对于新加入的 $T_{r+1}$ ,$H_{r+1}=T_{r+1}$ 。 - **性质③** : 记 $d_i$ 为 $\max_{(x,y)\in T_i}x$ ,即 $(m,i)$ 能到达的最大行。 对于 $i<j<k$ ,若 $\min(d_i,d_k)\leq d_j$ ,则 $T_i\cap T_k\subseteq T_j$。 即:在右侧加入 $T_k$ 不会对这样的 $T_i$ 的 $H_i$ 产生影响。 **证明** :当 $d_i\leq d_j$ ,如图: ![](https://cdn.luogu.com.cn/upload/image_hosting/cgxxfujy.png) 对于右侧的任意一个 $k$ ,$T_k$ 若强行穿过 $T_j$ 的“高墙”,则后来必然在 $T_j$ 的范围内。 若绕过“高墙”,由于无法向上回头,还是碰不到 $T_i$ 。 $d_k\leq d_j$ 的情况类似。 因此,我们可以按照 $d$ (递减地)维护一个单调队列,每次右侧加入 $T_{r+1}$ 时,只需要处理 $d_j\leq d_{r+1}$ 的 $H_j$ (也就是弹出来的 $j$),以及最后剩下的队尾的 $H$ 的的变化。这样,$H$ 的变化次数就是 $O(n)$ 的了。 观察 $H_j$ 的变化,也就是 $H_j$ 与 $H_k$ 交,且不与其他 $H_{j+1\sim r}$ 交的部分。 ![](https://cdn.luogu.com.cn/upload/image_hosting/i0spsnuz.png) 此处 $t\in[j+1,r]$ 是 $d_t$ 最大的 $t$ (是单调队列中的下一个)。 可以发现,$H_j\cap H_k$ 在 $>d_t$ 行,显然不可能与 $T_{j+1\sim r}$ 有交,而在 $\leq d_t$ 行,则必然与 $T_t$ 有交。 这说明了 $H_j$ 中缺少的正是 $H_j\cap H_k$ 在 $>d_t$ 行的部分。记为 $Both(a,b,d)$ 为 $T_a\cap T_b$ 在 $\geq d$ 行的部分。 综上,只需预处理 $U,l,r,d,Both$ ,即可 $O(n)$ 完成上述过程进而解决本题。 ## Part2 $\ l,r,U,d$ 的预处理 **在以下预处理过程中,为了便于考虑,将下半部所有从第 $m$ 行无法到达的位置全设置为障碍。** 将 $l,r,U$ 的定义从特定的行扩展到所有的行。 记 $l_{x,y}$ 表示 $(x,y)$ 能到达的最左 $(m,x)$ 的 $x$ 。$r$ 的定义类似。 $l,r$ 容易 $O(n^2)$ 递推计算。 记 $U_c$ 是 $c$ 行能到达的 $(m,x)$ 的集合。 计算 $U$ 时,对每个 $(m,i)$ 记 $low_i$ 表示能到达 $(m,i)$ 的 $(x,y)$ 的最小 $x$。则 $(m,i)$ 在在 $U_{low_i\sim m}$ 内,而不在 $U_{1\sim low_i-1}$ 内。$low$ 可以利用递推求出。 可以发现, $d$ 就是镜像的 $low$ ,也递推求出。 ## Part3 $Both$ 的预处理 > 我愿称之为全场最难 > 官方题解的思路比较跳跃,讲解也倒了过来 /yun > 这是和官方题解不同的思路,可能更自然 对于 $Both(a,b,d)$ ,状态数大于 $O(n^2)$ ,不可能逐个求出,只能做到预处理后 $O(1)$ 查询。 似乎无从下手,于是先用解析手段写出 $Both(a,b,d)$ 的形式,便于观察。 为了利用性质 ①②,先单独考虑每一行,记 $Both'(a,b,d)$ 为 $T_a,T_b$ 在第 $d$ 行的交。于是有 : $$Both(a,b,d)=\sum\limits_{k\geq d}Both'(a,b,d)$$ 记 $L_{x,d}$ 为 $(m,x)$ 在第 $d$ 行可达的最左侧,类似地有 $R_{x,d}$。 根据性质 ①② ,第 $d$ 行中,$T_a,T_b$ 是一个区间中的非障碍点(还要考虑是否从 $m$ 行可达,前文我们已经删除了所有不可达的点),分别为 $[L_{a,d},R_{a,d}],[L_{b,d},R_{b,d}]$ ,则交集为 $[L_{b,d},R_{a,d}]$ ,当 $L_{b,d}>R_{a,d}$ 时为空。 这又有讨论的必要,如下图: ![](https://cdn.luogu.com.cn/upload/image_hosting/13dpfyyr.png) 红线上方是满足 $L_{b,d}>R_{a,d}$ 的部分,下方是满足 $L_{b,d}\leq R_{a,d}$ 的部分,此时直接计算 $[L_{b,d},R_{a,d}]$ 的和,也就是部分和。 记 $S_{i,j}=\sum\limits_{k=1}^jA_{i,k}$ 为部分和数组。 由上图我们发现,随着 $k$ 的增大, $[L_{b,k}\leq R_{a,k}]$ 会从满足变为不满足,是单调的。其分界点就是 $meet_{a,b}$ ,为 $(x,y)\in T_i\cap T_j$ 的最小 $y$ (即 $(m,i),(m,j)$ 最靠上的汇合处)。 可以写出 $$ \begin{aligned} Both'(a,b,d)&=[L_{b,d}\leq R_{a,d}]\Big(S_{d,R_{a,d}}-S_{d,L_{b,d}-1}\Big)\\ Both(a,b,d)&=\sum\limits_{k\geq d}[L_{b,k}\leq R_{a,k}]\Big(S_{k,R_{a,k}}-S_{k,L_{b,k}-1}\Big)\\ &=\sum\limits_{k\geq \max(d,meet_{a,b})}\Big(S_{k,R_{a,k}}-S_{k,L_{b,k}-1}\Big)\\ &=\sum\limits_{k\geq \max(d,meet_{a,b})}S_{k,R_{a,k}}-\sum\limits_{k\geq \max(d,meet_{a,b})}S_{k,L_{b,k}-1}\\ \end{aligned} $$ 到这里就把 $a,b$ 拆开了,可以分别二维前缀和。 接下来只需求 $L,R,meet$ 即可。 求解 $L,R$ 时,固定 $a$ ,对 $L_{a,\_},R_{a,\_}$ 一起从上到下求解。 ![](https://cdn.luogu.com.cn/upload/image_hosting/1u2qwbe3.png) 从行 $k$ 扩展到行 $k+1$ 时,记 $nxt_{a,b}$ 为 $(a,b)$ 向右第一个能向下走的位置,$nxt_{k,L_{a,k}}$ 即为新的 $L_{a,k+1}$。 对于 $R$ ,先取 $pre_{k,R_{a,k}}$ ,向下走之后再尽量向右。记 $to_{x,y}$ 为 $(x,y)$ 向右一直走到达的 位置,容易预处理。 求解 $meet_{a,b}$ 时,固定 $a$ ,逐渐增大 $b$ 。 由路径交叉不难证明,$b_1<b_2$ 且 $meet_{a,b_1},meet_{a,b_2}\neq \infty\Rightarrow meet_{a,b_1}\leq meet_{a,b_2}$ 。如图: ![](https://cdn.luogu.com.cn/upload/image_hosting/ac843ufa.png) 维护一个单调的 $k$ ,当 $b$ 增加时,令 $k$ 按 $meet_{a,b}$ 不断尝试增加。 若 $k\geq \min(d_a,d_b)$ 则令 $meet_{a,b}=\infty$ 并停止增加。若正常停止, $meet_{a,b}=k$ 。 由于 $k$ 单调不降,处理每个 $a$ 的复杂度为 $O(n)$。 ------------ ## Part4 代码实现 是真的难写……差点放弃 写起来是个套娃多合一,多哈希降低正确率,单个过程其实不难搞懂。 ```cpp #include<algorithm> #include<cstdio> #define MaxN 1510 using namespace std; const int INF=1000000000; char a[MaxN][MaxN]; int l[MaxN][MaxN],r[MaxN][MaxN],d[MaxN][MaxN],D[MaxN],low[MaxN] ,nxt[MaxN][MaxN],pre[MaxN][MaxN],to[MaxN][MaxN] ,L[MaxN][MaxN],R[MaxN][MaxN] ,mp[MaxN][MaxN],oL[MaxN][MaxN],oR[MaxN][MaxN]; #define S d int n1,n2,m; void Init() { #define clr(x,y) {d[x][y]=m;l[x][y]=INF;r[x][y]=0;} for (int i=1;i<=n1;i++){clr(i,0);clr(i,n2+1);} for (int j=1;j<=n2;j++){clr(0,j);clr(n1+1,j);} //l,r for (int j=1;j<=n2;j++) if (a[m][j]==0){l[m][j]=INF;r[m][j]=0;} else l[m][j]=r[m][j]=j; for (int i=m-1;i;i--) for (int j=n2;j;j--) if (a[i][j]==0){l[i][j]=INF;r[i][j]=0;} else { l[i][j]=min(l[i+1][j],l[i][j+1]); r[i][j]=max(r[i+1][j],r[i][j+1]); } //d,low,D for (int i=1;i<=m;i++) for (int j=1;j<=n2;j++) d[i][j]=(a[i][j] ? min(i,min(d[i-1][j],d[i][j-1])) : m); for (int t=1;t<=n2;t++)low[t]=d[m][t]; for (int i=n1;i>=m;i--) for (int j=n2;j;j--) d[i][j]=(a[i][j] ? max(i,max(d[i+1][j],d[i][j+1])) : m); for (int t=1;t<=n2;t++)D[t]=d[m][t]; //to for (int i=m;i<=n1;i++){ to[i][n2+1]=0; for (int j=n2;j;j--) to[i][j]=(a[i][j] ? max(j,to[i][j+1]) : 0); } //pre,nxt,S for (int j=1;j<=n2;j++)S[m][j]=a[m][j]; for (int i=m+1;i<=n1;i++) for (int j=1;j<=n2;j++) S[i][j]=((S[i-1][j]||S[i][j-1]) ? a[i][j] : 0); for (int i=m;i<n1;i++){ nxt[i][n2+1]=INF; for (int j=n2;j;j--) nxt[i][j]=(S[i][j]&&S[i+1][j] ? j : nxt[i][j+1]); for (int j=1;j<=n2;j++) pre[i][j]=(S[i][j]&&S[i+1][j] ? j : pre[i][j-1]); } for (int i=m;i<=n1;i++) for (int j=1;j<=n2;j++) S[i][j]+=S[i][j-1]; //L,R for (int t=1;t<=n2;t++){ int tl,tr; if (!a[m][t]){tl=INF;tr=0;} else {tl=t;tr=to[m][t];} for (int k=m;k<=n1;k++){ L[t][k]=tl;R[t][k]=tr; if (tl!=INF){ tl=nxt[k][tl];tr=to[k+1][pre[k][tr]]; if (tl>tr){tl=INF;tr=0;} } } } //mp for (int a=1;a<=n2;a++){ int k=m; for (int b=a;b<=n2;b++){ while(L[b][k]!=INF&&L[b][k]>R[a][k]&&k<=n1)k++; if (L[b][k]==INF){mp[a][b]=n1+1;continue;} mp[a][b]=k; } } //oL,oR for (int t=1;t<=n2;t++){ oL[t][n1+1]=oR[t][n1+1]=0; for (int k=n1;k>=m;k--){ oL[t][k]=oL[t][k+1]+(L[t][k]!=INF ? S[k][L[t][k]-1] : 0); oR[t][k]=oR[t][k+1]+(L[t][k]!=INF ? S[k][R[t][k]] : 0); } } } int Both(int a,int b,int d){ d=max(d,mp[a][b]); if (d>n1)return 0; int lim=min(D[a],D[b]); return (oR[a][d]-oR[a][lim+1])-(oL[b][d]-oL[b][lim+1]); } int q[MaxN],st[MaxN],H[MaxN],sum,ql,qr; void add(int p) { D[q[qr+1]=0]=m-1; while(ql<=qr&&D[q[qr]]<=D[p]){ int buf=Both(q[qr],p,D[q[qr+1]]+1); H[q[qr]]-=buf;sum-=buf; qr--; } if (ql<=qr){ int buf=Both(q[qr],p,D[q[qr+1]]+1); H[q[qr]]-=buf;sum-=buf; } sum+=(H[q[++qr]=p]=Both(p,p,m)); } void del(int l) {while(ql<=qr&&q[ql]<l)ql++;} long long ans; void calc() { m=(1+n1)/2+1;Init(); int tn=0;sum=0; for (int i=1;i<m;i++){ int tr=0,tl=1;ql=1;qr=sum=0; for (int j=1;j<=n2;j++){ int L=l[i][j],R=r[i][j]; if (L==INF)continue; while(tr<R){++tr;if (low[tr]<=i)add(tr);} del(L); while(tl<L){sum-=H[tl];H[tl++]=0;} ans+=a[i][j]*sum; }for (int j=tl;j<=tr;j++)H[j]=0; } } char s[MaxN][MaxN]; void solve(int l1,int r1,int l2,int r2) { if (l1==r1&&l2==r2)return ; if (r1-l1>=r2-l2){ int mid=(l1+r1)/2; for (int i=l1;i<=r1;i++) for (int j=l2;j<=r2;j++) a[i-l1+1][j-l2+1]=s[i][j]; n1=r1-l1+1;n2=r2-l2+1;calc(); solve(l1,mid,l2,r2); solve(mid+1,r1,l2,r2); }else { int mid=(l2+r2)/2; for (int i=l1;i<=r1;i++) for (int j=l2;j<=r2;j++) a[j-l2+1][i-l1+1]=s[i][j]; n1=r2-l2+1;n2=r1-l1+1;calc(); solve(l1,r1,l2,mid); solve(l1,r1,mid+1,r2); } } int main() { int n;scanf("%d",&n); for (int i=1;i<=n;i++)scanf("%s",s[i]+1); for (int i=1;i<=n;i++) for (int j=1;j<=n;j++) s[i][j]=(s[i][j]=='#' ? 0 : s[i][j]-'0'); solve(1,n,1,n); printf("%lld\n",ans); return 0; } ``` 赠送样例: ```cpp input: 10 ########## ########## ########## ########## 1##1##1### 111111111# #11#111111 #11#11##11 ##1111#11# #####1111# output: 370 ```