算法基础-->图论(BFS,DFS)

本篇博文将总结和图论里面很重要的BFS,DFS算法,关于最小生成树,最短路径等一些其他图相关算法在贪心和动态规划部分详细总结。

图的表示:

  • 邻接矩阵
    nn 的矩阵,有边是 1 ,无边是 0 n 表示结点个数。

  • 邻接表
    为每个点建立一个链表(数组)存放与之连接的点。

搜索:

  • BFS(BreadthFirstSearch) 广(宽)度优先
  • DFS(DepthFirstSearch) 深度优先

广度优先搜索

最简单、直接的图搜索算法

  • 从起点开始层层扩展
    第一层是离起点距离为1的
    第二层是离起点距离为2的
    …..

  • 本质就是按层(距离) 扩展,无回退

BFS分析

给定某起点 a ,将 a 放入缓冲区,开始搜索;
过程:假定某时刻缓冲区内结点为 abc ,则访问结点 a 的邻接点 a1,a2,ax ,同时,缓冲区变成 bc,a1,a2,ax ,为下一次访问做准备;

  • 辅助数据结构:队列
  • 先进先出
  • 从队尾入队,从队首出队
  • 只有队首元素可见

结点判重:
如果在扩展中发现某结点在前期已经访问过,则本次不再访问该结点;显然,第一次访问到该结点时,是访问次数最少的:最少、最短;

路径记录:

  • 一个结点可能扩展出多个结点:多后继 a1,a2,ax
  • 但是任意一个结点最多只可能有1个前驱(起始结点没有前驱):单前驱
  • 用结点数目等长的数组 pre[0N1]pre[i]=j i 个结点的前一个结点是 j

注:再次用到“存索引,不存数据本身”的思路。

BFS算法框架

辅助数据结构

  • 队列 q
  • 结点是第几次被访问到的 d[0N1] :简称步数
  • 结点的前驱 pre[0N1]

算法描述

  • 起点 start 入队 q
    记录步数 d[start]=0
    记录 start 的前驱 pre[start]=1

  • 如果队列 q 非空,则队首结点 x 出队,尝试扩展 x (也就是找出所有与结点 x 直接相连的结点),找到 x 的邻接点集合 y|(x,y)E 进行遍历。
    对每个新扩展结点 y 判重(查看是否已经访问过),并且检查是否已经到达终点。如果 y 是新结点并且不是终点,则入队 q
    同时,记录步数 d[y]=d[x]+1 ;前驱 pre[y]=x

  • 我们以下图为例,在一个图中 a 是起点, b 是终点,这里假设为四邻域,也即是图中任意点可沿着上下左右四个方向向外扩展(前提没有越界),并且扩展的顺序为顺时针(也即是上右下左),求起点到终点的最短路径,我们从 a 开始不断的向外层按顺时针做四邻域扩展,每一层的扩展都是按顺时针方向,那么个时候就应该用队列来存储(先进先出) 上一层访问的结点(上一次先访问上方向结点,那么下次出队列时,也是首先访问下一层的上方向结点),每扩展一层 step+1 同一层的结点 step 相等),因此这样到达终点的step肯定是最短的。


这里写图片描述
这里写图片描述

BFS算法的思考

隐式图:实践中,往往乍看不是关于图的问题,但如果是给定一个起始状态和一些规则,求解决方案的问题:往往可以根据这些规则,将各个状态(动态的)建立连接边,然后使用 BFS/DFS 框架,一步一步的在解空间中搜索。

树的层序遍历,是按照从根到结点的距离遍历,可以看做是图的 BFS 过程。
树的先序/后序/中序遍历,是从根搜索到树的叶子结点,然后回溯,可以看做是图的 DFS 过程。

对BFS的改进——双向BFS:

  • 从起点和终点分别走,直到相遇;
  • 将树形搜索结构变成纺锤形;
  • 经典 BFS 中,树形搜索结构若树的高度非常高时,叶子非常多(树的宽度大),而把一棵高度为 h 的树,用两个近似为 h/2 的树代替,宽度相对较小。

单词变换问题Word ladder

