用c++实现棋盘覆盖问题、最近对问题

6.3.2 棋盘覆盖问题

 【问题】 在一个由2^k×2^k(k>=0)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格。显然,特殊方格在棋盘中可能出现的位置有4^k种,因而有4^k种不同的棋盘,图6-9(a)所示是k=2时16种棋盘中的一个。棋盘覆盖问题(chess cover problem)要求用图6-9(b)所示的4种不同形状的L形骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何2个L形骨牌不得重叠覆盖。

【想法】 应用分治法求解棋盘覆盖问题的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆盖问题。k>0时,可将2^k×2^k的棋盘划分为4个2^(k-1)×2^(k-1)的子棋盘,如图6-10(a)所示。这样划分后,由于原棋盘只有一个特殊方格,所以,这4个子棋盘中只有一个子棋盘包含该特殊方格,其余3个子棋盘中没有特殊方格。为了将这3个没有特殊方格的子棋盘转化为特殊棋盘,以便采用递归方法求解,可以用一个L形骨牌覆盖这3个较小棋盘的会合处,如图6-10(b)所示,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种划分策略,直至将棋盘分割为1×1的子棋盘。

【算法】 下面讨论棋盘覆盖问题有关存储结构的设计,如图 6-11所示。
(1) 棋盘:可以用一个二维数组 board[size][size]表示一个棋盘,其中,size=2^k。为了在递归处理的过程中使用同一个棋盘,将数组 board设为全局变量。
(2) 子棋盘:整个棋盘用二维数组 board[size][size]表示,其中的子棋盘由棋盘左上角的下标tr、tc 和子棋盘大小s表示。
(3) 特殊方格:用 board[dr][dc]表示特殊方格, dr 和 dc 是该特殊方格在二维数组 board中的下标。
(4)L形骨牌:一个2^k×2^k的棋盘中有一个特殊方格,所以,用到上形骨牌的个数为(4^k-1)/3,将所有L形骨牌从1开始连续编号,用一个全局变量t表示。


【算法实现】 设全局变量已初始化为0,变量1表示本次覆盖所用L形骨牌的编号,程序如下。

#include <iostream>
using namespace std;

#define N 4
int t = 0;
int board[N][N] = {0};

void ChessBoard(int tr, int tc, int dr, int dc, int size);

int main( )
{
 int i, j, dr = 1, dc = 2;
    ChessBoard(0, 0, dr, dc, N);
    for (i = 0; i < N; i++)
    {
        for (j = 0; j < N; j++)
        {
            std::cout << board[i][j] << " ";
        }
        std::cout << std::endl;
    }

    return 0;

}

void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
int s, t1;
if (size == 1) return; //只有一个特殊方格
t1 = ++t; // L型骨牌编号
s = size/2; // 划分棋盘
if (dr < tr + s && dc < tc + s) //特殊方格在左上角子棋盘中
ChessBoard(tr, tc, dr, dc, s); //递归处理子棋盘
else{ //用 t1号骨牌覆盖右下角,再递归处理子棋盘
board[tr + s - 1][tc + s - 1] = t1;
ChessBoard(tr, tc, tr+s-1, tc+s-1, s);
}
if (dr < tr + s && dc >= tc + s) //特殊方格在右上角子棋盘中
ChessBoard(tr, tc+s, dr, dc, s); //递归处理子棋盘
else { //用 t1号骨牌覆盖左下角,再递归处理子棋盘
board[tr + s - 1][tc + s] = t1;
ChessBoard(tr, tc+s, tr+s-1, tc+s, s);
}
if (dr >= tr + s && dc < tc + s) //特殊方格在左下角子棋盘中
ChessBoard(tr+s, tc, dr, dc, s); //递归处理子棋盘
else { //用 t1号骨牌覆盖右上角,再递归处理子棋盘
board[tr + s][tc + s - 1] = t1;
ChessBoard(tr+s, tc, tr+s, tc+s-1, s);
}
if (dr >= tr + s && dc >= tc + s) //特殊方格在右下角子棋盘中
ChessBoard(tr+s, tc+s, dr, dc, s); //递归处理子棋盘
else { //用 t1号骨牌覆盖左上角,再递归处理子棋盘
board[tr + s][tc + s] = t1;
ChessBoard(tr+s, tc+s, tr+s, tc+s, s);
}
}

