算法都是套路系列-搜索技术模板(3)

13 篇文章 3 订阅
12 篇文章 0 订阅

🌸折半搜索

将整个搜索过程分成两半,分别搜索,最后将两半的结果合并。由于搜索的复杂度往往是指数级的,而折半搜索可以使指数减半,也就能使复杂度开方。

🍂例子

有n 盏灯,每盏灯与若干盏灯相连,每盏灯上都有一个开关,如果按下一盏灯上的开关,这盏灯以及与之相连的所有灯的开关状态都会改变。一开始所有灯都是关着的,你需要将所有灯打开,求最小的按开关次数。

💥分析

如果这道题暴力 DFS 找开关灯的状态,时间复杂度就是 O(2^n), 显然超时。不过,如果我们用 meet-in-middle 的话,时间复杂度可以优化至 O(n2^(n/2))。 meet-in-middle 就是让我们先找一半的状态,也就是找出只使用编号为 1到mid 的开关能够到达的状态,再找出只使用另一半开关能到达的状态。如果前半段和后半段开启的灯互补,将这两段合并起来就得到了一种将所有灯打开的方案。具体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,每搜出一种方案,就把它与互补的第一段方案合并来更新答案。

c++

#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>

using namespace std;

typedef long long ll;

int n, m, ans = 0x7fffffff;
map<ll, int> f;
ll a[40];

int main() {
  cin >> n >> m;

  for (int i = 0; i < n; ++i) a[i] = (1ll << i);

  for (int i = 1; i <= m; ++i) {
    int u, v;
    cin >> u >> v;
    --u;
    --v;
    a[u] |= (1ll << v);
    a[v] |= (1ll << u);
  }

  for (int i = 0; i < (1 << (n / 2)); ++i) {
    ll t = 0;
    int cnt = 0;
    for (int j = 0; j < n / 2; ++j) {
      if ((i >> j) & 1) {
        t ^= a[j];
        ++cnt;
      }
    }
    if (!f.count(t))
      f[t] = cnt;
    else
      f[t] = min(f[t], cnt);
  }

  for (int i = 0; i < (1 << (n - n / 2)); ++i) {
    ll t = 0;
    int cnt = 0;
    for (int j = 0; j < (n - n / 2); ++j) {
      if ((i >> j) & 1) {
        t ^= a[n / 2 + j];
        ++cnt;
      }
    }
    if (f.count(((1ll << n) - 1) ^ t))
      ans = min(ans, cnt + f[((1ll << n) - 1) ^ t]);
  }

  cout << ans;

  return 0;
}

🌸启发式搜索(有信息搜索)

启发式搜索就是对取和不取都做分析,从中选取更优解(或删去无效解)

辅助信息所求解问题之外、与所求解问题相关的特定信息或知识
评价函数(evaluation function)f(n)从当前节点n出发,根据评价函数来选择后续节点
启发函数(heuristic function) h(n)计算从节点n到目标节点之间所形成路径的最小代价值。这里将两点之间的直线距离作为启发函数。

🍂例子

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 22 个整数 TT(1 \le T \le 10001≤T≤1000)和 MM(1 \le M \le 1001≤M≤100),用一个空格隔开,TT 代表总共能够用来采药的时间,MM 代表山洞里的草药的数目。