问题描述

给定字典和一个起点单词、一个终点单词,每次只能变换一个字母,问从起点单词是否可以到达终点单词?最短多少步?

如:

  • start= “hit”
  • end = “cog”
  • dict = [“hot”,”dot”,”dog”,”lot”,”log”]
  • “hit” -> “hot” -> “dot” -> “dog” -> “cog”

起点单词和终点单词不在字典里面。

问题分析

  1. 使用邻接表,建立单词间的联系

    单词为图的结点,若能够变换,则两个单词存在无向边;
    这里这样定义:若单词A和B只有1个字母不同,则(A-B)存在边;

    建图:
    ⑴ 预处理:对字典中的所有单词建立map、hash或者trie结构,利于后续的查找。
    ⑵ 对于某单词 w ,单词中的第 i 位记为 β ,则将 β 替换 为 [β+1,Z] ,查找新的串 nw 是否在字典中。如果在,将 (wnw) 添加到邻接表项 w nw 中(无向边)。
    ⑶ 循环处理第二步;
    ⑷ 若使用map,串在字典中的查找认为是 O(logN) 的,那么,整体时间复杂度为 O(Nlen13logN) ,即 O(NlogN) 。若使用 hash 或者 trie ,整体复杂度为 O(N)

  2. 从起始单词开始,广度优先搜索,看能否到达终点单词。若可以到达,则这条路径上的变化是最快的。

是否需要事先计算图本身?图可以记录路径

代码实现:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

//搜索cur的孩子结点,存入children中
void Extends(const string& cur, vector<string>& children, const set<string>& dict, const string& end, set<string>& visit)
{
    string child = cur;
    children.clear();
    int i;
    char c, t;
    for (i = 0; i < (int)cur.size(); i++)
    {
        t = child[i];
        for (c = 'a'; c != 'z'; c++)//每次循环都选择其中一个单词进行替换
        {
            if (c == t)
                continue;
            child[i] = c;
            if (((child == end) || (dict.find(child) != dict.end())) && (visit.find(child) == visit.end()))
                //child同时满足以下两个条件才会压入children
                //一:child等于终点单词或者字典中含有child
                //二:没有访问过该单词child
            {
                children.push_back(child);
                visit.insert(child);
            }
        }
        child[i] = t;//单词恢复到原始状态
    }
}

int CalcLadderLength(const string& start, const string& end, const set<string>& dict)
{
    //以下是广度优先遍历的典型步骤

    queue<string> q;//队列先进先出,队头进队尾出
    q.push(start);
    vector<string> children;//从当前结点可以扩展得到的新结点集合
    set<string> visit;
    int step = 0;
    string cur;
    while (!q.empty())
    {
        cur = q.front();//获取队尾元素
        q.pop();//队尾元素出队
        Extends(cur, children, dict, end, visit);//获取与cur结点直接相连的结点,存入children
        for (vector<string>::const_iterator it = children.begin(); it != children.end(); it++)//遍历所有与cur结点直接相连的结点
        {
            if (*it == end)//如果该元素和终点单词一致,那么返回步长即可
                return step;
            q.push(*it);//反之将其压入队列
        }
        step++;//步长加一
    }
    return 0;
}

int main()
{
    set<string> dict;
    dict.insert("hot");
    dict.insert("dot");
    dict.insert("dog");
    dict.insert("lot");
    dict.insert("log");
    string start = "hit";
    string end = "cog";
    cout << CalcLadderLength(start, end, dict)<<endl;
    return 0;
}

周围区域问题

问题描述

给定二维平面,格点处要么是 X ,要么是 O 。求出所有由 X 围成的区域。

找到这样的(多个)区域后,将所有的 O 翻转成 X 即可。

这里写图片描述

问题分析

我们把X理解为SOIL(土)结点,O理解为WATER(水)结点。

反向思索最简单:哪些 O 是应该保留的?

  • 从上下左右四个边界往里走,凡是能碰到的 O ,都是跟边界接壤的,应该保留(非内陆的water结点保留)。
  • 内陆的water结点直接填土。