【算法分析】 设T(k)是算法 ChessBoard覆盖一个2^k×2^k 棋盘所需时间,从算法的划分策略可知,T(k)满足如下递推式:

解此递推式可得T(k)=O(4^k).


6.4.1 最近对问题


【问题】设S={(x1,yz), (x2,y2),.…,(xn,yn))是平面上n个点构成的集合最近对问题(nearest points problem)要求找出集合 S 中距离最近的点对。严格地讲,最接近点对可能多于一对,简单起见,只找出其中的一对即可。


应用 实例
        假设在一个金属片上钻n个大小一样的洞,如果洞的距离太近,金属片就可能断裂。可以通过任意两个洞的最小距离来估算金属断裂的概率。这种最小距离问题实际上就是最近点对问题。


【想法】 最近对问题的分治策略如下。
(1)划分:将集合S分成两个子集S1和S2。 根据平衡子问题原则,为了将平面上的点集 S分割为点的个数大致相同的两个子集,选取垂直线x=m作为分割线,其中,m为S 中各点x坐标的中位数。由此将 S分制为S1=(p∈S|xp <=m)和S2=(q∈S|xq>m)。设集合 S的最近点对是pi和pj(1<i, j<=n),则会出现以下三种情况:
①pi∈S1,pj∈S1,即最近点对均在集合S1中;

②pi∈S2,pj∈S2,即最近点对均在集合S2中;
③pi∈S1,pj∈S2即最近点对分别在集合S,1和S2中。
(2) 求解子问题:对于划分阶段的情况①和②可递归求解,得到S1中的最近距离 d1和 S2中的最近距离d2。如果最近点对分别在集合 S1和S2中,问题就比较复杂了。
(3)合并:比较在划分阶段三种情况下的最近点对,取三者之中的距离较小者为原问题的解。
下面讨论划分阶段的情况③。令d=min(d1, d2)。若S的最近对(p,q)之间的距离小于d,则p和q必分别属于S1和S2,不妨设p∈S1,q∈S2,则p和q距直线x=m距离均小于d, 所以,可以将求解限制在以r=m 为中心、宽度为2d 的垂直带P1 和P2中,垂直带之外的任何点对之间的距离都一定大于d,如图6-12所示。


        假设点p(x,y)是集合P1和P2中y坐标最小的点,则点p可能在P1中也可能在P2中,现在需要找出和点p之间的距离小于d的点。显然,这样点的y坐标一定位于区间[y,y+d],因为P1和P2中点的相互之间的距离至少为d, 可用鸽笼原理证明P1 和P2中最多各有4个点,如图6-13所示。所以,可以将P1和P2中的点按照y 坐标升序排列,顺序地处理P1和P2中的点p(x,y),在y坐标区间[y,y+d]内最多取出8个候选点,计算候选点和点p之间的距离。

【算法】 简单起见,假设点集S已按x坐标升序排列,算法如下。
算法:最近对问题 ClosestPoints
输人:按工坐标升序排列的n(n>=2)个点的集合S=((x1,y1),(x2,y2).…,(xn,yn))
输出:最近点对的距离
1.如果n等于2,则返回(x1,y1)和(x2,y2)之间的距离,算法结束;
2.划分:m=S中各点x坐标的中位数;
3.d1=计算((x1,y1),…,(xm,ym))的最近对距离;
4.d2=计算((xm,ym),…,(xn,yn))的最近对距离;
5.d=min{d1,d2};
6.依次考查集合S中的点p(x,y):
6.1如果(x<=xm并且x>=xm-d),则将点p放入集合P中;
6.2如果(x>xm并且x<=xm+d),则将点p放人集合P中;
7.将集合P按y坐标升序排列;
8.对集合P中的每个点p(x,y),在y坐标区间[y,y+d]内最多取出8个候选点,计算与点力的最近距离d3;
9.返回 min(d, ds);


