[USACO21JAN] 金组 Dance Mooves

· · 题解

题目链接 : [USACO21JAN] Dance Mooves G

前置知识

推荐先写出银组的本题的弱化版 : [USACO21JAN] Dance Mooves : Silver , 这样对理解本题会有更大帮助 . 或者你也可以选择直接看本篇题解 .

总体思路

这里不再简述题意 , 因为题面比较清晰明了 .

当进行完一轮置换 , 也就是 K交换之后 , 我们设 \mathrm{next}_i 是执行完一轮置换之后 , 第 i 头奶牛会到达的位置 . 就拿样例来说 , 原来的奶牛序列是 :

1,2,3,4,5,6

那么经过 K=4 次的交换后 , 序列变成 :

2,3,4,5,1,6 现在我说 : 如果把每一个位置看做一个**点** , 点 $i$ 向 $\mathrm{next}_i$ 连边 , 那么我们会得到一堆**简单环**构成的**森林** . 对于样例来说 , 我们形成了两个环 : ![](https://cdn.luogu.com.cn/upload/image_hosting/0q5aq1cl.png?x-oss-process=image/resize,m_lfit,h_170,w_225) 证明也很简单 . 我们知道 , 在**一轮置换**之后 , $i$ 位置上的奶牛一定到达了 $\mathrm{next}_i$ 位置 , 而必然存在另一个位置 $j$ , **一轮置换**后到达了 $\mathrm{next}_j=i$ 位置 . 理由是 $i$ 位置不可能是没有奶牛的 , 所以 $j$ 的存在性显然 . 至于 $\mathrm{next}_i=i$ 的情况 , 那就显然是一个**自环** . 此时不可能存在 $j\neq i$ , 使得 $\mathrm{next}_j=i$ . 这也就是说 , 这 $n$ 个**结点**的入度和出度均为 $1$ , 且一共 $n$ 条边 . 那么就形成了一堆**简单环**构成的**森林** . 现在 , 我们就把**序列**问题转化到了**图上** . 我们知道 , 如果没有 $M$ 的限制 , 而是可以**无限**走 : 那么一个环上的点最终都可以**彼此到达** . 这也是本题**测试点** $6-10$ 的情况 , 或者说是**银组**本题弱化版的情况 . 那么在这种情况下 , 我们开 $n$ 个 $\rm vector$ , 令 $\mathrm{pass}_i$ 表示 $i$ 在**一轮置换**中 , 一共经过了**哪些位置** , 比如还是样例中 , $\mathrm{pass}_1=\{1,2,3,4,5\}$ . 对于一个环上的所有点 , 由于是无限置换 , 所以它们能到达的**位置集合**必然相同 . 只需要对每一个环上每一个点的 $\mathrm{pass}_i$ 进行遍历 , **位置集合**大小即为这一个环的答案 . 由于一个 $\mathrm{pass}_i$ 只会被统计 $1$ 次 , 而 $\mathrm{pass}_i$ 的总大小约为 $2K$ , 所以统计答案的时间复杂度为 $\mathcal O(K)$ 的 . 那么现在回到有限制的情况 . 我们知道 , 在 $M$ 分钟内 , 我们进行了 : $$\mathrm{times}=\lfloor\frac{M}{K} \rfloor$$ 轮**置换** , 进行完这 $\mathrm{times}$ 轮置换后 , 又按照操作序列执行了 : $$\mathrm{rest\_opt}=M-\mathrm{times}*K$$ 次**交换** (注意 , $K$ 次交换称为**一次置换**) 那么对于大小 $\leq \rm times$ 的环而言 , 这 $\rm times$ 次置换已经足以让一个点跑回自己 : 这个环的上所有点的**答案**还是像刚才讨论的**无限置换**一样 . 那么大小 $> \rm times$ 的环呢 ? 观察环上的一个点 $x$ , 经过 $\rm times$ 轮置换后 , 它会到达的点是确定的 , 设为 $y$ . 而还要再从 $y$ 位置出发 , 又经过了 $\rm rest\_opt$ 次**交换** . 在这个过程中经过的**位置集合** , 就是点 $x$ 的答案 . 所以我们现在需要另外 $n$ 个 $\rm vector$ , 来统计剩下的 $\rm rest\_opt$ 次交换中 , 每一个点经过了哪些位置 . 不妨使用 $\mathrm{rest}_i$ 表示剩下的 $\rm rest\_opt$ 次交换中 , 点 $i$ 经过的位置 . 那么这样 , 可以 $\mathcal O(NK)$ 地求出每一个点的答案 . 不过这样有什么用 ? 还不如打暴力来得快 ... 别急 , 有一个很显然的优化方法 . 考虑已经求出了某个环上 $x$ 点的答案 , 那么对于他所指向的点 $\mathrm{next}_x$ , $\mathrm{next}_x$相比 $x$ 而言 , **不会遍历** $\mathrm{pass}_x$ , 以及 $\mathrm{rest}_y$ . (这里 $y$ 的意思和上面讨论时相同 , $x$ 虽然置换到了 $y$ , 但是并不能再从 $y$ 开始进行完整的**一轮置换**) . 但是 , 他多**遍历**了 $\mathrm{pass_y}$ , 和 $\mathrm{rest}_{\mathrm{next}_y}$ . 所以我们破环成链 ,复制一遍增添在末尾 , 使用**双指针**移动 , 并且用一个桶 $\mathrm {buf}$ 记录**每个位置**的经过次数 , 从 $0$ 变成 $1$ 时答案增加 , 从 $1$ 变成 $0$ 时答案减少 . 这样就可以显然 $\mathcal O(K)$ 地统计答案 , 因为一个 $\mathrm {pass}_i$ 只会被**加入贡献**一次 , **消除贡献**一次 . $\mathrm{rest}_i$ 同理 . ### 注意细节 由于本题题目较难 , 作者水平有限只能提供总体思路 , 实现细节方面只能提供一些参考 . 如下 : $1.$ 我在计算完 $\mathrm{pass}_i$ 集合后 , 会弹出**最后一个元素** . 因为最后一个元素就是 $\mathrm{next}_i$ , 在扫描 $\mathrm {pass}_{i}$ 和 $\mathrm{pass}_{\mathrm{next}_i} $ 时 , 可能会导致 $\mathrm{next}_i$ 被重复统计 . 所以需要弹出 . (没有被交换过的位置除外 , 不用弹出) **(不弹出也没事 , 只是不严谨 , 不会影响计数)** $2.$ 记得消除贡献要消除完整 , 从一个环里出来后 , 要把桶清空 . 不要使用 $\rm memset$ , 可能会超时呢 . **(实际上好像没有超时 ? 但是不推荐这样做 .)** $3.$ 特别注意 $\mathrm{times}=0$ 的情况 . 此时一轮置换都无法完成 . 我在代码中是使用 $\rm l \leq r$ 特判(因为如果为 $0$ , 必然出现双指针移动时时刻 $\rm l > r$) $4.$ 注意 $M\leq 10^{18}$ , 一些变量需要使用 $\rm long \ long$ 存储 . ### 代码部分 代码只有关键部分的注释 , 不推荐仔细浏览 (因为实现细节还是自己调为好) . 头文件 , 快读 , 快写就不放上来了 . 跑得挺快的 , 总时间 $1s$ 左右 . (废话 , $\mathcal O(N+K)$ 的算法还能慢 ?) ```cpp int n;LL times,m,k; int A[N],B[N],pos[N],nxt[N]; vector <int> pass[N],rest[N]; vector <int> cir[N];int cnt,len[N];bool vis[N]; int ans[N]; int buf[N],sum; void GetCir(int x) { int to=x; do { vis[to]=1; cir[cnt].push_back(to); to=nxt[to]; } while(to != x); len[cnt]=cir[cnt].size(); } void Solve(int now) { if((LL)len[now] <= times) { //Add Contribution for(int i=0;i<len[now];++i) { int x=cir[now][i]; for(int j=0;j<pass[x].size();++j) { int y=pass[x][j]; buf[y] == 0 ? sum++,buf[y]++ : buf[y]++; } } for(int i=0;i<len[now];++i) { int x=cir[now][i]; ans[x]=sum; } //Delete contribution for(int i=0;i<len[now];++i) { int x=cir[now][i]; for(int j=0;j<pass[x].size();++j) { int y=pass[x][j]; buf[y]=0; } } sum=0; return ; } for(int i=0;i<len[now]-1;++i) cir[now].push_back(cir[now][i]); //Previous Treatment , Add Contribution : l -> r int l=0,r=times-1; for(int i=l;i<=r;++i) { int x=cir[now][i]; for(int j=0;j<pass[x].size();++j) { int y=pass[x][j]; buf[y] == 0 ? sum++,buf[y]++ : buf[y]++; } } for(int i=0;i<rest[cir[now][r+1]].size();++i) { int y=rest[cir[now][r+1]][i]; buf[y] == 0 ? sum++,buf[y]++ : buf[y]++; } ans[cir[now][l]]=sum; //Now Let's Get Each Answer while(l < len[now]-1) { int x=cir[now][l]; if(l <= r) { for(int i=0;i<pass[x].size();++i) { int y=pass[x][i]; buf[y] == 1 ? sum--,buf[y]-- : buf[y]--; } } x=cir[now][r+1]; for(int i=0;i<rest[x].size();++i) { int y=rest[x][i]; buf[y] == 1 ? sum--,buf[y]-- : buf[y]--; } ++l,++r; x=cir[now][r]; if(l <= r) { for(int i=0;i<pass[x].size();++i) { int y=pass[x][i]; buf[y] == 0 ? sum++,buf[y]++ : buf[y]++; } } x=cir[now][r+1]; for(int i=0;i<rest[x].size();++i) { int y=rest[x][i]; buf[y] == 0 ? sum++,buf[y]++ : buf[y]++; } ans[cir[now][l]]=sum; } //Remember to Delete Contribution for(int i=l;i<=r;++i) { int x=cir[now][i]; for(int j=0;j<pass[x].size();++j) { int y=pass[x][j]; buf[y]=0; } } for(int i=0;i<rest[cir[now][r+1]].size();++i) { int y=rest[cir[now][r+1]][i]; buf[y]=0; } memset(buf,0,sizeof buf); sum=0; } int main() { n=read(),k=read(),m=read();times=m/k; for(int i=1;i<=k;++i) A[i]=read(),B[i]=read(); for(int i=1;i<=n;++i) pass[i].push_back(i),rest[i].push_back(i),pos[i]=i; for(LL i=1;i<=k;++i) { pass[pos[A[i]]].push_back(B[i]), pass[pos[B[i]]].push_back(A[i]); if(i <= m-k*times) rest[pos[A[i]]].push_back(B[i]), rest[pos[B[i]]].push_back(A[i]); swap(pos[A[i]],pos[B[i]]); } for(int i=1;i<=n;++i) { nxt[pos[i]]=i; if(pass[i].size() != 1) pass[i].pop_back(); } for(int i=1;i<=n;++i) if(!vis[i]) ++cnt,GetCir(i); for(int i=1;i<=cnt;++i) Solve(i); for(int i=1;i<=n;++i) Writes(ans[i]); return 0; } ```