[日记]LeetCode算法·十一——回溯③

文章介绍了LeetCode中三道Hard难度的题目,包括重新安排行程、N皇后和解数独,主要探讨了如何利用回溯算法、剪枝策略和数据结构(如哈希表)来解决问题。关键在于理解问题本质,选择合适的数据结构,以及适时进行剪枝以提高效率。
摘要由CSDN通过智能技术生成

1 重新安排行程

LeetCode:重新安排行程

一道Hard题,但做起来的感觉还是和普通的回溯题区别不大,唯一值得关注的是对于Hash的利用。

这里最值得关注的点在于排序性带来的剪枝,由于我们是利用map的有序性对终点站进行排序,所以在固定第一趟起点为JFK的情况下,所找到的第一条路线就是所需要的路线。

理论上可以利用回溯函数backTrack的bool返回值作为条件,但我这里采用了另一种判断方式。

class Solution {
public:
    vector<string> result;
    vector<string> path;
    //<起点,<终点,航班次数>>,即[起点]:<终点:航班数>
    //因为map是有序的,map代表着终点站的有序性,在必定JFK起飞的情况下
    //找到的第一条路线就是我们所需要的,不需要搜索全部路线
    unordered_map<string,map<string,int>> targets;

    void backTrack(vector<vector<string>>& tickets)
    {
        if(result.size()==tickets.size()+1)
            return;
        if(path.size()==tickets.size()+1)
        {
            result=path;
            return;
        }
        //result.back()是上一次的终点站==这一次的起点站
        //target会遍历所有以path.back()为起点的机票,target为终点-次数的映射
        for(pair<const string,int>& target:targets[path.back()])
        {
            //还有机票剩余
            if(target.second>0)
            {
                --target.second;
                path.push_back(target.first);
                backTrack(tickets);
                path.pop_back();
                ++target.second;
            } 
        }
    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for(const vector<string>& vec:tickets)
        {
            ++targets[vec[0]][vec[1]];
        }
        result.push_back("JFK");
        path.push_back("JFK");
        backTrack(tickets);
        return result;
    }
};

2 N皇后

LeetCode:N皇后

大名鼎鼎的DFS问题,事实上DFS、递归与回溯本就是密不可分的思想。
这里针对N皇后问题采用了每次回溯在一行内选择的办法,即for控制列,递归控制行

class Solution {
public:
    vector<vector<string>> result;

    bool isValid(vector<string>& chessboard,int i,int j)
    {
        int n=chessboard.size();
        int k=0;
        //同行和同列的不行
        for(k=0;k<n;++k)
        {
            if(chessboard[i][k]=='Q' || chessboard[k][j]=='Q')
                return false;
        }
        //对角线不行
        //左上角
        for(k=1;i-k>=0 && j-k>=0;k++)
        {
            if(chessboard[i-k][j-k]=='Q')
                return false;
        }
        //左下角
        for(k=1;i-k>=0 && j+k<n;k++)
        {
            if(chessboard[i-k][j+k]=='Q')
                return false;
        }
        //右上角
        for(k=1;i+k<n && j-k>=0;k++)
        {
            if(chessboard[i+k][j-k]=='Q')
                return false;
        }
        //右下角
        for(k=1;i+k<n && j+k<n;k++)
        {
            if(chessboard[i+k][j+k]=='Q')
                return false;
        }
        return true;
    }

    void bactTrack(int numQueenLeft,vector<string>& chessboard)
    {
        if(numQueenLeft==0)
        {
            result.push_back(chessboard);
            return;
        }
        int n=chessboard.size();
        //n皇后是一个组合问题,若是有解,必然是每行一个
        //因此规定每m层的回溯结果,必须将这一枚棋子放置于第m行
        int i_index=n-numQueenLeft;
        for(int j=0;j<n;j++)
        {
            if(isValid(chessboard,i_index,j))
            {
                chessboard[i_index][j]='Q';
                bactTrack(numQueenLeft-1,chessboard);
                chessboard[i_index][j]='.';
            }
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> chessboard(n,string(n,'.'));
        bactTrack(n,chessboard);
        return result;
    }
};

3 解数独

LeetCode:解数独

剪枝的重要性!!
树层剪枝的效率远高于树叶剪枝!应该在任何出问题的分支路口立刻回溯,来保证算法的效率!
最开始选择在所有棋盘能下的都下了再回溯,直接TimeOut,改成某个格子无有效填入就回溯,就能正常通过了。

class Solution {
public:
    //因为题目保证仅有一个解,因此在board上进行操作即可
    //使用bool判断,从而使得得到正确答案后不进行回溯
    int countSpace(vector<vector<char>>& board)
    {
        int count=0;
        for(int i=0;i<9;++i)
        {
            for(int j=0;j<9;++j)
            {
                if(board[i][j]=='.')
                    ++count;
            }
        }
        return count;
    }

    bool isValid(vector<vector<char>>& board,int i,int j,char num)
    {
        int k;
        //同行与同列
        for(k=0;k<9;k++)
        {
            if(board[k][j]==num || board[i][k]==num)
                return false;
        }
        //九宫格的坐标
        int block_i=i/3;
        int block_j=j/3;
        //九宫格内唯一
        for(int m=0;m<=2;++m)
        {
            for(int n=0;n<=2;++n)
            {
                if(board[block_i*3+m][block_j*3+n]==num)
                    return false;
            }
        }
        return true;
    }

    bool backTrack(int numSpaceLeft,vector<vector<char>>& board)
    {
        if(numSpaceLeft==0)
        {
            return true;
        }
        //行
        for(int i=0;i<9;++i)
        {
            //列
            for(int j=0;j<9;++j)
            {
                //有数字跳过
                if(board[i][j]=='.')
                {
                    //1-9逐个实验
                    for(int k=1;k<=9;k++)
                    {
                        char num=k+'0';
                        //有效才填入
                        if(isValid(board,i,j,num))
                        {
                            board[i][j]=num;
                            if(backTrack(numSpaceLeft-1,board))
                                return true;
                            board[i][j]='.';
                        }
                    }
                    //提前剪枝,这个地方9个数都不行就立刻返回false,回溯到上一刻
                    //如果到了i的循环之外,意味着你把整个棋盘都能填的都填了才意识到不对返回false
                    //太浪费了!一定要提前剪枝!
                    return false;
                }
            }
        }
        //这里return什么都无所谓,事实上一定不会走到这一步
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        int spaceCount=countSpace(board);
        backTrack(spaceCount,board);
    }
};

4 总结

这三道题都是Hard难度的题,但如果理清楚了思路,事实上依然是较为轻松可以解决的。
这三道题的重要性大致在于合理的数据结构选择、剪枝意识、排列与组合的选择、排列与全排列的区别以及递归和循环的内涵选择上。

感言一会儿在Hash里写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值