算法设计与分析——第五章回溯法 N后问题+旅行售货员问题+符号三角形

3、N后问题(子集树)

在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上
当且仅当 n = 1 或 n ≥ 4 时问题有解。
思路参考文章
代码参考文章

代码思路:
一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要当前列是否合法,如果合法,则将皇后放置在当前位置,并进行递归t+1,直到所有位置都不合法时,回溯到上一步,使上一步的皇后向右移。每行都摆满皇后时(t>n),则产生了一种解法,即可输出当前的解。
判断合法:
当前将要摆放‘Q’的位置和其他已摆放‘Q’的位置不能在同一列,且不能在同一条45度斜线或135度斜线上。这里判断是否在同一条斜线上可通过当前将要摆放‘Q’的位置和其他已摆放‘Q’的位置横坐标之差和纵坐标之差的绝对值是否相等来判断

  • a[i]=k表示第i行的皇后放在a[i]列,这样设计的棋盘所有皇后必定不在同一行。
  • 所以现在考虑不在一列的条件:由于a数组中记录的是皇后的所在列,假设现在运行到t行,我们让i从1到t-1遍历查找a[i]==a[t],如果有存在一个与a[t]相等,那么当前的摆放与之前处于同一列了。
  • 对角线则用|row-i|!=|a[row]-a[i]|(行差的绝对值≠列差的绝对值)。include <math.h>后用abs()可以求得正数的绝对值。
# include<bits/stdc++.h>

using namespace std;
int a[100]; //记录i行皇后的列号
int sum;//记录接解的个数
int n;
bool is_conflict(int t)//查找t行的a[t]与之前的列是否冲突
{
    for (int i = 1; i < t; i++ )
        if ( a[i]==a[t] || abs(t-i)==abs(a[t]-a[i]) )
            return false;
    return true;
}
void BackTrace(int t)
{
    if (t > n)     //求出一种解, count+1
    {
        sum++;
        //输出最后最终皇后的位置
        printf("第%d种方案:",sum);
        for(int i=1; i<=n; i++)
            cout<<a[i]<<" ";
        cout<<endl;
    }
    else
    {
        for (int i = 1; i <= n; i++)//遍历每一列
        {
            a[t] = i;
            if (is_conflict(t))//查找之前的列和当前的a[t]冲突
                BackTrace(t + 1);   
                //查找下一行,如有不符合的上一行右移
        }
    }
}
int main()
{

    printf("请输入皇后的个数n:");
    cin>>n;
    BackTrace(1);
    return 0;
}
/*
输入:
4
输出:
第1种方案:2 4 1 3
第2种方案:3 1 4 2
*/

4、旅行售货员问题(排序树)

问题: 某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程(或总旅费)最小

如下图:1,2,3,4 四个城市及其路线费用图,任意两个城市之间不一定都有路可达。
在这里插入图片描述
全排列的理解如下:
在这里插入图片描述
全排列的代码见文章链接
原代码的参考文章

代码的主要思想和全排列问题相同,只是添加了计算最优权值的问题

我们用一个二维数组g[100][100]定义这个无向有权图,如果两点之间没有边相连那么他们的权是0。通过x[i]记录全排列的结果,将全排列的相邻两点带入g则可得到这两点间的权。

同时添加了上界函数排除不是最优的解。和最优装载问题一样,如果发现得不到最优的结果,需要回溯到上一个点,同时减去当前的权,并将x的元素再交换回来以恢复之前的状态。

# include<bits/stdc++.h>
using namespace std;
int g[100][100];//无向图
int x[100],bestx[100],cw,bestw;
int n,m;//总点数、总边数
//第i步城市,最优路径,当前权重,最优权重
void Swap(int &a, int &b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}
void BackTrace(int t)
{
    if(t==n)
    {
        //若上一步能到达最后一点,最后一点和起点有边,且邮费更少则更新,要考虑第一次的bestw为0
        if(g[x[t - 1]][x[t]] !=0&&g[x[t]][1] != 0&&(cw + g[x[t - 1]][x[t]] + g[x[t]][1] < bestw||bestw == 0))
        {
            for (int i = 0; i <= n; i++)
                bestx[i] = x[i];
            bestw = cw + g[x[t - 1]][x[t]] + g[x[t]][1];
        }
        return;
    }

    //从t开始找下一个路径
    for (int i = t; i <= n; i++)//x[t]先和当前换再与后面的依次交换
    {
        //如果t-1和t之间有边,且有更小的邮费则往下找
        if (g[x[t - 1]][x[i]] != 0 && (cw + g[x[t - 1]][x[i]] < bestw|| bestw == 0))
        {
            Swap(x[t], x[i]);
            cw += g[x[t - 1]][x[t]];
            BackTrace(t + 1);
            cw -= g[x[t - 1]][x[t]];//恢复原态
            Swap(x[t], x[i]);
        }
    }
}

