算法进阶21-23

21搜索:数和图的遍历:
数和图最常见储存方式:使用一个邻接表保存边集。
无向图中边可以看做是双向边,树可以看作具有N-1条边的无向图。
边都存储在一个邻接表中,以head数组为表头,使用ver和edge数组分别存储边的终点和权值,使用next数组模拟链表指针。

wolf:树和图的深度优先遍历,树的DFS序,深度和重心:
   深度优先遍历,首先对于点x的多条分支,任意选一条边走下去,执行递归,直至回溯到点x后,再考虑其他的边。

代码:
void dfs(int x)
{
   v[x] = 1;//记录点x被访问过,v是visit的缩写;
   for(int i = head[x];i;i = next[i])
   {
         int y = ver[i];
         if(v[y])
         continue;// 点y已经被访问过了
         dfs(y);
    }
}

这段代码访问每个点和每个边恰好1次(如果是无向边,正反向各访问1次)
其中时间复杂度为 0(N+M),其中M是边数。

wolf:时间戳:
按照 优先遍历的的顺序,以每个节点第一次被访问(v【x】被赋值为1时),依次给N个节点的整数标记,该标记被称为时间戳,记为dfn。

wolf:树的DFS序:
   对树进行深搜,每个节点,递归后回溯前记录一次该点的编号,最后产生2N的节点序列称为树的DFS序。
   void  dfs(int x)
{
    a[++m] = x;// 记录a数组存储dfs序
    v[x] = 1;//记录点x被访问过
    for(int i = head[x];i;i = next[i])
    {
        int y = ver[i];
        if(v[y])
        continue;
        dfs(y);
    }
    a[++m] = x;
}

DFS序的特点: 每个节点x的编号在序列中恰好出现两次。设两次出现的位置L【x】R【x】,
那么闭区间【L【x】,R【x】】就是以x为根的子树的dfs序。

wolf:树的深度:
  树中 各个节点的深度是一种 自顶向下统计。已知根节点的深度为0.
若节点x的的深度为d【x】,他的子节点y的深度就是d【y】 = d【x】 + 1;
  
代码:void dfs(int x)
{
    v[x] = 1;
    for(int i = head[x];i;i = next[i])
    {
        int y = ver[i];
        if(v[y])
        continue;
        d[y] = d[x] + 1;//从父节点x到子节点y递推,计算深度;
        dfs(y);
    }
    
}

wolf:树的重心:(详细例题可参考poj1655)
  树的重心(质心)。对于一个n个节点的无根树,找到一个点,使得把树变为以该点为根的有根树
顾名思义,找一个节点当作根,用搜索找一下这个根最大子树的结点数是否最少即可。

网搜:性质:
性质
1树中所有点到某个点的距离和中,到重心的距离和是最小的,如果有两个重心,他们的距离和一样。
2一个点是重心,等价于,以这个点为根,它的每个子树的大小,都不会超过整个树大小的一半
3把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。
4一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。
5一棵树最多有两个重心,且相邻。

对于叶子节点,以他为根的子树大小为1.
若节点x有k个子节点y1~yk为根的子树是,size[y1],size[y2],size[y3]....size[yk].
那么以x为根的树的大小就是上面的总和+1;

代码:void   dfs(int x)
{
    v[x] = 1; size[x] = 1;
    int max_part = 0;
    for(int i = head[x];i;i = next[i])
    {
        int y  = ver[i];
        if(v[y])
        continue;
        dfs(y);
        size[x] += size[y];//从子节点向父节点递推
        max_part = max(max_part,size[y]);
    }
    max_part = max(max_part,n -size[x]); //n为整棵树的节点数目;
    if(max_part < ans)
    {
        ans = max_part;
        pos = x;
    }
}

wolf:图的连通块划分
   从x开始一次遍历,就会访问x能够到达的所有的点和边。通过多次深优先遍历,可以划分出一张无向图的各个连通块。
对于一个森林进行深度优先遍历,可以划分出森林中的每棵树。
cnt就是无向图包含的连通块的个数,v数组标记了每个点属于哪一个连通块。