可把上述任务理解为把内陆的water填土。
思路:

  • 对于每一个边界上的 O (边界water结点)作为起点,做若干次广度优先搜索,对于碰到的 O (water结点),标记为其他某字符 Y (Ocean结点,表示海水非内陆);
  • 最后遍历一遍整个地图,把所有的 Y (Ocean结点)恢复成 O (water),把所有现有的 O (原来的water结点)都改成 X (填土)。

代码实现:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;


//检查某个结点是否为water
bool IsWater(vector<vector<int>>& land, int M, int N, int i, int j)
{
    if ((i < 0 || i >= M) || (j < 0 || j >= N))//检查是否越界
        return false;
    return land[i][j] == WATER;
}

//从边缘的water结点开始广度优先遍历,依次搜索出非内陆的water结点
void Ocean(vector<vector<int>>& land, int M, int N, int i, int j)
{
    queue <pair<int, int>> q;
    q.push(make_pair(i, j));

    //广度优先搜索的方向,这里是四领域,四个方向(-1,0),(1,0),(0,-1),(0,1)
    int iDirect[] = { -1, 1, 0, 0 };
    int jDirect[] = { 0, 0, -1, 1 };
    int iCur, jCur;
    int k;
    while (!q.empty())
    {
        i = q.front().first;
        j = q.front().second;
        q.pop();
        for (k = 0; k < 4; k++)
        {
            iCur = i + iDirect[k];
            jCur = j + jDirect[k];
            if (!IsWater(land, M, N, iCur, jCur))//检查该结点是否为water结点
            {
                q.push(make_pair(iCur, jCur));
                land[iCur][jCur] = OCEAN;//所以非内陆的water结点全部变为Ocean
            }
        }
    }
}

void FillLake(vector<vector<int>>& land, int M, int N)
{
    int i, j;
    //不断查找边界为water的结点,并且将其代入Ocean方法中,通过广度优先搜索到所有的非内陆的water结点
    for (i = 0; i < M; i++)
    {
        if (land[i][0] == WATER)
            Ocean(land, M, N, i, 0);
        if (land[i][N - 1] == WATER)
            Ocean(land, M, N, i, N-1);
    }
    for (j = 1; j < N - 1; j++)
    {
        if (land[0][j] == WATER)
            Ocean(land, M, N, 0, j);
        if (land[M - 1][j] == WATER)
            Ocean(land, M, N, M - 1, j);
    }

    for (i = 0; i < M; i++)
    {
        for (j = 0; j < N; j++)
        {
            if (land[i][j] == OCEAN)//之前操作已经把所有非内陆的water结点变为Ocean,这时把Ocean结点变为water
                land[i][j] = WATER;
            else if (land[i][j] == WATER)//而还没有改变为Ocean结点的water结点肯定是内陆的water结点,直接填土
                land[i][j] = SOIL;
        }
    }
}

深度优先搜索DFS