int main()
{
    printf("请输入要经过的城市数n,总边数m:");
    cin>>n>>m;
    printf("请输入点i到点j的边权重w:\n");
    for(int i=1; i<=m; i++)
    {
        int x,y,w;
        cin>>x>>y>>w;
        g[y][x]=g[x][y]=w;
    }
    for(int i=1; i<=n; i++) //初始化城市号
        x[i]=i;

    //已经确定了起点为城市1,从城市1-2开始
    BackTrace(2);
    printf("最小费用为:%d\n",bestw);
    printf("路径如下:");
    for(int i=1; i<=n; i++)
    {
        cout<<bestx[i]<<' ';
    }
    return 0;
}
/*
输入
4 6
1 2 30
1 3 6
1 4 4
2 3 5
2 4 10
3 4 20
输出:
最小费用为:25
路径如下:1 3 2 4
*/


5、符号三角形问题(子集树)

在这里插入图片描述

补充上图的几个规律:
(1)上图中三角形是居中对齐的,但在二维数组中的存储是左对齐的形式a[i][j]。
(2)当第一行有t个符号时,三角形有t行,且每行的符号数依次递减1个。第j行的列从t-j+1开始。

更多点击参考文章
“+”“+”相遇下面的为“+”、“+”“-”相遇下面的为“-”,“-”“-”相遇下面的为“+”,这个运算规则可以用异或来表示,相同的符号异或得0,不同的符号异或得1,刚好表示出“+”、“-”的两种状态。字符不容易表示所以用1来表示“-”,0表示“+”。

假设当前为a[1][t],要计算他下面所有的三角形,则需要从第j行到t行结束,对该行的每一列求解a[j][t-j+1]=a[j][t-j+1]=a[j-1][t-j+1]^a[j-1][t-j+2];(当前=正上异或右上)

对于我们的问题可以简化为求解所有的可能三角形,减去“+”≠“-”的三角形。也就是将“-”或“+”的数量>总符号数的一半的情况去掉。由于用1表示“-”,所以记录“-”的数量更加方便。由于我们只知道第一行的符号数n,所以利用等差数列求和公式得到总的符号数为n*(n+1)/2,一半half为n*(n-1)/4。

代码:

# include<bits/stdc++.h>
using namespace std;
int n,num1,sum;//n记录第一行的符号个数,num1记录减号的个数
int a[100][100];//记录符号表
int half;//half表示当前所有符号数的一半
void BackTrace(int t)
{
    if((num1>half)||(t*(t-1)/2-num1>half))
       return;
    if(t>n)//达到末尾,得出一种解法
    {
        printf("num1=%d,half=%d\n",num1,half);
        sum++;//解的数量
        for(int i=1;i<=n;i++)//打印符号三角形
        {
            for(int k=1;k<i;k++)
                cout<<" ";
             for(int j=1;j<=n;j++)
            {
                if(a[i][j]==0&&j<=n-i+1)
                    cout<<"+ ";
                else if(a[i][j]==1&&j<=n-i+1)
                    cout<<"- ";
                else
                    cout<<" ";
            }
            cout<<endl;
        }
        cout<<endl;
    }
    else
    {
        for(int i=0;i<=1;i++)//遍历第一行的所有可能
        {
            a[1][t]=i;
            num1+=i;
            for(int j=2;j<=t;j++)//从第2行到第t行遍历所有可能
            {
                a[j][t-j+1]=a[j-1][t-j+1]^a[j-1][t-j+2];
                num1+=a[j][t-j+1];//是1则加上,是0加上也不影响
            }
            BackTrace(t+1);//进入下一行
            for(int j=2;j<=t;j++)//回溯后num1要还原
                num1-=a[j][t-j+1];
            num1-=i;
        }
    }
}
int main()
{
    printf("请输入第一行的符号数n:");
    cin>>n;
    half=(n*(n+1))/4;
    BackTrace(1);
    cout<<sum;

    return 0;
}
/*
输入:
3
输出:
num1=3,half=3
+ + -
 + -
  -

num1=3,half=3
+ - +
 - -
  +

num1=3,half=3
- + +
 - +
  -

num1=3,half=3
- - -
 + +
  +

4
*/

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值