回溯法之符号三角形问题

问题描述

在这里插入图片描述

题目分析

对于符号三角形问题,用n元组x[1:n]表示符号三角形第一行的n个符号,由于我们只有两种符号——"+“或”-",所以取值是一个二值问题,如果取"+“我们就假设x[i]=1,如果取”-“我们就设x[i]=0。所以这显然是一个解空间为子集树的问题。我们不需要每次都遍历到树的叶节点,可以通过剪枝来节约时间。
明确了解空间以后,我们要确定的就是约束函数。由于”+“和”-“数目相同,且我们的符号三角形有n行,所以总共有n(n+1) / 2个符号,因此每个符号只能有n(n +1) / 4。所以n(n+1)/2如果为奇数的话我们就要淘汰掉这种情况。
我们用sum表示合理的符号三角形的数目。count来统计其中的”+“和”-",如果是"+"我们将count+1,否则将count+0,最后用count与n(n+1)/4比较即可。
在这里我还是想要说明几个细节:
1.回溯问题我们应该回溯的是什么?
我们应该回溯的是状态,这个状态指的是我们在做了选择之后发生改变的变量,简而言之 就是约束函数约束的变量。这道题里,我们做出了选择,p[1][t]=i,于是我们就会马上让count+i,因为i如果等于0.则为负号,count自然不用加。当i=1时,符号为正号,count+1。所以我们改变的是count,而却count也是约束函数约束到的变量,所以我们在回溯过程中应该将其恢复原来的状态
2.回溯问题有没有更直接清晰的模板?
之前我们说过的子集树或排列树的解空间的模板都是分情况的更细致的模板,但其实回溯法也有一个更直接清晰的模板:

int Backtrack(i)
{
	for(穷举所有能做的选择)
	{
		做选择
		回溯(Backtrack(i+1))
		撤销选择(约束函数约束的变量恢复原状态)
	}
}

还有一些实现上需要注意的地方,在代码里都有详细注释。

代码

#include <iostream>
using namespace std;
class Triangle
{
    friend int Compute(int);//计算符号三角形个数的函数
    private:
        void Backtrack(int i);
        int half,//符号三角形中符号数目的一半
            **p;//符号三角形矩阵
            n,//第一行符号个数
            count;//当前"+"的个数
        long sum;//符号三角形的符号个数(防止符号过多,声明为long类型) 
};
void Triangle::Backtrack(int t)
{
    //可行性约束
    if(t > n)
    {
        sum++;
    }
    //约束函数
    if(count > half || t*(t - 1) / 2 - count > half)
    //判断"+"和"-"是否都超过了一半
    {
        return;
    }
    for(int i = 0;i <= 1;i++)
    {
        //从上往下,最上面是第一行
        //一定要给一个初始值
        p[1][t] = i;
        count += i;
        for(int j = 2;j <= t;j++)
        {
            //这里可能有一些难推导,第t列的这个符号只能影响下面几行t列之前的符号
            //如果当前行为k,下面的行数为j,则t最多可以影响到的列数为t-(j-k)即为t-j+k
            //在一行中即为t-j+1
            p[j][t-j+1] = p[j-1][t-j+1] ^ p[j-1][t-j+2];
            count += p[i][t-j+1];//只能为0或者1
        }
        Backtrack(t + 1);
        //回溯
        //这里的回溯不需要将p[1][t]-=i,因为我们外层是有循环的,所以回到原地后下次循环会重新给p[1][t]一个数值的
        //在子集树中,我们只回溯最终的结果(如装载问题的最优容量和当前重量,这道题目的当前正号数目等),
        //并不会回溯选择的0或者1,因为在回溯回来的时候我们会改变它的
        //回溯的时候我们只找那些约束函数约束的变量来恢复到原状态,如本题的count
        for(int j = 2;j < = t;j++)
        {
            count -= p[j][t - j + 1];
        }
        count -= i;
    }
}
int Compute(int n)
{
    Triangle X;
    X.n = n;
    X.half = n(n+1) / 2;
    X.sum = 0;
    if(X.half % 2 == 1)
        return 0;
    X.half = X.half / 2;
    X.count = 0;
    //二维数组,记录符号三角形
    int **p = new int *[n + 1];
    for(int i = 0;i <= n;i++)
        p[i] = new int [n + 1];
    for(int i = 0;i <= n;i++)
    {
        for(int j = 0;ij<=n;j++)
            p[i][j] = 0;
    }
    X.p = p;
    X.Backtrack(1);
    return X.sum;
}

