彻头彻尾的理解回溯算法

定义

在程序设计中,有相当一类求一组解,或求全部解或求最优解的问题,例如读者熟悉的八皇后问题,不是根据某种特定的计算法则,而是利用试探和回溯的搜索技术求解。回溯法也是设计递归过程的一种重要方法,它的求解过程实质上是一个先序遍历一棵"状态树"的过程,只是这棵树不是遍历前预先建立的,而是隐含在遍历过程中。

---《数据结构》(严蔚敏)

怎么理解这段话呢?

首先,某种问题的解我们很难去找规律计算出来,没有公式可循,只能列出所有可能的解,然后一个个检查每个解是否符合我们要找的条件,也就是通常说的遍历。而解空间很多是树型的,就是树的遍历。

其次,树的先序遍历,也就是根是先被检查的,二叉树的先序遍历是根,左子树,右子树的顺序被输出。如果把树看做一种特殊的图的话,DFS就是先序遍历。所以,回溯和DFS是联系非常紧密的,可以认为回溯是DFS的一种应用场景。另外,DFS有个好处,它只存储深度,不存储广度。所以空间复杂度较小,而时间复杂度较大。

最后,某些解空间是非常大的,可以认为是一个非常庞大的树,此时完全遍历的时间复杂度是难以忍受的。此时可以在遍历的同时检查一些条件,当遍历某分支的时候,若发现条件不满足,则退回到根节点进入下一个分支的遍历。这就是“回溯”这个词的来源。而根据条件有选择的遍历,叫做剪枝或分枝定界。

DFS

首先看DFS,下面是算法导论上DFS的伪代码,值得一行行的去品味。需要注意染色的过程,因为图有可能是有环的,所以需要记录那些节点被访问过了,那些没有,而树的遍历是没有染色过程的。而且它用 π[m]来记录m的父节点,也就可以记录DFS时的路径。

DFS(G)
1  for each vertex u ∈ V [G]
2       do color[u] ← WHITE
3          π[u] ← NIL
4  time ← 0
5  for each vertex u ∈ V [G]
6       do if color[u] = WHITE
7             then DFS-VISIT(u)
DFS-VISIT(u)
1  color[u] ← GRAY
2  time ← time +1
3  d[u] <-time
4  for each v ∈ Adj[u]
5       do if color[v] = WHITE
6             then π[v] ← u
7                        DFS-VISIT(v)
8  color[u] <-BLACK

例子

例一求幂集问题,就是返回一个集合所有的子集。为什么叫幂集呢?因为一个集合有n个元素,那么它的所有的子集数是2^n个。比如[1,2,3]的子集是[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]。

也就是下面这棵树的叶子节点:


那问题就变成了如何输出一棵树的叶子节点。那就需要知道现在到底遍历到哪一层了。方法有很多,可以用全局变量记录,也可以用递归函数的参数记录。

A)这里是用全局变量记录,在进入函数的时候level++,退出函数的时候level--

int level=0;
vector<vector<int> > result;
vector<int> temp;
void dfs(vector<int>& S){
    level++;
    if(level>S.size()){
        result.push_back(temp);
        level--;
        return;
    }
    temp.push_back(S[level-1]);
    dfs(S);
    temp.pop_back();
    dfs(S);
    level--;
    return;
}
vector<vector<int> > subsets(vector<int>& S){
    sort(S.begin(),S.end());
    dfs(S);
    reverse(result.begin(),result.end());
    return result;
}

B)这里记录层数用的是函数参数

vector<vector<int> > result;
vector<int> temp;
void dfs(vector<int>& S, int i){
    if(i==S.size()){
        result.push_back(temp);
        return;
    }
    temp.push_back(S[i]);
    dfs(S,i+1);
    temp.pop_back();
    dfs(S,i+1);
    return;
}
vector<vector<int> > subsets(vector<int>& S){
    dfs(S,0);
    reverse(result.begin(),result.end());
    return result;
}

总结一下,伪代码就是:

void dfs(层数){

if(条件){

    输出;

}

else{

    左子树的处理;

    dfs(层数+1);

    右子树的处理;

    dfs(层数+1);

}

}

例二:皇后问题,比如8*8的棋盘,能摆放多少个皇后呢?国际象棋规则,皇后在同一行,同一列,同一斜线均可互相攻击。

伪代码如下:

int a[n];
void try(int i)
{
    if(i==n){
        输出结果;
         }
         else
         {
                   for(j = 下界; j <= 上界; j=j+1)  // 枚举i所有可能的路径
                   {
                            if(fun(j))                // 满足限界函数和约束条件
                            {
                                     a[i] = 1;
                                     ...                        // 其他操作
                                     try(i+1);
                                     a[j] = 0;
                            }
                   }
         }
 }

根据伪代码,写出最关键的一段代码如下。其中vector<vector<int> > m是全局变量,用来记录遍历轨迹,遍历前设上值,遍历后去掉。每一次调到output的时候,所有压入栈中的函数返回,都会调到m[level][i]=0;