注意:连通块:若在一个无向图的子图中,任意两个节点之间都存在一条路经(并且可以相互到达),并且这个子图不能再扩张,则称该子图为无向图的一个连通块
一张不连通的无向图由2个或者2个以上的连通块组成。
代码:
void dfs(int x)
{
    v[x] = cnt;
    for(int i = head[x];i;i = next[i])
    {
        int y = ver[i];
        if(v[y])
        continue;
        dfs(y);
    }
}
  for(int i =1;i <=n;i++)//在 int main()中
  {
      if(!=v[i])
      {
          cnt++;
          dfs(i);
      }
  }

wolf;树与图的广度优先遍历,拓扑排序:
  数和图的广度优先遍历需要使用一个队列来实现。
不断地从队头取出一个节点x,
对于x面对多条分支,把沿着每条分支到达下一个节点(如果尚未访问过)插入队尾。
重复执行上述过程直到队列为空。

有图:pdf97

代码:
void bfs()
{
    memset(d,0,sizeof(d));
    queue<int> q;
    q.push(1);
    d[1] = 1;
    while(q.size()>0)
    {
        int x = q.front();
        q,pop();
        for(int i =head[x];i;i = next[i])
        {
            int  y  = ver[i];
            if(d[y]) continue;
            d[y] = d[x] + 1;
            q.push(y);    
        }
    }
}
时间复杂度O(N + M)

对于一棵树而言,d【x】就是点x在树中的深度。对于一张图而言,d【x】
被称为点x的层次(从起点1走到点x需要经过的最小点数)

性质:
1.在访问完所有的第i层的节点后,才会开始访问第i+1层的节点。
2.任意时刻,队列中至多有两个层次的节点。其中一部分属于第i层,则另一部分属于第i+1层节点之间
广遍满足”两段性“和”单调性“;


wolf:拓扑排序:
入度和出度定义:在有向图中,以节点x为终点的有向边的条数被称为x的入度,以节点x为起点的有向边的条数被称为x的出度。
在无向图中,以x为端点的无向边的条数被称为x的度。

给定 一张有向无环图(在有向图中,从一个节点出发,最终回到他自身的路径被称为环。不存在环的有向图即为有向无环图),若一个由图中所有点构成的序列A满足:
对于图中每条边(x,y),x在A中都出现在y之前,则称A是该有向无环图顶点的一个拓扑序。
求解序列A的过程就称为拓扑排序。

拓扑排序的思想:
只需要不断选择图中入度为0的节点x,然后把x连向的点的入度减1。结合广度优先遍历框架
高效实现;
1建立空的拓扑序序列A;
2预处理出所有点的入度deg[i],起初把所有入度为0的 点入队。
3取出队头节点x,把x加入拓扑序列A的末尾。
4对于从x出发的每条边(x,y),把deg【y】减1.若被减为0,则把y入队。
5重复3-4步直到队列为空,此时A即为所求。

拓扑排序可以判定图中是否存在环。我们可以对任意有向图执行该操作。
若序列A的长度小于图中点的数量,则说明某些节点未被遍历,进而说明图中存在环。

void add(int x,int y)//在邻接表中添加一条有向边
{
    ver[++tot] = y,next[tot] = head[x],head[x] = tot;
    deg[y]++;
}
void topsort()
{
    queue<int> q;
    for(int i = 1;i <=n;i++)
        if(deg[i] == 0)
        q.push(i);
        while(q.size()) 
        {
            int x = q.front();q.pop();
            a[++cnt] = x;
            for(int i = head[x];i;i = next[i])
            {
                int y = ver[i];
                if(--deg[y]==0)
                q.push(y);
            }
        }
    }
    int main()
    {
        cin>>n>>m;//点数,边数
        for(int i = 1; i <=m;i++)
        {
            int x,y;
            scanf("%d%d",&n,&m);
            add(x,y);
        }
        topsort();
        for(int i =1;i <=cnt; i++)
        printf("%d ",a[i]);
        cout<<endl;
    }