接下来的 MM 行每行包括两个在 11 到 100100 之间(包括 11 和 100100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

💥分析

我们写一个估价函数f ,可以剪掉所有无效的0 枝条(就是剪去大量无用不选枝条)。

估价函数f的运行过程如下:

我们在取的时候判断一下是不是超过了规定体积(可行性剪枝)。

在不取的时候判断一下不取这个时,剩下的药所有的价值 + 现有的价值是否大于目前找到的最优解(最优性剪枝)。

c++

#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 105;
int n, m, ans;
struct Node {
  int a, b;  // a 代表时间,b 代表价值
  double f;
} node[N];

bool operator<(Node p, Node q) { return p.f > q.f; }

int f(int t, int v) {
  int tot = 0;
  for (int i = 1; t + i <= n; i++)
    if (v >= node[t + i].a) {
      v -= node[t + i].a;
      tot += node[t + i].b;
    } else
      return (int)(tot + v * node[t + i].f);
  return tot;
}

void work(int t, int p, int v) {
  ans = max(ans, v);
  if (t > n) return;
  if (f(t, p) + v > ans) work(t + 1, p, v);
  if (node[t].a <= p) work(t + 1, p - node[t].a, v + node[t].b);
}

int main() {
  scanf("%d %d", &m, &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d %d", &node[i].a, &node[i].b);
    node[i].f = 1.0 * node[i].b / node[i].a;
  }
  sort(node + 1, node + n + 1);
  work(1, m, 0);
  printf("%d\n", ans);
  return 0;
}

🍂贪婪最佳优先搜索

评价函数f(n)=启发函数h(n)

  • 贪婪最佳优先搜索不是最优的。
  • 启发函数代价最小化这一目标会对错误的起点比较敏感。可能存在死循环路径。
  • 贪婪最佳优先搜索也是不完备的。所谓不完备即它可能沿着一条无限的路径走下去而不回来做其他的选择尝试,因此无法找到最佳路径这一答案。
  • 在最坏的情况下,贪婪最佳优先搜索的时间复杂度和空间复杂度都是O(b"),其中b是节点的分支因子数目、m是搜索空间的最大深度。
  • 因此,需要设计一个良好的启发函数

🍂A* 算法

定义评价函数:f(n)= g(n)+ h(n)

  • g(n)表示从起始节点到节点n的开销代价值,h(n)表示从节点n到目标节点路径中所估算的最小开销代价值。
  • f(n)可视为经过节点n、具有最小开销代价值的路径。
评估函数 = 当前最小开销代价 + 后续最小开销代价

为了保证A*算法是最优(optimal),需要启发函数h(n)是可容的(admissibleheuristic)和一致的(consistency,或者也称单调性,即monotonicity)

最优不存在另外一个解法能得到比A*算法所求得解法具有更小开销代价。
可容(admissible)专门针对启发函数而言,即启发函数不会过高估计(over-estimate)从节点n到目标结点之间的实际开销代价(即小于等于实际开销)。如可将两点之间的直线距离作为启发函数,从而保证其可容。
一致性(单调性)假设节点n的后续节点是n’,则从n到目标节点之间的开销代价一定小于从n到n’的开销再加上从n’到目标节点之间的开销,即h(n)≤c(n,a, n’)+ h(n’)。这里n’是n经过行动a所抵达的后续节点,c(n, a,n’)指n’和n之间的开销代价。

其实…… h = 0时就是 DFS算法, 并且h = 0边权为 1时就是 BFS

例如:起始位置是A,目标位置是P,字母后的数字表示节点的估价值

搜索过程中设置两个表:OPEN和CLOSED。OPEN表保存了所有已生成而未考察的节点,CLOSED表中记录已访问过的节点。算法中有一步是根据估价函数重排OPEN表。这样循环中的每一步只考虑OPEN表中状态最好的节点。具体搜索过程如下:

初始状态:                
 OPEN=[A5];CLOSED=[];
2)估算A5,取得搜有子节点,并放入OPEN表中;
 OPEN=[B4,C4,D6];CLOSED=[A5]
3)估算B4,取得搜有子节点,并放入OPEN表中;
 OPEN=[C4,E5,F5,D6];CLOSED=[B4,A5]
4)估算C4;取得搜有子节点,并放入OPEN表中;
 OPEN=[H3,G4,E5,F5,D6];CLOSED=[C4,B4,A5]
5)估算H3,取得搜有子节点,并放入OPEN表中;
 OPEN=[O2,P3,G4,E5,F5,D6];CLOSED=[H3,C4,B4,A5]