void dfs(int level){  
    if(level==N){  
        output();  
    }  
    else{  
        for(int i=0;i<N;i++){  
            if(check(level+1,i+1)){  
                m[level][i]=1;  
                dfs(level+1);  
                m[level][i]=0;  
            }  
        }  
    }  
}  

完整代码:

int N;  
vector<vector<int> > m;  
vector<vector<string> > result;  
bool check(int row,int column){  
            if(row==1) return true;  
            int i,j;  
            for(i=0;i<=row-2;i++){  
                if(m[i][column-1]==1) return false;  
            }  
            i = row-2;  
            j = i-(row-column);  
            while(i>=0&&j>=0){  
                if(m[i][j]==1) return false;  
                i--;  
                j--;  
            }  
            i = row-2;  
            j = row+column-i-2;  
            while(i>=0&&j<=N-1){  
                if(m[i][j]==1) return false;  
                i--;  
                j++;  
            }  
            return true;  
        }  
void output()  
{  
    vector<string> vec;  
    for(int i=0;i<N;i++){  
        string s;  
        for(int j=0;j<N;j++){  
            if(m[i][j]==1)  
                s.push_back('Q');  
            else  
                s.push_back('.');  
        }  
        vec.push_back(s);  
    }  
    result.push_back(vec);  
}  
void dfs(int level){  
    if(level==N){  
        output();  
    }  
    else{  
        for(int i=0;i<N;i++){  
            if(check(level+1,i+1)){  
                m[level][i]=1;  
                dfs(level+1);  
                m[level][i]=0;  
            }  
        }  
    }  
}  
vector<vector<string> > solveNQueens(int n) {  
    N=n;  
    for(int i=0;i<n;i++){  
        vector<int> a(n,0);  
        m.push_back(a);  
    }  
    dfs(0);  
    return result;  
}

例三:数独问题,就是给出一个数独,解决它。

比如给出:


求解:


解空间是这样的:


由于数独都是9*9的,所以解空间有81层,每层有9个分支,我们做的就是遍历这个解空间。

如果只求一个解,那我们可以在得到解之后返回,而标记是否得到解可以用全局变量或返回值来做,

用全局变量的话,代码如下:

bool flag= false;
bool check(int k, vector<vector<char> > &board){
        int x=k/9;
        int y=k%9;
        for (int i = 0; i < 9; i++)
            if (i != x && board[i][y] == board[x][y])
                return false;
        for (int j = 0; j < 9; j++)
            if (j != y && board[x][j] == board[x][y])
                return false;
        for (int i = 3 * (x / 3); i < 3 * (x / 3 + 1); i++)
            for (int j = 3 * (y / 3); j < 3 * (y / 3 + 1); j++)
                if (i != x && j != y && board[i][j] == board[x][y])
                    return false;
        return true;
    }
void dfs(int num,vector<vector<char> > &board){
    if(num==81){
        flag=true;
        return;
    }
    else{
        int x=num/9;
        int y=num%9;
        if(board[x][y]=='.'){
            for(int i=1;i<=9;i++){
                board[x][y]=i+'0';
                if(check(num,board)){
                    dfs(num+1,board);
                    if(flag)
                        return;
                }
            }
            board[x][y]='.';
        }
        else{
            dfs(num+1,board);
        }
    }
}
void solveSudoku(vector<vector<char> > &board) {
    dfs(0,board);
}
用返回值的话,关键部分做一下修改就可以了:

 bool f(int i, vector<vector<char> > &board){
        if(i==n*m)
            return true;
        if(board[i/n][i%m]=='.'){
            for(int k=1;k<=9;k++){
                board[i/n][i%m]=k+'0';
                    if(check(i,board) && f(i+1,board))
                            return true;
            }
            board[i/n][i%m]='.';
            return false;
        }
        else
            return f(i+1,board);
    }

要求得到所有解的话,可以在解出现的时候存下来:

vector<vector<vector<char> >> sum;
bool check(int k, vector<vector<char> > &board){
        int x=k/9;
        int y=k%9;
        for (int i = 0; i < 9; i++)
            if (i != x && board[i][y] == board[x][y])
                return false;
        for (int j = 0; j < 9; j++)
            if (j != y && board[x][j] == board[x][y])
                return false;
        for (int i = 3 * (x / 3); i < 3 * (x / 3 + 1); i++)
            for (int j = 3 * (y / 3); j < 3 * (y / 3 + 1); j++)
                if (i != x && j != y && board[i][j] == board[x][y])
                    return false;
        return true;
    }
