图搜索算法

算法一:深度优先搜索

1.1、图的表示方法

1.1.1、图的表示方法 --- 邻接矩阵

用一个二维数组G存放图,G[i][j]表示节点i和j之间边的情况(如有无边,边方向,权值大小等)
遍历复杂度:O(n^2) n为节点数目

1.1.2、 图的表示方法 --- 邻接表

每个节点V对应一个一维数据(vector),里面存放从V连出去的边,边的信息包括另一顶点,还可能包含边的权值等;


原图

使用邻接表的表示

遍历复杂度:O(n+e) n为节点数目,e为边数目;
在边稀疏的情况下,邻接表的效率比邻接矩阵效率高。

1.2、深度优先搜索思路

从起点出发,走过的点要做标记,发现有没有走过的点,就随意挑一个往前走,走不了就回退,此种路径搜索策略就称为“深度优先搜索”。所谓深度,就是以距离起点的步数来衡量的;按目的可以分为以下3种情况。

1.2.1、判断从V出发是否能走到终点

思路如下:

bool Dfs()
{
      if(v 为终点)
          return true;
      if(v 为旧点)
          return false;
      将v标记为旧点;
      对和v相邻的每个节点u {
          if(Dfs(U) == true)
              return true;
         }
        return false;
}

int main()
{
      将所有点都标记为新点
      设置起点
      设置终点
      cout<<Dfs(起点);
}

1.2.2、判断从V出发是否能走到终点,如果能,要记录路径

Node path[MAX_LEN];   //MAX_LEN取节点总数即可,用来记录路径
int  depth;
bool Dfs()
{
      if(v 为终点) {
          path[depth] = V;
          return true;
     }
      if(v 为旧点) {
          return false;
      }
      将v标记为旧点;
      path[depth] = V;
      depth++;
      对和v相邻的每个节点u {
          if(Dfs(U) == true)
              return true;
         }
        depth--;
        return false;
}

int main()
{
      将所有点都标记为新点
      设置起点
      设置终点
      depth = 0;
      if(Dfs(起点)) {
          for(int i = 0; i <= depth; I++)
                cout << path[i] << endl; 
      }
}

1.2.3、遍历图上所有节点