22 深度优先搜索:(DFS)
递归实现的指数型,排列型,组合型枚举,是深搜的三种最简单形式。
例题(子集和问题、全排列问题、N皇后问题)
子集和问题:
子集和问题的一个实例为<S,c>。其中S={x1,x2,…,xn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,
使得S1中所有元素的和为c

用回溯算法解决问题的一般步骤为:
1、定义一个解空间,它包含问题的解。
2、利用适于 搜索的方法组织解空间。
3、利用深度优先法搜索解空间。
4、利用限界函数避免移动到不可能产生解的子空间。
问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性


回看问题,该问题有2^n中子集,用正常的for循环不可能找到问题的解,
所以只可能用递归实现,联系搜索算法,所以我们就想到了DFS。
既然是搜索,首先我们要画出来他的搜索树,根据搜索树方可得到代码
因为2 2 6 5 4 五个元素都需要访问,并且第一个元素不一定是最优解
所以我们在搜索的时候需要补充一个根节点0!
若不补充根节点0,则每次搜索都会包含第一个数字,导致结果可能错误!
下面是我画的搜索树,因为该问题不需要每次都把所有的子集用到。
举一个例子,本题是要搜索10,假如是5 8 9 6 4,当搜索到8的时候不满足条件
我们就可以直接跳到9,跳到9的前提是前面的数字不满足条件或者前面的数字已经用了
所以,我们就不需要再去尝试前面的数字,只需要尝试我们没有尝试过的数字。
所以该搜索树会的每一颗子树可能不等高。

代码:

#include<bits/stdc++.h>
using namespace std;
#define N 10010
int n, s;
//数组a[N]代表输入的子集,用sum判断是否与s相等,
//k用来计数已经存入数组中元素的个数
int a[N] = {0}, sum = 0, k = 0;
//用数组v来存放符合条件的子集
int v[N] = {0};
//flag判断是否已经找到满足条件的子集
bool flag = false;
void DFS(int x)
{
    //这两个语句必须放在这个位置,根据搜索树可以知道
    //要补加一个根节点0,若在for里面,根节点不起作用!
    sum += a[x];
    v[k++] = a[x];
    //剪枝操作
    if(sum > s)
        return;
    if(sum == s)
    {
        flag = true;
        return;
    }
    for(int i = x + 1; i <= n; i++)
    {
 
        DFS(i);
        //本次算法的核心!!!
        //必须是在这里进行判断,若没找到,则回溯
        //若找到了,则返回。
        if(!flag)
        {
            k--;
            sum -= a[i];
        }
        //如果找到了,立马返回,否则可能会输出冗杂内容
        else
            return ;
    }
}
int main()
{
    cin >> n >> s;
    int temp = 0;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        temp += a[i];
    }
    //这里进行判断,防止超时。
    if(temp < s)
    {
        cout << "No Solution!" << endl;
        return 0;
    }
    //从根节点0开始访问!
    DFS(0);
    if(flag)
    {
         for(int i = 1; i < k; i++)
         {
             printf("%d%c", v[i], i==k-1?'\n':' ');
         }
    }
    else
        cout <<"No Solution!"<<endl;
    return 0;
}


23剪枝(减小搜索树的规模,尽早排除搜索树中不必要的分支的一种手段)
1优化搜索顺序:
搜索树的各个层次,各个分支之间的顺序不是固定的,不同的搜索顺序会产生不同的搜索树形态,其规模大小相差甚远。
2排除等效冗余:
如果能够判定从搜索树当前节点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中一条分支进行搜索。
3可行性剪枝,搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。
4最优性剪枝
最优化搜索过程中,如果当前花费的代价已经超过当前搜到优解,此时停止对当前分支的搜索,执行回溯。
5记忆化
可以记录每个状态的搜索结果,在重复遍历一个状态的时候直接检索并返回。

例题,poj3076

24迭代加深:
深搜每次选一个分支,不断深入,直到达到递归边界才回溯。这种策略带有一定的缺陷。
若搜索树每个节点的分支数目非常多,并且问题的答案在某个较浅的节点上。
 如果一开始就选错了分支,就可能在不包含答案的深层子树上浪费许多时间。
 
此时,我们选择由小到大限制搜索的深度,如果当前深度限制下搜索不到答案,就把深度限制增加,重新进行一次搜索,
就是以上一次的结果为基础,重复执行以逼近答案的的意思。

总之,当搜索树的规模随着层次的深入增长很快,并且我们能够确保答案在一个较浅的层的节点时,可以采取迭代加深的深度优先搜索算法解决问题。

例题,POJ2248
   

wolf:双向搜索:
从初态和终态出发各搜索一半状态,产生两棵深度减半的搜索树,在中间交会、组合成最终的答案。

例题:送礼物 CH2401:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值