总结

计算可行性约束需要O(n)的时间(因为1行有n个元素,我们需要对这个n个元素进行依次搜索并回溯,直到i > n才能sum++),而我们的解空间为子集树,结点个数为2^n
所以我们的时间复杂度为O(n2^n)

以下是用回溯法解决符号三角形问题的C语言代码: ```c #include <stdio.h> #define MAX_N 10 int n; char triangle[MAX_N][MAX_N * 2 - 1]; char result[MAX_N][MAX_N * 2 - 1]; int check(int row, int col) { int i, j; // 检查当前位置是否已经填上符号 if (result[row][col] != ' ') { return 0; } // 检查当前符号是否已经在本行出现过 for (j = 0; j < col; j++) { if (result[row][j] == triangle[row][col]) { return 0; } } // 检查当前符号是否已经在本列出现过 for (i = 0; i < row; i++) { if (result[i][col] == triangle[row][col]) { return 0; } } // 检查当前符号是否与左上角和右上角的符号满足运算关系 if (row > 0 && col > 0) { switch (result[row - 1][col - 1]) { case '+': if (result[row][col] <= result[row - 1][col] || result[row][col] <= result[row][col - 1]) { return 0; } break; case '-': if (result[row][col] >= result[row - 1][col] || result[row][col] >= result[row][col - 1]) { return 0; } break; case '*': if (result[row][col - 1] == ' ' || result[row - 1][col] == ' ') { return 0; } if (result[row][col] <= result[row - 1][col] * result[row][col - 1] || result[row][col] <= result[row - 1][col] + result[row][col - 1]) { return 0; } break; case '/': if (result[row][col - 1] == ' ' || result[row - 1][col] == ' ' || result[row - 1][col] == 0 || result[row][col - 1] == 0) { return 0; } if (result[row][col] != result[row - 1][col] * result[row][col - 1] / result[row - 1][col] && result[row][col] != result[row - 1][col] + result[row][col - 1]) { return 0; } break; } } return 1; } void solve(int row, int col) { int i; if (row == n) { // 找到了符合要求的解,输出结果 for (i = 0; i < n; i++) { printf("%s\n", result[i]); } printf("\n"); return; } if (col == 2 * n - 1) { // 当前行已经填满,继续填下一行 solve(row + 1, 0); return; } if (triangle[row][col] != ' ') { // 当前位置已经有符号,直接尝试填下一个位置 solve(row, col + 1); return; } // 尝试填入所有可能的符号 result[row][col] = '+'; if (check(row, col)) { solve(row, col + 1); } result[row][col] = '-'; if (check(row, col)) { solve(row, col + 1); } result[row][col] = '*'; if (check(row, col)) { solve(row, col + 1); } result[row][col] = '/'; if (check(row, col)) { solve(row, col + 1); } result[row][col] = ' '; } int main() { int i; scanf("%d", &n); for (i = 0; i < n; i++) { scanf("%s", triangle[i]); } // 初始化结果矩阵 for (i = 0; i < n; i++) { int j; for (j = 0; j < 2 * n - 1; j++) { result[i][j] = ' '; } } solve(0, 0); return 0; } ``` 该代码使用了递归函数 `solve` 来进行搜索,并使用一个 `check` 函数来判断当前位置填入的符号是否符合要求。在 `solve` 函数中,首先判断当前行和当前位置是否已经填满,然后尝试填入所有可能的符号,如果符合要求,则继续递归调用 `solve` 函数,否则回溯到上一个状态,继续尝试其他符号。在找到符合要求的解时,将结果输出即可。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值