题解 CF1411C 【Peaceful Rooks】
题意简述
- 给定一个n*n的棋盘,然后有m个车分布在一些位置
- 移动一个车的时候不能移动到会被其他车吃掉的位置
- 目标是用最少的移动步数把m个车放在对角线上
solving
这道题看起来有点难度,我们不妨先模拟一下样例
3 1
2 3
只有一个车的话,只要把他移动到对角线上即可
3 2
2 1
1 2
考虑一下怎样移动最优,由于车的数量少于列的数量,我们可以试试用第三行当做缓冲行,先把第一列的车移到第三行,然后把第二列的车移到第二行,再把第三行的车移到第一行
我们可以发现在操作过程中并没有移动每个车的列号,可以想到如果左右移动改变了列,那么相当于多走了一步,似乎并不能更优(当然可能会出现移动列可以直接到对角线上的情况,不过我们先这么猜想,后面再回来证明)
有了这个猜想以后就好做很多,可以用一条边从它当前的行指向它需要去的行(即从它当前的行号位置指向它的列号的那个位置)
对于上面那个图,就构成了一个环,那么我们不能把1节点放到2位置去,因为2正占着2号位置,而2也不能移动,因此需要把1或2移到3位置,然后就只剩另一个节点了,这时候直接把那个节点归位,然后把移出去的节点放回来即可
于是我们有一个猜想:
- 对于形成环的情况,需要先断开一个节点,把这个节点移动到一个没有节点的列上(一定有,因为m<n),然后剩下的就是一条链
- 对于一条链,直接从链的头开始,移动到自己需要去的节点位置(由于那个位置被断开了或者为空,因此可以直接移动过去),然后指向链的头原来的位置的节点移动到链的头的原来的位置,依次处理下去就可以把整条链归位
对于一条链的情况,需要的移动步数就是链的长度。而对于一个环,需要的移动步数就是环的大小+1,这个+1是指某一个节点先要移到缓冲列上的这一步,剩下就是一样了
举例模拟:
5 4
2 3
3 1
1 2
4 5
按照刚才的方式,建立一个有向图:
这样相当于3要移到1位置,1要移到2位置,2要移到3位置,4要移到5位置(相当于就是从行向列发出了一条有向边)
那么这个时候3,1,2构成了一个环,如果我想把3移到1位置,就得把2移到3位置,就得把1移到2位置,就得把3移到1位置……永远都不能移动出来
因此可以按照刚才猜想的规律,随便取出一个节点放到5的位置去,例如拿出3,这样1,2就形成了一条链,2放到3原来的位置,1再放到2原来的位置就归位了,这时候1原来的位置自然也让出来了,让3移回去即可,总步数是节点数+1=4
然后对于4的情况,5因为是空的,因此这个链实际上只有4这一个节点,只需要把4移动过去即可,因此步数为1
所以总步数为5
证明
可行性:显然
最优性:
对于有向图中的一条链,需要移动的步数就是链的长度,因此每个需要移动的节点都只会移动一次,不会有比这个更优秀的方案了。
对于环来说,用我们的方法步数是环的长度+1,破坏这个环是必须的,因为我们不能通过一个环的整体移动得到归位。破坏这个环的最短步数就是1步,然后这个环里面每个节点按我们的方法都只会移动一次,因此对于这个破坏后的环来说最优步数就是环中节点个数,而破坏一个环最优步数是1,因此总共的最优步数仍然是环中节点个数+1
因此我们证明了这个方法的可行性与最优性
实现
可以使用tarjan算法查找环,相当于把这个图分成许多个强连通分量,那么对于节点个数大于1的强连通分量,相当于就是一个环,这时候就把答案加上节点个数+1,对于其他等于1的强连通分量,对答案的贡献就只有1了。
#include<iostream>
#include<cstdio>
#define N 100010
int t,n,m,s,k,hd[N],nxt[N],to[N],st[N],top,low[N],dfn[N],inst[N],vis[N],ans;
void newedge(int u,int v){
nxt[++k]=hd[u];
to[k]=v;
hd[u]=k;
return;
}void dfs(int x){
dfn[x]=low[x]=++s;
st[top++]=x;
inst[x]=1;
for(int i=hd[x];i;i=nxt[i]){
int nex=to[i];
if(!vis[nex])continue;//vis记录了有车的节点,有的节点上面没有车就不要走
if(!dfn[nex]){
dfs(nex);
low[x]=std::min(low[x],low[nex]);
}else if(inst[nex])low[x]=std::min(low[x],dfn[nex]);
}if(low[x]==dfn[x]){
int cnt=0;
do{
cnt++;
inst[st[top-1]]=0;
}while(st[--top]!=x);
if(cnt<=1)ans+=1;//节点个数小于1,说明在链上,只能贡献1
else ans+=cnt+1;//是一个环,贡献节点个数+1
}return;
}
int main(){
scanf("%d",&t);
start:
if(t--==0)return 0;
scanf("%d%d",&n,&m);
top=s=k=ans=0;
for(int i=1;i<=n;i++)vis[i]=hd[i]=nxt[i]=to[i]=st[i]=low[i]=dfn[i]=inst[i]=0;
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
if(x==y)continue;//已经满足要求,不要加入图中
newedge(x,y);//从当前行号向列号建一条边,相当于目标是把行号变成列号
vis[x]=1;//把这个节点标记成有车的节点
}for(int i=1;i<=n;i++){
if(vis[i]&&!dfn[i]){//tarjan求强连通分量
dfs(i);
}
}printf("%d\n",ans);
goto start;
}