算法分析】 应用分治法求解含有n个点的最近对问题,由于划分阶段的情况①和②可递归求解,情况③的时间代价是O(n),合并子问题解的时间代价是O(1),因此算法的时间复杂度可由下面的递推式表示:

使用扩展递归技术求解递推式,可得T(n)=0(nlog以2为底n)。


【算法实现】 设数组x[n]和y[n]表示n个点的坐标且已按x坐标升序排列,数组px[n]和py[n]存储集合 P1和 P2的坐标,函数 ClosestPoints 求最近对的距离,函QuickSort将集合中的点按y坐标升序排列,程序如下。

#include <iostream>
#include<stdio.h>
#include<stdlib.h>
#include<math.h> 
using namespace std;
double ClosestPoints(int x[], int y[], int low, int high);
int Partition(int px[], int py[], int first, int end);
void QuickSort(int px[], int py[], int first, int end);

int main() {
    int n = 5;
    int x[] = {1, 2, 3, 4, 5};
    int y[] = {1, 2, 3, 3, 2};

    double minDist = ClosestPoints(x, y, 0, n - 1);

    cout << "最短距离是: " << minDist << endl;

    return 0;
}

double ClosestPoints(int x[], int y[], int low, int high) {
    double d1, d2, d3, d;
    int mid, i, j, index, px[100], py[100];

    if (high - low == 1) //只有两个点
        return (x[high] - x[low]) * (x[high] - x[low]) + (y[high] - y[low]) * (y[high] - y[low]);

    mid = (low + high) / 2; //计算中间点

    d1 = ClosestPoints(x, y, low, mid); //递归求解子问题①

    d2 = ClosestPoints(x, y, mid, high); //递归求解子问题②

    if (d1 <= d2) d = d1; //以下为求解子问题③
    else d = d2;

    index = 0;
    for (i = mid; (i >= low) && (x[mid] - x[i] < d); i--) //建立点集合 P1
    {
        px[index] = x[i];
        py[index++] = y[i];
    }
    for (i = mid; (i <= high) && (x[i] - x[mid] < d); i++) //建立点集合 P2
    {
        px[index] = x[i];
        py[index++] = y[i];
    }

    QuickSort(px, py, 0, index - 1); //对集合 P 按 y 坐标升序排列

    for (i = 0; i < index; i++) //依次处理集合 P 中的点
        for (j = i + 1; j < index; j++)
            if (py[j] - py[i] >= d) //超出 y 坐标的范围
                break;
            else
            {
                d3 = (x[j] - x[i]) * (x[j] - x[i]) + (y[j] - y[i]) * (y[j] - y[i]);
                if (d3 < d) d = d3;
            }

    return sqrt(d);
}

void QuickSort(int px[], int py[], int first, int end) {
    if (first < end) {
        int pivot = Partition(px, py, first, end); //划分,pivot 是轴值的位置
        QuickSort(px, py, first, pivot - 1); //对左侧子序列进行快速排序
        QuickSort(px, py, pivot + 1, end); //对右侧子序列进行快速排序
    }
}

int Partition(int px[], int py[], int first, int end) {
    int temp, i = first, j = end;
    while (i < j) {
        while (i < j && py[i] <= py[j]) j--; //右侧扫描
        if (i < j) {
            temp = px[i];
            px[i] = px[j];
            px[j] = temp; //将较小记录交换到前面
            temp = py[i];
            py[i] = py[j];
            px[j] = temp;
            i++;
        }
        while (i < j && py[i] <= py[j]) i++; //左侧扫描
        if (i < j) {
            temp = px[i];
            px[i] = px[j];
            px[j] = temp; //将较大记录交换到后面
            temp = py[i];
            py[i] = py[j];
            px[j] = temp;
            j--;
        }
    }
    return i; //返回轴值记录的位置
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值