6)估算O2,取得搜有子节点,并放入OPEN表中;
 OPEN=[P3,G4,E5,F5,D6];CLOSED=[O2,H3,C4,B4,A5]
7)估算P3,已得到解;

看一下伪代码

Best_First_Search()
    {
     Open = [起始节点];
     Closed = [];
     while (Open表非空)
     {
      从Open中取得一个节点X,并从OPEN表中删除。
      if (X是目标节点)
      {
       求得路径PATH;
       返回路径PATH;
      }
      for (每一个X的子节点Y)
      {
       if (Y不在OPEN表和CLOSE表中)
       {
        求Y的估价值;
        并将Y插入OPEN表中;
       }
       //还没有排序
       else if (Y在OPEN表中)
       {
        if (Y的估价值小于OPEN表的估价值)
         更新OPEN表中的估价值;
       }
       else //Y在CLOSE表中
       {
        if (Y的估价值小于CLOSE表的估价值)
        {
         更新CLOSE表中的估价值;
         从CLOSE表中移出节点,并放入OPEN表中;
        }
       }
       将X节点插入CLOSE表中;
       按照估价值将OPEN表中的节点排序;
      }//end for
     }//end while
    }//end func
💥经典例子:八数码(我也不懂。。)

在 3 X3的棋盘上,摆有八个棋子,每个棋子上标有 1至8 的某一数字。棋盘中留有一个空格,空格用 0来表示。空格周围的棋子可以移到空格中,这样原来的位置就会变成空格。给出一种初始布局和目标布局(为了使题目简单,设目标状态如下),找到一种从初始布局到目标布局最少步骤的移动方法。

    123
    804
    765

h 函数可以定义为,不在应该在的位置的数字个数。

容易发现h 满足以上两个性质,此题可以使用 A*算法求解。

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <set>
using namespace std;
const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
int fx, fy;
char ch;
struct matrix {
  int a[5][5];
  bool operator<(matrix x) const {
    for (int i = 1; i <= 3; i++)
      for (int j = 1; j <= 3; j++)
        if (a[i][j] != x.a[i][j]) return a[i][j] < x.a[i][j];
    return false;
  }
} f, st;
int h(matrix a) {
  int ret = 0;
  for (int i = 1; i <= 3; i++)
    for (int j = 1; j <= 3; j++)
      if (a.a[i][j] != st.a[i][j]) ret++;
  return ret;
}
struct node {
  matrix a;
  int t;
  bool operator<(node x) const { return t + h(a) > x.t + h(x.a); }
} x;
priority_queue<node> q;
set<matrix> s;
int main() {
  st.a[1][1] = 1;
  st.a[1][2] = 2;
  st.a[1][3] = 3;
  st.a[2][1] = 8;
  st.a[2][2] = 0;
  st.a[2][3] = 4;
  st.a[3][1] = 7;
  st.a[3][2] = 6;
  st.a[3][3] = 5;
  for (int i = 1; i <= 3; i++)
    for (int j = 1; j <= 3; j++) {
      scanf(" %c", &ch);
      f.a[i][j] = ch - '0';
    }
  q.push({f, 0});
  while (!q.empty()) {
    x = q.top();
    q.pop();
    if (!h(x.a)) {
      printf("%d\n", x.t);
      return 0;
    }
    for (int i = 1; i <= 3; i++)
      for (int j = 1; j <= 3; j++)
        if (!x.a.a[i][j]) fx = i, fy = j;
    for (int i = 0; i < 4; i++) {
      int xx = fx + dx[i], yy = fy + dy[i];
      if (1 <= xx && xx <= 3 && 1 <= yy && yy <= 3) {
        swap(x.a.a[fx][fy], x.a.a[xx][yy]);
        if (!s.count(x.a)) s.insert(x.a), q.push({x.a, x.t + 1});
        swap(x.a.a[fx][fy], x.a.a[xx][yy]);
      }
    }
  }
  return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值