bool Dfs()
{
      if(v 为旧点)
          return;
      将v标记为旧点;
      对和v相邻的每个节点u {
          Dfs(U);
}
int main()
{
      将所有点都标记为新点
      while(在图中能找到的新点k)
              Dfs(k);
  }

1.3、深度优先搜索例题

/*
    右图是一个城堡的地形图 。请你编写一个程序,计算城堡一共有多少房间, 最大的房间有多大。 城堡被分割成m×n(m≤50,n≤50)个方块,每个方块可以有0~4面墙。
 
 解题思路:
 建模:
 可以将城堡的地形图转化成一个无向图,每个房间是图中的一个节点,相邻两个房间之间如果没有墙,则在房间之间有一条边;
 这样,求房间的个数就转换成了求图中有多少个极大连通子图;
 什么事极大连通子图:对于一个连通子图,往里头加任何一个图里的其他节点,就会变得不连通,那么这个连通子图就是极大连通子图;
 
 对每一个房间,深度优先搜索,从而给这个房间能够到达的所有位置标记。最后统计一共用了几种记号,以及每种记号的数量;
 
 */

int rawNum = 0, colNum = 0;
int rooms[60][60];
int visited[60][60];
int roomNum = 0, maxRoomArea = 0, roomArea = 0;

void Dfs(int i, int j)
{
    if(visited[i][j]) {
        return;
    }
    roomArea++;
    visited[i][j] = roomNum;
    if((rooms[i][j] & 1) == 0) {
        Dfs(i,j-1);
    }
    if((rooms[i][j] & 2) == 0) {
        Dfs(i-1,j);
    }
    if((rooms[i][j] & 4) == 0) {
        Dfs(i,j+1);
    }
    if((rooms[i][j] & 8) == 0) {
        Dfs(i+1,j);
    }
}

int main(int argc, const char * argv[]) {

    cin >> rawNum >> colNum;
    for(int i = 1; i <= rawNum; i++) {
        for(int j = 1; j <= colNum; j++) {
            cin >> rooms[i][j];
        }
    }
    
    memset(visited,0,sizeof(visited));
    for(int i = 1; i <= rawNum; i++) {
        for(int j = 1; j <= colNum; j++) {
            if(!visited[i][j]) {
                roomNum++;
                roomArea = 0;
                Dfs(i,j);
                maxRoomArea = max(roomArea, maxRoomArea);
            }
        }
    }
    cout << roomNum << endl;
    cout << maxRoomArea << endl;
    
    return 0;
}
/*
踩方格
题目描述:有一个方格矩阵,矩阵边界在无穷远处。我们做如下假设:
a.每走一步时,只能从当前方格移动一格,走到某个相邻的方格上; b. 走过的格子立即塌陷无法再走第二次;
c. 只能向北、东、西三个方向走;
请问:如果允许在方格矩阵上走n步(n<=20),共有多少种不同的方案。 2种走法只要有一步不一样,即被认为是不同的方案。


思路:
递归
从(i,j)出发,走n步的方案数,等于一下三项之和:
从(i+1,j)出发,走n-1步的方案数。前提:(i+1,j)还没走过;
从(i,j+1)出发,走n-1步的方案数。前提:(i,j+1)还没走过;
从(i,j-1)出发,走n-1步的方案数。前提:(i,j-1)还没走过;
*/

//int visited[30][50];
int ways(int i, int j, int n)
{
    if(n == 0) {
        return 1;
    }
    visited[i][j] = 1;
    int num = 0;
    if(visited[i][j-1] == 0) {
        num+= ways(i,j-1,n-1);
    }
    if(visited[i][j+1] == 0) {
        num+= ways(i,j+1,n-1);
    }
    if(visited[i+1][j] == 0) {
        num+= ways(i+1,j,n-1);
    }
    visited[i][j] = 0; //这里在尝试完从i, j出发后需要将其恢复为可以访问的状态,这样在尝试从其他点开始的时候,还是可以访问i, j点的。
    return num;
}


int main(int argc, const char * argv[]) {

    int n;
    cin >> n;
    memset(visited,0,sizeof(visited));
    
    cout << ways(0,25,n) << endl;
    return 0;
}
/*
ROADS
N个城市,编号1到N。城市间有R条单向道路。 每条道路连接两个城市,有长度和过路费两个属性。Bob只有K块钱,他想从城市1走到城市N。问最短共需要走多长的路。如果到不了N,输出-1

2<=N<=100
0<=K<=10000
1<=R<=10000 每条路的长度 L, 1 <= L <= 100每条路的过路费T , 0 <= T <= 100
 
 思路:
 从城市1开始遍历整个图,找到所有能到达N的走法,选最优的一个;这里参考深
*/
int K,N,R; //K:bob拥有的钱,N:城市数量,R:路的数量
struct Road {
    int d; //目的城市
    int l; //路的长度
    int t; //过路费
};

int minLen;
int totalCost;
int totalLen;
int visited[110];
vector<vector<Road> > G(100);

void dfs(int s)
{
    if(s == N) {
        minLen = min(minLen, totalLen);
        return;
    }
    for(int i = 0; i < G[s].size(); i++) {
        Road r = G[s][I];
        if(totalCost + r.t > K) {  //可行性剪枝
            continue;
        }
        if(totalLen + r.l > minLen) {  //最优性剪枝
            continue;
        }
        if(!visited[r.d]) {
            totalLen += r.l;
            totalCost += r.t;
            visited[r.d] = 1;
            dfs(r.d);
            totalLen -= r.l;
            totalCost -= r.t;
            visited[r.d] = 0;
        }
    }
}

int main()
{
    cin >> K >> N >> R;
    for(int i = 0; i < R; i++) {
        int n;
        Road r;
        cin >> n >> r.d >> r.l >> r.t;
        if(n != r.d) {
            G[n].push_back(r);
        }
    }
    memset(visited, 0, sizeof(visited));
    totalLen = 0;
    minLen = 1 << 30;
    totalCost = 0;
    visited[1] = 1;
    dfs(1);
    if(minLen < (1 << 30)) {
        cout << minLen << endl;
    }
    else {
        cout << "-1" << endl;
    }
    return 0;
}

算法二:广度优先搜索

1.1、算法思路

广度优先搜索算法如下:(用QUEUE)

(1) 把初始节点S0放入Open表中;
(2) 如果Open表为空,则问题无解,失败 退出;
(3) 把Open表的第一个节点取出放入 Closed表,并记该节点为n;
(4) 考察节点n是否为目标节点。若是,则 得到问题的解,成功退出;
(5) 若节点n不可扩展,则转第(2)步;
(6) 扩展节点n,将其不在Closed表和 Open表中的子节点(判重)放入Open表的尾 部,并为每一个子节点设置指向父节点的指针 (或记录节点的层次),然后转第(2)步。

广度优先算法流程图

2.2、广度优先搜索例题

/*
 八数码问题:
 有一个3*3的棋盘,其中有0-8共9个数字,0表示空格, 其他的数字可以和0交换位置。求由初始状态 到达目标状态的步数最少的解。
 8 2 3       1 2 3
 1 4 6  -->  4 5 6
 5 7 0       7 8 0
 
 广度优先搜索:
 1、用队列保存待扩展的节点
 2、从队列队首取出节点,扩展出的新节点放入队尾,直到队首出现目标节点,即问题的解
 3、关键问题:判重:新扩展出的节点如果和以前扩展出的节点相同,则这个新节点就不必再考虑;
 这里存在的问题是状态节点数据众多,如何存储?以及怎样才能较快的判断一个状态是否是重复?
 
 */

int goalStatus = 123456780; //目标状态
const int MAXS = 400000;
char result[MAXS]; //要输出的移动方案
struct Node {
    int status; //状态
    int father; //父节点指针,即myQueue的下标
    char move; //父节点到本节点的移动方式 u/d/r/l
    Node(int s,int f,char m):status(s), father(f),move(m) { } Node() { }
};
Node myQueue[MAXS]; //状态队列,状态总数362880
int qHead = 0; //队头指针
int qTail = 1; //队尾指针
char moves[] = "udrl"; //四种移动

//求从status经过 move 移动后得到的新状态。若移动不可行则返回-1
int NewStatus( int status, char move)
{
    char tmp[20];
    int zeroPos; //字符'0'的位置
    sprintf(tmp,"%09d",status); //需要保留前导0
    for( int i = 0;i < 9; ++ I )
    {
        if(tmp[i] == '0') {
            zeroPos = I;
            break;
        } //返回空格的位置
    }
    switch(move) {
        case 'u':
            if(zeroPos - 3 < 0 ) {
                return -1; //空格在第一行
            } else {
                tmp[zeroPos] = tmp[zeroPos - 3];
                tmp[zeroPos - 3] = '0';
                
            }
            break;
        case 'd':
            if(zeroPos + 3 > 8 ) {
                return -1; //空格在第三行
            } else {
                tmp[zeroPos] = tmp[zeroPos + 3];
                tmp[zeroPos + 3] = '0';
            }
            break;
        case 'l':
            if(zeroPos % 3 == 0 ) {
                return -1; //空格在第一行
            } else {
                tmp[zeroPos] = tmp[zeroPos - 1];
                tmp[zeroPos - 1] = '0';
            }
            break;
        case 'r':
            if(zeroPos % 3 == 2) {
                return -1; //空格在第三列
            } else {
                tmp[zeroPos] = tmp[zeroPos + 1];
                tmp[zeroPos + 1 ] = '0';
            }
            break;
    }
    return atoi(tmp);
}

//寻找从初始状态status到目标的路径,找不到则返回false
bool Bfs(int status)
{
    int newStatus;
    set<int> expanded;
    myQueue[qHead] = Node(status,-1,0);
    expanded.insert(status);
    while(qHead != qTail) { //队列不为空
        status = myQueue[qHead].status;
        if(status == goalStatus) //找到目标状态
            return true;
        for(int i = 0; i < 4; i ++ ) { //尝试4种移动
            newStatus = NewStatus(status,moves[I]);
            if(newStatus == -1)
                continue; //不可移,试下一种
            if(expanded.find(newStatus)!=expanded.end())
                continue; //已扩展过,试下一种
            expanded.insert(newStatus);
            myQueue[qTail++] = Node(newStatus,qHead,moves[i]); //新节点入队列
        }
        qHead ++;
    }
    return false;
}

int main()
{
    char line1[50];
    char line2[20];
    while( cin.getline(line1, 48)) {
        int i, j;
        for(i = 0, j = 0; line1[i]; i++) {
            if(line1[i] != ' ') {
                if(line1[i] == 'x') {
                    line2[j++] = '0';
                } else {
                    line2[j++] = line1[I];
                }
            }
        }
        line2[j] = 0;
        if(Bfs(atoi(line2))) {
            int moves = 0;
            int pos = qHead;
            do {
                result[moves++] = myQueue[pos].move;
                pos = myQueue[pos].father;
            } while(pos);
            for(int i = moves - 1; i >= 0; i--) {
                cout << result[I];
            }
        } else {
            cout << "unsolvable" << endl;
        }
    }
}

总结

  • 广度搜索一般用于状态表示比较简单、求最优策略的问题
  • 优点:是一种完备策略,即只要问题有解,它就一定可以找到解。并且,广度优先搜索找到的解,还一定是路径最短的解。
  • 缺点:盲目性较大,尤其是当目标点距初始节点较远时,将产生许多无用的节点,因此其搜索效率较低。需要保存所有扩展出的状态,占用的空间大。
  • 用队列存节点: FIFO
  • 深度搜索几乎可以用于任何问题
  • 只需要保存从起始状态到当前状态路径上的节点
  • 用栈存节点:LIFO
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352