【问题描述】
在一个划分成网格的操场上,n个士兵散乱地站在网格点上。网格点由整数最表(x,y)表示。士兵可以沿着网格边上、下、左、右移动一步,但在同一时刻一个网格上只能有一名士兵。按照军官的命令,士兵们要整齐地列成一个水平队列,即排列成(x,y),(x+1,y),…,(x+n-1,y)。如何选择x,y的值,才能使士兵们以最少的总移动步数排成一列。
请计算使所有士兵排成一行需要的最少移动步数。
【输入格式】
第1行是士兵总数n。接下来的n行是士兵的初始位置,每行两个整数x和y。
【输出格式】
输出士兵排成一行需要的最少移动步数。
【输入样例】
5
1 2
2 2
1 3
3 -2
3 3
- 1
- 2
- 3
- 4
- 5
- 6
【输出样例】
8
- 1
【数据范围】
1<=n<=10000
-10000<=x,y<=10000
问题分析:
通过适当的移动顺序和移动路线可以使得同一时刻不会有两名士兵站在同一点。
题目要求移动的最少步数
题目要求可转化为求士兵站立的“最终位置”,即如何取“最终位置”使得士兵移动的步数最少
1. Y轴方向上的考虑(找出Y0的值)
设目标坐标为Y0,即n个士兵最终需要移动到的Y轴的坐标值为M
n个士兵的Y轴坐标分别为:
Y1,Y2 …… …… Yn
移动步数
S=|Y1-Y0|+|Y2-Y0|+ …… …… +|Yn-Y0|
结论:Y0取所有Yi的中间值时可以使得S达到最小。
(可以证明)
中位数是Y0;
解决办法:
1.对所有的Y轴坐标进行排序(O(nlogn))或者进行线性时间选择(O(n))然后取“中间”点的Y轴坐标值作为最佳位置Y0的值
2.通过公式求出Y轴方向上移动的最优步数
2. X轴方向上的考虑
(1)首先需要对所有士兵的X轴坐标值进行排序
(2)然后,按从左至右的顺序依次移动到每个士兵所对应的“最终位置”(最优),所移动的步数总和就是X轴方向上需要移动的步数
设排序后n个士兵在X轴坐标为:
X1’,X2’ …… …… Xn’
他们最终位置”的X轴坐标值为:
X0,X0+1,X0+2 …… …… X0+(n-1)
则所求移动的步数
S=|X1’-X0| + |X2’-(X0+1)|+ …… +|Xn’-(X0+(n-1)|
经过变形
S=|X1’–X0|+ |(X2’-1)–X0|+ …… +|(Xn’-(n-1))–X0|
注意到公式的形式与Y轴方向上的考虑一样,同样是n个已知数分别减去一个待定数后取绝对值,然后求和
问题转化为:
求出x1’,x2’-1,…Xn’-(n-1)的中位数,即求得X0值,最后算出最优解。
中位数寻找的快速算法
一般寻找中位数可以先将数组排序,按照次序将中间的数据作为中位数即可,其时间复杂度主要取决于排序算法的时间复杂度,利用快速排序可以将其控制为线性对数量级。
但是能否打破线性对数的限制呢?其中最关键的问题是,寻找中位数并不需要整个数组完全有序,如果把多余的元素排序省略掉,那么就可以超越线性对数的限制实现最快的算法。
启发来源于快速排序算法中的切分法,比如我们需要找到数组中第 k小的元素,首先将数组a[lo,hi]切分返回整数j,使得a[lo,j-1]都小于等于a[j],而a[j+1,hi]都大于等于a[j],如果j==k,那么j位置的元素就是我们要找的第k小的元素,而如果j>k,就要切分左子数组,如果j<k,就要切分右子数组,不断缩小选定的子数组的规模直到只剩下一个元素,则它就是最终我们要找的第k小的元素。
经过数学推导,这种快速切分法寻找中位数仅仅为线性量级,是寻找中位数最为快速的算法。
int partition(int *a, int low, int high)
//分治算法中的分
{ int X0ey = a[low];
while(low<high)
{ while(low < high && a[high] >= X0ey)
high--;
a[low] = a[high];
while(low<high && a[low] <= X0ey)
low++;
a[high]=a[low];
}
a[low]=X0ey;
return low;
}
// 找出数组中第k小的元素,非递归实现
int select1(int *a, int k,int n) {
int lo = 0;
int hi = n-1;
//找到最小的开始元素,以及终点元素;
while (hi > lo) {
int j = partition(a, lo, hi);//先是进行分的思想;返回每一轮中已经排好的数的位置;从0开始;
if(j == k){//如果已经排好的数的位置的数刚好是第k小的数;就直接返回,
return a[k];
}
else if(j > k) {//如果是已经排好数的位置坐标大于k,表示将从大的一边继续进行查找;
hi = j - 1;
}
else if(j < k) {//如果是已经排好数的位置坐标小于k,表示将从小的一边继续进行查找;
lo = j + 1;
}
}
return a[k];
}
// 找出数组中第
int select2(int a[], int k, int lo, int hi) {
int j = partition(a, lo, hi);
if (j == k) {
return a[k];
} else if (j > k) {
return select2(a, k, lo, j - 1);
} else {
return select2(a, k, j + 1, hi);
}
}
int main()
{
int a[10] = {2,1,3,4,7,9,8,10,5,11};
cout<<select1(a, 9,10)<<endl;
cout<<select2(a, 9,0,9)<<endl;
return 0;
}
题目代码:
这是没用上面的找中位数的方法写的,在poj上能过,不过时间稍稍有点长;
#include <iostream>
#include <cstring>
#include <cmath>
#include <ctime>
#include <cstdlib>
#define NUM 10001
using namespace std;
int com(const void *a,const void *b)
{
return *((int*)a)-*((int*)b);//从小到大
}
int main() {
int N,X[NUM],Y[NUM];//存放x和y坐标
cin>>N;//表示士兵数
for ( int i = 0; i<N;i++){
cin>>X[i]>>Y[i];
}//输入操作
//士兵移动x坐标或者是y坐标移动一个单位,在poj上提交可以不用这个功能;
for( int i = 0 ; i<N;i++){
int dirction =0+ rand()%(int)(1-0+1);//移动的坐标
int move = (-1)+rand()%(int)(1-(-1)+1);//移动方向
if(dirction == 0){
int x = X[i] + move;
int j = 0;
for(j = 0; j<N;j++){
if(x!=X[j]&&Y[i]!=Y[j])//有相同的坐标就不需移动
continue ;
else
break;
}
if(j == N)
X[i] = x;
}
//移动x坐标
else if(dirction == 1){
int y = Y[i] + move;
int j = 0;
for(j = 0; j<N;j++){
if(y!=Y[j] &&X[i]!=X[j])//有相同的坐标就不需移动
continue ;
else
break;
}
if(j == N)
Y[i] = y;
}//移动y坐标
}
qsort(Y, N, sizeof(int), com);
qsort(X, N, sizeof(int), com);
for(int i = 0;i<N;i++){
X[i] -=i;
}
qsort(X, N, sizeof(int), com);
int sum = 0;
for(int i = 0; i<N;i++){//移动的最小步数
sum +=abs(X[i]-X[N/2])+abs(Y[i]-Y[N/2]);
}
cout<<endl;
return 0;
}
关于用上面所讨论的寻找中位数的方法可以自己试试;