void dfs(int num,vector<vector<char> > &board){
    if(num==81){
        sum.push_back(board);
        return;
    }
    else{
        int x=num/9;
        int y=num%9;
        if(board[x][y]=='.'){
            for(int i=1;i<=9;i++){
                board[x][y]=i+'0';
                if(check(num,board)){
                    dfs(num+1,board);
                    //if(flag)
                      //  return;
                }
            }
            board[x][y]='.';
        }
        else{
            dfs(num+1,board);
        }
    }
}
void solveSudoku(vector<vector<char> > &board) {
    dfs(0,board);
}
int main()
{
    vector<string> myboard({"...748...","7........",".2.1.9...","..7...24.",".64.1.59.",".98...3..","...8.3.2.","........6","...2759.."});
    vector<char> temp(9,'.');
    vector<vector<char> > board(9,temp);
    for(int i=0;i<myboard.size();i++){
        for(int j=0;j<myboard[i].length();j++){
            board[i][j]=myboard[i][j];
        }
    }
    solveSudoku(board);
    for(int k=0;k<sum.size();k++){
    for(int i=0;i<sum[k].size();i++){
        for(int j=0;j<sum[k][i].size();j++){
            cout<<sum[k][i][j]<<" ";
        }
        cout<<endl;
    }
    cout<<"######"<<endl;
    }
    cout<<"sum is "<<sum.size()<<endl;
    cout << "Hello world!" << endl;
    return 0;
}

最终,我们得到了8个解。

wiki上有一张图片形象的表达了这个回溯的过程:



人,只要来到这个世上,就一定会感觉到失落。 我们的生活总是没有那么完美,所以如果你总是紧盯着别人的人生,和自己的反复对比,那么就一定会觉得自己过得不幸福。 古罗马哲学家塞涅卡说过:“我们总是活在别人的影子里,却忘了点亮自己的灯。” 活在别人的生活里太久,就会忘记掉热爱自己生活的方法。 所以,无论你觉得自己过得再怎么落魄,都不要去羡慕别人的生活。 毕竟,生活从来都没有好坏之分。 只要你相信自己并坚持热爱,就一定能把生活过的如诗一般美好。 一、别人的好,只是一种错觉 好像每个人都会有这样一种错觉,那就是别人的家庭永远那么幸福美满。 但事实上如果你觉得一个人的生活很幸福,那么一定是因为你不够了解他。 毕竟,很多时候,那些你觉得好只是一种表面上的光鲜亮丽罢了。 家家有本难念的经,每个人都有自己的苦难。 列夫·托尔斯泰就说过:“幸福的家庭都是相似的,不幸的家庭各有各的不幸。” 所以,真的没有那么多真正的幸福,每个人的人生其实都有自己要经历的苦难。 当你去羡慕别人的时候,别人其实内心并不觉得他是的生活是那么值得你去羡慕的。 他甚至也会被你生活上的表面的光线让你所吸引,也觉得你过得很好。 但其实你也知道你也有很多苦,有很多泪。 所以,对别人的人生我们真的没有必要窥探太多。 即便一个人表面上过的真的很成功,很富裕。 那么他背后必然也是付出多了足够多的辛苦才让他能够在台面上过得这么的耀眼。 总之,生活从来没有绝对的幸福与绝对的好。 更多的都是相对的,别人的好真的只是一种错觉罢了。 二、生活,需要自己热爱 哲学家梭罗曾说过:“大多人都生活在平静的绝望中。” 太多的人内心空虚,对于生活失去了激情。 所以他们才会总是去羡慕别人,觉得别人过得不好,还要拼命的去感叹自己的不幸。 但其实,人生的步调掌握在自己的手里,想要幸福就要自己去热爱生活。 因为,真实的生活里,我们就是会有悲伤的时刻,我们就是会有绝望的时刻。 没有人的人生是一帆风顺的,你想要没有痛苦的生活,那是绝对不可能的。 所以,若你想自渡,跨过这些苦难,那就不要放弃对生活的热爱。 阿尔贝·加缪说过:“在隆冬,我终于知道,我身上有一个不可战胜的夏天。” 越是在你觉得生活很绝望最无助的时候,你越是去热爱生活,你就会拥有对抗生活更强大的力量。 所以,对于人生不需要害怕那么多。 不管你遇到什么事情,你只需要坚持去热爱你自己的生活,不要有那么多的羡慕。 羡慕只会让你怀疑你自己的人生,让你变得弱小,并不能真的让你改变自己。 想要让自己的人生有个彻头彻尾的改变,还是要踏踏实实的过好自己的日子。 在通过自己热爱的那幅激情把自己的生活过成自己理想的样子,最终才会收获真正的幸福。 三、知足常乐,才是人生 人生想要幸福其实并不复杂,你只需要知足常乐,幸福就会来临。 古希腊哲学家伊壁鸠鲁就说过:“知足是最宝贵的财富。” 一个人越知足,他内心对于幸福的感知力就会越强。 因为,生活中那些在别人眼里不起眼的小细节,在他眼里都是命运的一种馈赠。 他不会轻易的感觉自己不幸,更不会在意自己拥有的多还是少。 他只会看着自己手里所拥有的东西感到满足,没有那么多欲望,没有那么多的杂念。 真正的做到了珍惜当下,珍惜眼前。 所以,知足的人永远幸福,永远不会去悲伤自己的人生。 作家林语堂写过:“幸福:一是睡在自家的床上;二是吃父母做的饭菜;三是听爱人给你说情话;四是跟孩子做游戏。” 总之,幸福不是拥有,它只是简简单单的存在于我们生活的当下。 打理好当下的生活,再踏实地走好脚下的路,幸福自然会离你越来越近。
最新发布
07-18
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值