问题描述
小华和小沈发明了一个简单的棋盘游戏,他们称之为“长城游戏”。这个游戏需要一个n*n的网格和n颗石子。这些石子随机地放在网格的方格之中,一个格子中最多放一颗石子。每一次移动,可以将任意一颗石子移动到上下左右相邻的空方格之中。游戏的目标是用最少的移动步数,使得n颗石子构成“一堵墙”——排成一条水平、竖直或斜的直线。
现在的问题是,小华和小沈不知道对于一个给定的初始棋盘,达到目标需要移动的最小步数。他们想要你写一个程序能够实现对于任意一个给定的初始状态,求出将所有石子排成一条直线所需要的最小步数。
输入格式
输入由多组数据组成。每组数据第一行包含一个整数n,1<=n<=15。接下来n行,每行包含两个数分别表示每颗石子所在的行和列。行和列的编号如上图。输入数据最后一行包含一个数0表示结束。
输出格式
对于每一组数据,输出数据的编号和将所有n颗石子排成一条直线所需的最小移动步数。按照样例中的输出格式输出。每组数据之后输出一个空行。
样例输入
5
1 2 2 4 3 4 5 1 5 3
2
1 1 1 2
3
3 1 1 2 2 2
0
样例输出
Board 1: 6 moves required.
Board 2: 0 moves required.
Board 3: 1 moves required.
题解
二分图:列举横竖斜线所有情况,把直线上的点当作y集,给你的点当作x集,x,y组成二分图,边的权值为x点到y点的曼哈顿距离。
求每种情况的最优匹配(KM
算法
)
package ADV;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
/**
* The Great Wall Game
* http://lx.lanqiao.cn/problem.page?gpid=T514
* AC
*/
public class ADV273 {
static int turn = 0;//每组数据轮次
static String[] s;
static int[] posX;//石子行号
static int[] posY;//石子列号
static int[][] edge;
static int[] Lx,Ly;//顶标
static boolean[] sx,sy; //记录寻找增广路时点集x,y里的点是否搜索过
static int[] match; //match[i]记录y[i]的匹配点编号
static int n;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
turn++;
s = in.readLine().split(" ");
n = Integer.valueOf(s[0]);
if(n==0)
break;
s = in.readLine().split(" ");
init();
int ans = 0x3f3f3f3f;
//枚举一列直线
for (int i = 1; i <= n; i++) {//第i列
for (int j = 0; j < n; j++) {//对每个石子
for (int k = 0; k < n; k++) {//对直线上的y集
add_Edge(j, k, -dis(posX[j],posY[j],i,k+1));
}
}
KM();
int sum = 0;
for(int u= 0;u < n;u++)
sum += -edge[match[u]][u];
ans = Math.min(ans,sum);
}
//枚举一行直线
for (int i = 1; i <= n; i++) {//第i行
for (int j = 0; j < n; j++) {//对每个石子
for (int k = 0; k < n; k++) {//对直线上的y集
add_Edge(j, k, -dis(posX[j],posY[j],k+1,i));
}
}
KM();
int sum = 0;
for(int u= 0;u < n;u++)
sum += -edge[match[u]][u];
ans = Math.min(ans,sum);
}
//枚举2条斜线
for (int j = 0; j < n; j++) {//对每个石子
for (int k = 0; k < n; k++) {//对直线上的y集
add_Edge(j, k, -dis(posX[j],posY[j],k+1,n-k));
}
}
KM();
int sum = 0;
for(int u= 0;u < n;u++)
sum += -edge[match[u]][u];
ans = Math.min(ans,sum);
for (int j = 0; j < n; j++) {//对每个石子
for (int k = 0; k < n; k++) {//对直线上的y集
add_Edge(j, k, -dis(posX[j],posY[j],k+1,k+1));
}
}
KM();
sum = 0;
for(int u= 0;u < n;u++)
sum += -edge[match[u]][u];
ans = Math.min(ans,sum);
System.out.println("Board "+turn+": "+ans+" moves required.\n");
}
}
static void add_Edge(int u, int v, int val) {
edge[u][v] = val;
}
static void KM() {
Arrays.fill(match, -1);
for(int i = 0; i < n; i++) {//顶标
Lx[i] = Ly[i] = 0;
for(int j = 0; j < n; j++)
Lx[i] = Math.max(Lx[i], edge[i][j]);
}
//不断修改顶标,直到找到完备匹配或完美匹配
for(int i = 0; i < n; i++) {//为x里的每一个点找匹配
while(true) {
Arrays.fill(sx, false);
Arrays.fill(sy, false);
if(match(i)) //x[i]在相等子图找到了匹配,继续为下一个点找匹配
break;
else
update(); //如果在相等子图里没有找到匹配,就修改顶标,直到找到匹配为止
}
}
}
static boolean match(int i){ //给x[u]找匹配,匈牙利匹配
sx[i] = true;
for(int j = 0; j < n; j++)
if (Lx[i]+Ly[j] == edge[i][j] && !sy[j]){
sy[j] = true;
if (match[j]==-1 || match(match[j])){//如果y集合j没有匹配或者为j的匹配match[j]换了一个对象
match[j] = i;
return true;
}
}
return false;
}
static void update(){
//匹配失败时首先找到修改顶标时的增量a=min(lx[i] + ly [i] - weight[i][j],a); a为最小的顶标改变量
//lx[i]为搜索过的x点,ly[i]是未搜索过的y点
//因为如果sx[i]与sy[j]都为真,则必然符合lx[i] + ly [j] =weight[i][j],a则为0没有改变
//所以只需要修改找的过程中搜索过的点,
int a = 0x3f3f3f3f;
for(int i = 0; i < n; i++)
if(sx[i])
for(int j = 0; j < n; j++)
if(!sy[j])
a = Math.min(a, Lx[i]+Ly[j] - edge[i][j]);
//找到增量后修改顶标
//将lx[i]减inc,ly[j]加inc不会改变等式
//但是如果sx[i]与sy[j]只有一个为真,lx[i] + ly [j] !=weight[i][j]
for(int i = 0; i < n; i++) {
if(sx[i])
Lx[i] -= a;
if(sy[i])
Ly[i] += a;
}
}
static void init() {
posX = new int[n];
posY = new int[n];
for (int i = 0; i < s.length; i+=2) {
posX[i/2] = Integer.valueOf(s[i]);
posY[i/2] = Integer.valueOf(s[i+1]);
}
edge = new int[n][n];
Lx = new int[n];
Ly = new int[n];
sx = new boolean[n];
sy = new boolean[n];
match = new int[n];
}
static int dis(int x1, int y1, int x2, int y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
}