相比广度优先搜索,深度优先搜索的应用远远多于广度优先搜索。也比 BFS 更为重要。

  • 选择一个方向进行递归的不断深入,“走到头”,然后回退(恢复原始状态),选择另外一个方向再次进行搜索递归(回溯的话就是求所有解,如果不回溯只是选择一个方向,并且沿着这个方向能够递归的不断深入直到满足条件,则是求一个解)。这样就把所有解空间遍历了一遍。这也是 DFS 的核心思想。(回溯思想) ( BFS 无回退。);总而言之一句话:深度搜索加回溯就是 DFS

    例如这段全排列核心代码:

    void Permutation(int* a, int size, int n)
    {
        if (n == size - 1){
            Print(a, size);
            return;
        }
        for (int i = n; i < size; i++){
            swap(a[i], a[n]);
            Permutation(a, size, n + 1);//选择其中一个方向进行递归深入
            swap(a[i], a[n]);//,当上一个方向走到头后,开始回溯。这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。
        }
    }
  • 我们以下图为例, a 点为起点, b 点为终点,从起点 a 有两条不同的路径可以到达终点 b ,第一条路径 a>A1>A2>A3>A4>A5>b ,第二条路径 a>A1>A6>A7>A8>A9>b ,那么 DFS 是如何搜索到这两条路径呢?图中粉红色箭头 a>>>b 表示在点 A1 处选择第一个可选方向 A1>A2 一直深入到 b ,然后绿色箭头 b>>>A1 表示在 A1 处沿着第一个可选方向深入到 b 后再回溯 A1 ,然后再选择 A1 的第二个可选方向 A1>A6 ,再选择 A6 的第一个可选方向 A6>A10 再递归深入,发现到不了 b ,此时再回溯到 A6 在选择第二个可选方向 A6>A7 递归深入到 b 。 其实每一个箭头都是一个方法,每个箭头都是一次递归。每次都可以回溯 到任意一点,然后看看该点有没有其他可选方向,如有选择该方向深入递归。然后再回溯再递归深入直到遍历了所有解空间。这种回溯可以搜索解空间里面的所有可能解。从图中的回溯方式可以看出 DFS 可能会用带堆栈 (后进先出,比如 A1,A2,A3,A4,A5,b 压入堆栈,那么在回溯时 b 先弹栈,其次是 A5,A4,A3,A2


这里写图片描述

  • 一般所谓“暴力枚举(因为把解空间全部遍历了一遍)”搜索都是指 DFS ,例如之前说的 Nsum 问题。
  • 一般使用堆栈,或者递归。

八皇后问题

问题描述

8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线 上,问有多少种解法。

这里写图片描述

问题分析

解法一:可以利用全排列知识解决:

显然任意一行有且仅有 1 个皇后,使用数组 queen[8] 表示第 i 行的皇后位于哪一列。已经确定了每一个皇后位于不同的行中,第一个皇后在第一行,第二个皇后在第二行,….,因此除去了行冲突的情况,剩下的就是确定每个皇后所在列的问题,因为 queue 数组内元素为 1,2,3,4,5,6,7,8 各不相同,其全排列得出的每一个结果中肯定无重复元素。故无需考虑两个皇后或者多个皇后在同一列的情况,剩下的就是去掉全排列结果中处于同一斜线的情况。

对于 12345678 这个字符串,调用全排列问题 的代码,并且加入分支限界的条件判断 是否相互攻击即可;这样就解决了八皇后问题。

这是一个非常重要的思想,务必掌握

实现代码:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

void Print(int* a, int size)
{
    for (int i = 0; i < size; i++)
    {
        cout << a[i] << " ";
    }
    cout << endl;
}

//判断是否有任意两个或多个结点在同一斜线
bool isValid(int *a, int size)
{
    for (int i = 0; i < size - 1; i++)
    {
        for (int j = i + 1; j < size; j++)
        {
            if (abs(j - i) == abs(a[j] - a[i]))
                return false;
        }
    }
    return true;
}

void Permutation(int* a, int size, int n,int& count)//变量使用地址符号才会改变变量值内容
{
    if (n == size - 1){
        if (isValid(a, size))
        {
            count += 1;
            Print(a, size);
            return;
        }
    }

    for (int i = n; i < size; i++){
        swap(a[i], a[n]);
        Permutation(a, size, n + 1,count);//已固定前n+1个数。
        swap(a[i], a[n]);//这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。
    }
}

int main()
{
    int a[] = { 0,1, 2, 3, 4, 5, 6, 7 };
    int count = 0;
    Permutation(a, sizeof(a) / sizeof(int), 0, count);//当前已经有0个数已经固定
    cout << "解的个数为 "<<count << endl;
    return 0;
}

解法二:深度优先搜索

深度优先搜索:将第 i 个皇后放置在第 j 列上,如果当前位置与其他皇后相互攻击,则剪枝掉该结点。

分析对角线:

  • 主对角线上 (ij) 为定值,并且不同的主对角线 (ij) 值不同。由此在主对角方向上,确定了 (ij) 就确定了这条主对角线。取值范围是 (N1)(ij)N1 ,从而: 0(ij+N1)2N2

  • 次对角线同理,只不过 (i+j) 为定值,取值范围是 0(i+j)2N2

  • 由此,如果确定了一个皇后的位置坐标 (i,j) ,我们计算 (ij) (i+j) 就能确定该皇后所在列,所在主次对角线。

  • 使用布尔数组 m1[02N2]m2[02N2] 记录皇后占据的对角线

上述数据结构与剪枝过程适用于 N 皇后问题。

代码实现:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

class CQueen
{
private:
    int m_nQueue;
    vector<bool> m_Colomn;//某皇后已占据的列
    vector<bool> m_MainDiagonal;//某皇后已占据的某条主对角线
    vector<bool> m_MinorDiagonal;//某皇后已占据的某条次对角线
    vector<vector<int>> m_Answer;//最终解
public:
    CQueen(int N) :m_nQueue(N)
    {
        m_Colomn.resize(N, false);
        m_MainDiagonal.resize(2 * N - 1, false);
        m_MinorDiagonal.resize(2 * N - 1, false);
    }
    void Queue()
    {
        int *path = new int[m_nQueue];//长度为m_nQueue的数组,path[0]表示第1个皇后所在列,path[1]表示第2个皇后所在列等等
        CalcNQueue(path, 0);
        delete[] path;
    }

private:
    // 将皇后(i,j)所在的列,主对角线和次对角线上的点全设为true,表示已被占用
    bool CanLay(int row, int col) const
    {
        return !m_Colomn[col] && !m_MinorDiagonal[row + col] && !m_MainDiagonal[m_nQueue - 1 + row - col];
    }

    void CalcNQueue(int* path, int row)//row表示第row个皇后在row行,接下来就是确定第row个皇后所在列的问题
    {
        if (row == m_nQueue)//row是从0开始计,所以当row==m_nQueue时,表面所有行的皇后所在列都已经确定
        {
            m_Answer.push_back(vector<int>(path, path + m_nQueue));//此时将结果存入m_Answer中即可
            return;
        }
        for (int col = 0; col < m_nQueue; col++)
        {
            if (CanLay(row, col))//确定[row,col]该点所在列,所在主次对角线是否已经被占。
            {
                path[row] = col;//如果没有被占用,即将第row个皇后放在该坐标上
                //然后将所在列,主次对角线上全部设为true,表示已被占用
                m_Colomn[col] = true;
                m_MinorDiagonal[row + col] = true;
                m_MainDiagonal[m_nQueue - 1 + row - col] = true;
                CalcNQueue(path, row + 1);//在这个皇后位置确定的前提下,也就是沿着这个方向递归的不断深入再去确定其他皇后的位置。
                //如果不执行下面代码则不回溯,那么只是找一个解

                //上面的深入到头开始不断回溯,恢复到当前步骤的初始状态,换其他的方向重新深入,然后深入到头再回溯
                //不断深入回溯再深入回溯,就可以找到所有解
                m_Colomn[col] = false;
                m_MinorDiagonal[row + col] = false;
                m_MainDiagonal[m_nQueue - 1 + row - col] = false;
            }
        }
    }
public:
    void Print() const
    {
        cout << "所有解的个数:" << (int)m_Answer.size() << endl;
        for (vector<vector<int>>::const_iterator it = m_Answer.begin(); it != m_Answer.end(); it++)
        {
            PrintOne(*it);
        }
    }
    void PrintOne(const vector<int>& v)const
    {
        for (vector<int>::const_iterator it = v.begin(); it != v.end(); it++)
        {
            cout << *it << " ";
        }
        cout << endl;
    }
};

int main()
{
    CQueen queue(8);
    queue.Queue();
    queue.Print();
    return 0;
}

数独Sudoku

问题描述

要求每行、每列、每个九宫内,都是 19 9 个数字。

这里写图片描述

问题分析

此题和上面的八皇后问题是一个套路,都是可以用深度优先搜索解决。若当前位置是空格,则尝试从 1 9 的所有数;如果对于 1 9 的某些数字,当前是合法的,则继续尝试下一个位置——调用自身即可。

这里写图片描述

代码实现:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

class CSudou
{
private:
    int m_chess[9][9];
    int m_result[9][9];
    bool m_bSolve;

public:
    CSudou(int chess[9][9])//构造函数,初始化
    {
        memcpy(m_chess, chess, sizeof(m_chess));
        m_bSolve = false;
    }

    bool isValid(int i, int j)
    {
        int t = m_chess[i][j];
        int k;
        for (k = 0; k < 9; k++)
        {
            if ((j != k) && (t == m_chess[i][k]))//查看坐标(i,j)所在行有无重复
                return false;
            if ((i != k) && (t == m_chess[k][j]))//查看坐标(i,j)所在列有无重复
                return false;
        }
        int iGrid = (i / 3) * 3;//坐标(i,j)所在九宫格左上角横坐标
        int jGrid = (j / 3) * 3;//坐标(i,j)所在九宫格左上角纵坐标
        int k1, k2;
        //查看坐标(i,j)所在九宫格有无重复元素
        for (k1 = iGrid; k1 < iGrid + 3; k1++)
        {
            for (k2 = jGrid; k2 < jGrid + 3; k2++)
            {
                if ((k2 == j) && (k1 == j))
                    continue;
                if (t == m_chess[k1][k2])
                    return false;
            }
        }
        return true;
    }

    bool Sudouku()
    {
        int i, j, k;
        for (i = 0; i < 9; i++)
        {
            for (j = 0; j < 9; j++)
            {
                if (m_chess[i][j] == 0)//只有为0时才填充
                {
                    for (k = 1; k < 10; k++)
                    {
                        m_chess[i][j] = k;//尝试填充为k
                        if (isValid(i, j) && Sudouku())//如果所在行所在列,九宫格无重复,并且在这个位置填充为k的前提下,
                        //就是沿着这个方向深入递归可以到达满足条件状态
                        {
                            if (!m_bSolve)
                                memcpy(m_result, m_chess, sizeof(m_chess));//把结果存入到m_result
                            m_bSolve = true;
                            return true;
                        }
                        m_chess[i][j] = 0;//如果不满足条件该坐标位置恢复为0,如果满足条件深入到头再回溯,恢复当前初始状态,
                        //另选其他方向进行递归深入
                    }
                    return false;
                }
            }
        }
        return true;//说明所有位置都有值了
    }
};

马踏棋盘

问题描述

给定 m×n 的棋盘,将棋子“马”放在任意位置上,按照走棋规则将“马”移动,要求每个方格只能进入一次,最终使得“马”走遍棋盘的所有位置。

这里写图片描述

问题分析

显然,如果从 A 点能够跳到 B 点,则从 B 点也能够跳到 A 点。所以,马的起始位置可以从任意一点开始,不妨从左上角开始。

若当前位置为 (i,j) ,则遍历 (i,j) 八邻域,如果邻域尚未经过,则跳转。

这里写图片描述

对这个“八邻域”的理解很重要。,在一些隐式图中,准确的找出几个邻域,然后进行深度或者广度搜索很重要。

显然此题可以用深度优先搜索解决。

实现代码:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

//本题中马处于任意位置都有八个方向可跳(暂不考虑越界和已跳的问题),下面八个坐标就是相对于当前坐标的八个方向
int iHorse[] = { -2, -2, -1, +1, +2, +2, +1, -1 };
int jHorse[] = { -1, +1, +2, +2, +1, -1, -2, -2 };

int m = 8;
int n = 9;


//考察是否越界并且是否为0,即是否跳到过该坐标
bool CanJump(const vector<vector<int>>& chese, int i, int j)
{
    if ((i < 0) || (i >= m) || (j < 0) || (j >= n))
        return false;
    return (chese[i][j] == 0);
}


bool jump(vector<vector<int>>& chese, int i, int j, int step)
{
    if (step == m*n)
        return true;
    int iCur, jCur;
    for (int k = 0; k < 8; k++)//八个方向可跳
    {
        iCur = i + iHorse[k];
        jCur = j + jHorse[k];

        if (CanJump(chese, iCur, jCur))//考察下一跳位置是否越界并且是否为0
        {
            chese[iCur][jCur] = step + 1;
            if (jump(chese, iCur, jCur, step + 1))//深入递归
                return true;
            chese[iCur][jCur] = 0;//上一个方向深入到头,则回溯到当前初始状态,另选方向继续深入
        }
    }
    return false;
}

所有括号匹配的字符串问题

问题描述

N 对括号能够得到的有效括号序列有哪些?
N=3 时,有效括号串共5个,分别为:

1: ()()()
2: ()(())
3: (())()
4: (()())
5: ((()))

问题分析

任何一个括号序列,都可以写成形式 A(B)

  • AB 都是若干括号对 形成的合法串(可为空串)
  • N=0 ,括号序列为空。
  • N=1 ,括号序列只能是 () 这一种。

算法描述: i[0,N1]

  • 计算 i 对括号的可行序列 A
  • 计算 Ni1 对括号的可行序列 B
  • 组合得到 A(B)

注:加上额外一对括号 () ,总括号共 N

实现代码:

#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;

void Unit(vector<string>& res, const vector<string>& prefix, const vector<string>& suffix)
{
    vector<string>::const_iterator ip, is;//两个迭代器,分别迭代prefix,suffix
    for (ip = prefix.begin(); ip != prefix.end(); ip++)
    {
        for (is = suffix.begin(); is != suffix.end(); is++)
        {
            res.push_back("");//压入
            string& r = res.back();//返回vector中最后一个元素
            r += "(";
            r += *ip;
            r += ")";
            r += *is;
        }
    }
    //返回的res=(prefix)suffix
}

vector<string> AllParentheses(int n)
{
    if (n == 0)
        return vector<string>(1, "");//大小为1,初值为空
    if (n == 1)
        return vector<string>(1, "()");//大小为1,初值为()
    vector<string> prefix, suffix, res;
    for (int i = 0; i < n; i++)
    {
        prefix = AllParentheses(i);//i对括号的可行情况,在这种情况下递归结果res每个字符串为i对括号
        suffix = AllParentheses(n - i - 1);//n-i-1对括号的可行情况,这里面还需要不断的递归,res每个字符串为n-i-1对括号
        Unit(res, prefix, suffix);
    }
    return res;
}

void Print(vector<string>  res)
{
    vector<string>::iterator ip;
    for (ip = res.begin(); ip != res.end(); ip++)
    {
        cout << ip->c_str();//注意这一步不能是cout<<*ip
        cout << endl;
    }
}

int main()
{
    vector<string>  res = AllParentheses(5);//有五对括号,这里面返回的res中每个字符串为5对括号
    Print(res);
}

如果只是计算可行括号串的数目,如何计算?事实上,数组 A[i] 表示长度为 i 的括号串的可行数目,即著名的Catalan 数。

Catalan数计算

void GetCatalan(int *pCatalan, int N)
{
    pCatalan[0] = 1;
    pCatalan[1] = 1;
    int i, j;
    int c=0;
    for (i = 2; i <= N; i++)
    {
        for (j = 0; j < i; j++)
        {
            c += pCatalan[j] * pCatalan[i - j - 1];
        }
        pCatalan[i] = c;
    }
}

最小平方划分

问题描述:

一个正整数可以由若干个正整数的平方和表示,求整数 201314 最小的平方划分数目

10 ,可以写成 12+32 ,或者 12+12+22+22 ,显然, 12+32 只需要划分为2个完全平方数的和,因此,应该返回 2

这种平方划分是一定存在的。如:将任意正整数 n 划分成 n 1 显然是一个合理划分(不一定最小)。

问题分析:

贪心并不能解决这类问题。贪心能找到平方划分数目,但却不一定是最小的。
12 ,第一次找到 9 ,继续计算 3 。最终得到 12=9+1+1+1 ,划分数目为 4 ;但 12=4+4+4 ,最小划分数目为 3

如果已经求出了 1n1 的所有数的最小划分,如何求数 n 的最小划分呢?
a+k2=n ,则在 a 的划分方案上加上数字 k ,即为 n 的划分方案。显然 k 小于 n (向下取整)

算法描述:

已知 1n1 的最小划分,求数 n 最小划分:

  • 1n1 的最小划分为数组 split[n] ,其中 split[i] 表示数i的最小划分数目。令 n 的最小划分数目为 x ,初始化为 n

  • k :遍历 1K ,令 t=nkk ,其中, K=n ,根号向下取整。其中 split[t] 已经计算过。
    split[t]+1<x ,则将 x 更新为 split[t]+1 。这样不断迭代更新下去, split[n] 肯定取最小。

  • x 即为 n 的最小划分。
    赋值 split[n]=x ,为计算更大的 n 做准备。

如何判断完全平方数:

给定正整数 n ,如果判断 n 是否是一个完全平方数?

通常在c++中, 调用系统函数 float sqrt(float) ,记返回值取整后为 a ,计算 a2 是否等于 n 。系统函数本身仍然需要浮点运算(如 Taylor 展式)

这里我们使用使用牛顿迭代法来判断一个数是否可以完全开方。

使用牛顿迭代法:
这里我们求数 a 是否可以完全开方。在任意点 x0 Taylor 展开到一阶:

这里写图片描述

得到 x 的迭代公式 x=12(x0+ax0) ,首先随机的选择一个初值 x0 ,计算出 x ,再令 x0=x ,再作迭代,一般若干次(5、6次)迭代即可获得比较好的近似值。

代码实现:

// 计算a的平方根的近似值
float GetSquare(float a)
{
    if (a < 1e-6)//负数或者0直接返回0
        return 0;

    float x = a / 2;//设置x0初值
    float t = a;
    while (fabs(x-t)>1e-6)//判断是否收敛
    {
        t = x;
        x = (x + a / x) / 2;
    }
    return x;
}

// 计算N平方划分最小数目
void SquareCut2(int N, int *pre, int *minCut)
{
    int n, k, K,t;
    for (n = 1; n <= N; n++)
    {
        K = GetSquare(n);//n的平方根取整
        if (K*K == n)
        {
            minCut[n] = 1;
            pre[n] = 0;
            continue;
        }
        minCut[n] = n;//默认分成n份
        pre[n] = n - 1;//n若分为n份,那么其前驱为n-1
        for (k = 1; k <= K; k++)
        {
            t = n - k*k;
            if (minCut[n] > minCut[t] + 1)
            {
                minCut[n] = minCut[t] + 1;
                pre[n] = t;//n的前驱为t
            }
        }
    }
}

void Print(int a, const int* pre)
{
    while (a!=0)
    {
        cout << a - pre[a] << '-' << GetSquare(a - pre[a]) << '\t';
        a = pre[a];
    }
    cout << endl;
}

int main()
{
    int N = 201314;
    int* pre = new int[N + 1];//0未用
    int* minCut = new int[N + 1];//0未用
    SquareCut2(N, pre, minCut);
    Print(N, pre);
    delete[] pre;
    delete[] minCut;
    return 0;
}

DFS BFS 总结

  • 对于较典型的 DFS BFS ,首先要确定题中给出了 “几邻域”,这个非常重要。

  • 通常涉及到 最短路径 问题,往往是一种广度优先搜索的应用。 BFS 一般用到队列,将开始结点压入队列,然后遍历该结点所有邻域,符合条件的邻域结点依次 压入队列。这是一层层的搜索。直到遇到满足终止条件的结点则退出。 BFS 无递归无回溯

  • 通常涉及到求多解 的题,往往需要使用深度优先搜索。因为 DFS 有回溯,这种回溯就能遍历解空间里面的所有可能解。 DFS 中,从起点开始,遍历它的所有邻域结点,沿着第一个方向递归深入,如果该邻域结点不满足题给条件 或者沿着这个方向深入过程中发现不满足题给条件或者已经深入到头时,再回溯到当前的初始状态,再沿着第二个方向递归深入做同样操作,不断的递归再回溯,再递归在回溯。把所有可行解都搜一遍。

猜你喜欢

转载自blog.csdn.net/mr_tyting/article/details/77749494