Acwing算法提高课—搜索

搜索

  • BFS
    • Flood Fill
      • AcWing 1097. 池塘计数
      • AcWing 1098. 城堡问题
      • AcWing 1106. 山峰和山谷
    • 最短路模型
      • AcWing 1076. 迷宫问题
      • AcWing 188. 武士风度的牛
      • AcWing 1100. 抓住那头牛
    • 多源BFS
      • AcWing 173. 矩阵距离
    • 最小步数模型
      • AcWing 1107. 魔板
    • 双端队列广搜
      • AcWing 175. 电路维修
    • 双向广搜
      • AcWing 190. 字串变换
    • A*
      • AcWing 178. 第K短路
      • AcWing 179. 八数码
  • DFS
    • DFS之连通性模型
      • AcWing 1112. 迷宫
      • AcWing 1113. 红与黑
    • DFS之搜索顺序
      • AcWing 1116. 马走日
      • AcWing 1117. 单词接龙
      • AcWing 1118. 分成互质组
    • DFS之剪枝与优化
      • AcWing 165. 小猫爬山
      • AcWing 166. 数独
      • AcWing 167. 木棒
      • AcWing 168. 生日蛋糕
    • 迭代加深
      • AcWing 170. 加成序列
    • 双向DFS
      • AcWing 171. 送礼物
    • IDA*
      • AcWing 180. 排书
      • AcWing 181. 回转游戏

BFS

Flood Fill

AcWing 1097. 池塘计数

/*
基础课回忆:
搜索:DFS BFS
BFS:
    最短距离(AcWing 844. 走迷宫):给一个地图(二维矩阵),定义起点终点,问从起点走到终点的最短距离
    最小步数(AcWing 845. 八数码):对地图本身操作,求其中一种状态(地图)变成另外一种地图的最小步数,把整个地图看成一个点
BFS特点:
    1.求最小
    2.基于迭代,不会爆栈(相对于DFS,当一个问题层数很深,但节点个数不深,优先选择BFS)
    
Flood Fill算法:可以在线性时间复杂度内,找到某个点所在的连通块

地图连通有两种:四连通,八连通
*/
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 1010, M = N * N;

int n, m;
char g[N][N];
PII q[M];//用Flood Fill算法遍历二维地图,一般都要存下标
bool st[N][N];//宽搜都需要判重,标记数组,防止重复遍历某个点

void bfs(int sx, int sy)
{
    int hh = 0, tt = 0;//数组模拟队列
    q[0] = {sx, sy};
    st[sx][sy] = true;

    while (hh <= tt)
    {
        PII t = q[hh ++ ];

        for (int i = t.x - 1; i <= t.x + 1; i ++ )//八连通,两重循环,中间点挖掉
            for (int j = t.y - 1; j <= t.y + 1; j ++ )
            {
                if (i == t.x && j == t.y) continue;
                if (i < 0 || i >= n || j < 0 || j >= m) continue;
                if (g[i][j] == '.' || st[i][j]) continue;

                q[ ++ tt] = {i, j};
                st[i][j] = true;
            }
    }
}

int main()
{
    cin>>n>>m;//读入数据较大,推荐用scanf读
    for (int i = 0; i < n; i ++ ) cin>>g[i];//读入每一行

    int cnt = 0;//池塘数目
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            if (g[i][j] == 'W' && !st[i][j])//宽搜判断条件一般都很长,想清楚
            {
                bfs(i, j);//输入起点
                cnt ++ ;
            }

    cout<

AcWing 1098. 城堡问题

/*
找到所有连通块:Flood Fill  
但是输入比较复杂,依次输入每个小方块周围墙的情况,可以用二进制编码表示,判断第0到3位哪一位为1
*/
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 55, M = N * N;

int n, m;
int g[N][N];
PII q[M];
bool st[N][N];

int bfs(int sx, int sy)
{
    int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};

    int hh = 0, tt = 0;
    int area = 0;

    q[0] = {sx, sy};
    st[sx][sy] = true;

    while (hh <= tt)
    {
        PII t = q[hh ++ ];
        area ++ ;

        for (int i = 0; i < 4; i ++ )
        {
        for(int i=0;i<4;i++){
            int a=t.x+dx[i], b=t.y+dy[i];
            if(a>=0&&a=0&&b>i&1)){
                q[++tt]={a,b};
                st[a][b]=true;
            }
            // if (a < 0 || a >= n || b < 0 || b >= m) continue;
            // if (st[a][b]) continue;
            // if (g[t.x][t.y] >> i & 1) continue;//二进制表示第i位是否为1

            // q[ ++ tt] = {a, b};
            // st[a][b] = true;
        }
        }
    }

    return area;
}

int main()
{
    cin >> n >> m;//输入较少,cin
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            cin >> g[i][j];

    int cnt = 0, area = 0;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            if (!st[i][j])
            {
                area = max(area, bfs(i, j));
                cnt ++ ;
            }

    cout << cnt << endl;
    cout << area << endl;

    return 0;
}

AcWing 1106. 山峰和山谷

/*
八连通,统计连通块与周围关系
*/
#include
#include
#include

using namespace std;

const int N=1010;

#define x first
#define y second
typedef pair PII;

int n;
int h[N][N];
PII q[N*N];
bool st[N][N];

void bfs(int sx,int sy,bool& has_higher,bool& has_lower){
    int hh=0,tt=0;
    q[0]={sx,sy};
    st[sx][sy]=true;
    
    while(hh<=tt){
        auto t=q[hh++];
        
        for(int i=t.x-1;i<=t.x+1;i++){
            for(int j=t.y-1;j<=t.y+1;j++){
                if(i==t.x&&j==t.y) continue;
                if(i<0||i>=n||j<0||j>=n) continue;
                if(h[i][j]!=h[t.x][t.y]){// 山脉的边界
                    if(h[i][j]>h[t.x][t.y]) has_higher=true;
                    else has_lower=true;
                }
                else if(!st[i][j]){
                    q[++tt]={i,j};
                    st[i][j]=true;
                }
            }
        }
    }
}

int main(){
    cin>>n;
    for(int i=0;i>h[i][j];
        }
    }
    
    int peak=0,valley=0;
    for(int i=0;i

最短路模型

AcWing 1076. 迷宫问题

/*
当所有边权重相等时,用宽搜从起点开始搜就可以得到所有点的单元最短路
走迷宫题目扩展,不止要求出最短路长度,还要求出方案路径
*/
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 1010, M = N * N;

int n;
int g[N][N];
PII q[M];
PII pre[N][N];//从st数组扩展而来,保存每一个格子从哪儿走过来的

void bfs(int sx, int sy)
{
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    int hh = 0, tt = 0;
    q[0] = {sx, sy};

    memset(pre, -1, sizeof pre);
    pre[sx][sy] = {0, 0};
    while (hh <= tt)
    {
        PII t = q[hh ++ ];

        for (int i = 0; i < 4; i ++ )
        {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= n) continue;
            if (g[a][b]) continue;
            if (pre[a][b].x != -1) continue;

            q[ ++ tt] = {a, b};
            pre[a][b] = t;
        }
    }
}

int main(){
    cin>>n;
    
    for(int i=0;i>g[i][j];
        }
    }
    
    bfs(n-1,n-1);//到这搜索是为了最后输出时正向输出
    
    PII end(0,0);
    
    while(true){
        cout<

AcWing 188. 武士风度的牛

/*
给定一个图,从起点走到终点,每条边权值为1,所以可以用BFS做,
走迷宫题目的简单扩展,走迷宫是只能走上下左右四个方向,这道题扩展成走日字型的八个方向

*/
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 155, M = N * N;

int n, m;
char g[N][N];
PII q[M];
int dist[N][N];
//很多时候不用先实现细节,先把整个思路整理好
int bfs(int sx, int sy)
{
    int dx[] = {-2, -1, 1, 2, 2, 1, -1, -2};
    int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};

    int hh = 0, tt = 0;
    q[0] = {sx, sy};

    memset(dist, -1, sizeof dist);
    dist[sx][sy] = 0;

    while (hh <= tt)
    {
        auto t = q[hh ++ ];
        if(g[t.x][t.y]=='H') return dist[t.x][t.y];
        
        for (int i = 0; i < 8; i ++ )
        {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (g[a][b] == '*') continue;
            if (dist[a][b] != -1) continue;

            dist[a][b] = dist[t.x][t.y] + 1;
            q[ ++ tt] = {a, b};
        }
    }

    return -1;
}

int main()
{
    cin >> m >> n;//这道题先输入列数,再输入行数,一定看清楚

    for (int i = 0; i < n; i ++ ) cin >> g[i];

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            if (g[i][j] == 'K')
                cout << bfs(i,j) << endl;

    return 0;
}

AcWing 1100. 抓住那头牛

/*
给定一个图,从起点走到终点,每条边权值为1,所以可以用BFS做
*/
#include 
#include 
#include 

using namespace std;

const int N = 1e5 + 10;//步数可能大于数据范围

int n, k;
int q[N];
int dist[N];

int bfs()
{
    memset(dist, -1, sizeof dist);
    dist[n] = 0;
    q[0] = n;

    int hh = 0, tt = 0;

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        if (t == k) return dist[k];//一定有解

        if (t + 1 < N && dist[t + 1] == -1)
        {
            dist[t + 1] = dist[t] + 1;
            q[ ++ tt] = t + 1;
        }
        if (t - 1 >= 0 && dist[t - 1] == -1)
        {
            dist[t - 1] = dist[t] + 1;
            q[ ++ tt] = t - 1;
        }
        if (t * 2 < N && dist[t * 2] == -1)
        {
            dist[t * 2] = dist[t] + 1;
            q[ ++ tt] = t * 2;
        }
    }

    return -1;
}

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

    cout << bfs() << endl;

    return 0;
}

多源BFS

AcWing 173. 矩阵距离

/*
求每一个位置到所有1的最短距离,到最近的1的距离
当只有一个起点时,把这个起点放入队列,往外扩展
多个起点时,朴素做法是分别以每个起点遍历一遍取最小值
优化:加一个虚拟源点之后转化成单源最短路问题
这里不需要建立虚拟源点,只需要在最开始时候把所有是1的位置初始化为0,然后把这些是1的位置添加到队列里面,也就是队列第一层
存的是所有起点,距离为1的点,后面部分跟一般搜索完全一样
*/
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 1010, M = N * N;

int n, m;
char g[N][N];
PII q[M];
int dist[N][N];

void bfs()
{
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    memset(dist, -1, sizeof dist);

    int hh = 0, tt = -1;
    for(int i = 0; i < n;i ++) 
        for(int j = 0; j < m;j ++)
            if (g[i][j] == '1')
            {
                dist[i][j] = 0;
                q[ ++ tt] = {i, j};
            }

    while (hh <= tt)
    {
        auto t = q[hh ++ ];

        for (int i = 0; i < 4; i ++ )
        {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (dist[a][b] != -1) continue;

            dist[a][b] = dist[t.x][t.y] + 1;
            q[ ++ tt] = {a, b};
        }
    }
}

int main()
{
    cin>>n>>m;
    for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);//每行输入数字之间没有空格,用字符串读入

    bfs();

    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ ) cout<

最小步数模型

AcWing 1107. 魔板

/*
如何存储状态:哈希法
思路简单,代码不好写
先把12345678这个状态放入队头,然后用宽搜搜所有状态,直到终点为止,每一次扩展时,分别做abc三种操作,得到三个字符串
判断是否哪个字符串之前搜到过,如果没有搜到过,更新距离,放入队列
记录方案:
记录每一个状态是由哪个状态转移过来的,这样就可以由终点倒推到起点
如何保证最小字典序输出:
扩展每个状态时,按照ABC的顺序
三个操作需要分别写
*/
#include 
#include 
#include 
#include 
#include 

using namespace std;

char g[2][4];
unordered_map> pre;
//存下来每一个状态怎么样转移过来的,用pair来存,因为不只是要知道哪个状态转移,还要知道怎么样转移
unordered_map dist;//存下来每一个状态的步数,用哈希表来存

void set(string state)
{//把字符串放入原来二维数组中
    for (int i = 0; i < 4; i ++ ) g[0][i] = state[i];
    for (int i = 7, j = 0; j < 4; i --, j ++ ) g[1][j] = state[i];
}

string get()
{//set的逆操作
    string res;
    for (int i = 0; i < 4; i ++ ) res += g[0][i];
    for (int i = 3; i >= 0; i -- ) res += g[1][i];
    return res;
}

string move0(string state)
{
    set(state);
    for (int i = 0; i < 4; i ++ ) swap(g[0][i], g[1][i]);
    return get();
}

string move1(string state)
{
    set(state);
    int v0 = g[0][3], v1 = g[1][3];
    for (int i = 3; i >= 0; i -- )
    {
        g[0][i] = g[0][i - 1];
        g[1][i] = g[1][i - 1];
    }
    g[0][0] = v0, g[1][0] = v1;
    return get();
}

string move2(string state)
{
    set(state);
    int v = g[0][1];
    g[0][1] = g[1][1];
    g[1][1] = g[1][2];
    g[1][2] = g[0][2];
    g[0][2] = v;
    return get();
}

int bfs(string start, string end)
{
    if (start == end) return 0;

    queue q;
    q.push(start);
    dist[start] = 0;

    while (!q.empty())
    {
        auto t = q.front();
        q.pop();

        string m[3];
        m[0] = move0(t);
        m[1] = move1(t);
        m[2] = move2(t);

        for (int i = 0; i < 3; i ++ )
            if (!dist.count(m[i]))
            {
                dist[m[i]] = dist[t] + 1;
                pre[m[i]] = {'A' + i, t};
                q.push(m[i]);
                if (m[i] == end) return dist[end];
            }
    }

    return -1;
}

int main()
{
    int x;
    string start, end;
    for (int i = 0; i < 8; i ++ )
    {
        cin >> x;
        end += char(x + '0');//把数字i变成字符i
    }

    for (int i = 1; i <= 8; i ++ ) start += char('0' + i);

    int step = bfs(start, end);

    cout << step << endl;

    string res;
    while (end != start)
    {
        res += pre[end].first;
        end = pre[end].second;
    }

    reverse(res.begin(), res.end());

    if (step > 0) cout << res << endl;

    return 0;
}

双端队列广搜

AcWing 175. 电路维修

/*
求从左上角到右下角的最短路,最短路中边权有两种
如果可以直接走到的话,不需要旋转,旋转次数为0,边权为0,如果需要旋转才能走到的话,旋转次数为1,边权为1
等价于给了一张无向图,图中有两种边,边权为0或1,从起点到终点的最短路径
之前的题是所有边权重一样,直接BFS求出最短路
双向队列与一般宽搜相比,一般宽搜是取出队头之后,把所有能从队头扩展的元素放入队尾
而双向队列是判断从队头取出来的元素,边权重是0还是1,如果扩展出来的边权重是0就插到队头,否则插到队尾
*/
#include 
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N = 510, M = N * N;

int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];

int bfs()//之前每个点只会入队一次,这里由于有0的边权存在,所有每个点可能入队多次
{
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    dist[0][0] = 0;
    deque q;
    q.push_back({0, 0});

    char cs[] = "\\/\\/";//存一下四个方向什么方向才是通路\/\/  \是转义字符所有前面再加一个\
    int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
    int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};//判断每个方向的边,注意格点坐标和字符在所给二维字符数组坐标

    while (q.size())
    {
        PII t = q.front();
        q.pop_front();

        if (st[t.x][t.y]) continue;//和dijistar一样,判断之前是否算过它
        st[t.x][t.y] = true;

        for (int i = 0; i < 4; i ++ )
        {
            int a = t.x + dx[i], b = t.y + dy[i];
            if (a < 0 || a > n || b < 0 || b > m) continue;//注意格点长度比格子长度多1

            int ca = t.x + ix[i], cb = t.y + iy[i];
            int d = dist[t.x][t.y] + (g[ca][cb] != cs[i]);

            if (d < dist[a][b])
            {
                dist[a][b] = d;

                if (g[ca][cb] != cs[i]) q.push_back({a, b});
                else q.push_front({a, b});
            }
        }
    }

    return dist[n][m];
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d%d", &n, &m);
        for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);

        int t = bfs();

        if (t == 0x3f3f3f3f) puts("NO SOLUTION");
        else printf("%d\n", t);
    }

    return 0;
}

双向广搜

AcWing 190. 字串变换

/*
把一个字符串看成一个单独的状态,把它所有可以经过一步变换变成的状态看作从这个状态向变到的状态连一条长度为1的边
从而转变为图论问题,从起点走到终点最少需要走几步
数据范围:
字符串长度的上限为20,最多6个规则,每一个规则都可能是从每一个字母开始,最坏情况下20*6(变换方式)^10(最多变换十次)
如果单向搜索会超时,那就从起点和终点两方同时开始搜
BFS优化:双向广搜一般用在最小步数模型(指数级别)中,在最短路模型,Flood Fill一般用不到,因为他们搜的点数量一般不大
两个方向同时搜索,而不是一个方向搜到另外一个方向
最坏情况下20*6^10->2*20*6^5
*/
#include 
#include 
#include 
#include 
#include 

using namespace std;

const int N = 6;

int n;
string a[N], b[N];

int extend(queue& q, unordered_map& da, unordered_map& db, string a[], string b[])
{
    for (int k = 0, sk = q.size(); k < sk; k ++ )//每次每边扩展完整一层
    {
        string t = q.front();
        q.pop();
        //开始扩展,两个维度考虑
        for (int i = 0; i < t.size(); i ++ )//扩展起点
            for (int j = 0; j < n; j ++ )//枚举规则
                if (t.substr(i, a[j].size()) == a[j])//匹配规则
                {
                    string state = t.substr(0, i) + b[j] + t.substr(i + a[j].size());
                    //判断此时尝试扩展的状态是否已经被这个方向或另外一个方向扩展
                    if (da.count(state)) continue;//判断此时尝试扩展的状态之前已经被扩展过,只记录第一次
                    if (db.count(state)) return da[t] + 1 + db[state];//此时枚举到了两个方向重合的地方
                    da[state] = da[t] + 1;
                    q.push(state);
                }
    }

    return 11;
}

int bfs(string A, string B)
{
    queue qa, qb;//由于是两个方向,所以要建两个方向的队列
    unordered_map da, db;
    qa.push(A), da[A] = 0;//从起点开始搜索,存储每个点到起点的距离
    qb.push(B), db[B] = 0;

    while (qa.size() && qb.size())//如果只有一方还有,另一方没了,说明AB不连通
    {
        int t;//都存在情况下,判断哪个方向队列长度更小,说明这个方向空间更小,就朝这个方向扩展
        if (qa.size() <= qb.size()) t = extend(qa, da, db, a, b);//扩展qa a的距离 b的距离 把a变成b
        else t= extend(qb, db, da, b, a);

        if (t <= 10) return t;
    }

    return 11;
}

int main()
{
    string A, B;
    cin >> A >> B;
    while (cin >> a[n] >> b[n]) n ++ ;

    int step = bfs(A, B);
    if (step > 10) puts("NO ANSWER!");
    else cout<

A*

AcWing 178. 第K短路

/*
每条最短路中至少要包含一条边,所以当s==t时,k++
如何枚举到所有路线:
每次从0开始扩展,每次要把当前这个点能扩展到的点全部加进来,求最短路时是只有能更新它的时候才加进来,但这里是所有加进来
因为不只是求最短路而是第k短路,从所有路径中找到第k小
搜索空间非常庞大
估价函数(小于等于真实距离,越接近越好):
当前点到终点的最短距离,这个可以反向求终点到每个点的最短距离,反向图跑dijkstra算法
如何求出第k短路:
当终点第一次从队列中弹出来时,一定是最小值(第一小),第k次弹出来就是第k小
*/
#include 
#include 
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair PII;
typedef pair PIII;

const int N = 1010, M = 200010;//因为要建反图,两倍边

int n, m, S, T, K;//点数,边数,起点S,终点T和第K短路
int h[N], rh[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int h[], int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dijkstra()
{
    priority_queue, greater> heap;
    heap.push({0, T});

    memset(dist, 0x3f, sizeof dist);
    dist[T] = 0;

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.y;
        if (st[ver]) continue;
        st[ver] = true;

        for (int i = rh[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
}

int astar()
{
    priority_queue, greater> heap;
    heap.push({dist[S], {0, S}});//起点估价,

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.y.y, distance = t.y.x;
        cnt[ver] ++ ;
        if (cnt[T] == K) return distance;

        for (int i = h[ver]; ~i; i = ne[i])
        {//扩展所有边
            int j = e[i];
            if (cnt[j] < K)
                heap.push({distance + w[i] + dist[j], {distance + w[i], j}});
        }
    }

    return -1;
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(h, a, b, c);
        add(rh, b, a, c);
    }
    scanf("%d%d%d", &S, &T, &K);
    if (S == T) K ++ ;

    dijkstra();
    printf("%d\n", astar());

    return 0;
}

AcWing 179. 八数码

/*
A*算法解决的问题和双向广搜一样
这个算法正确的条件:有解且估计距离<=真实距离(这里都是当前点到终点的距离),越接近越好
它只能保证当终点出队时,终点距离最小,并不能保证中间的点最小
启发函数:从起点开始搜,遍历很少的状态搜到终点
基本流程:
    把BFS中的队列换成优先队列,给每个点一个估计值,按照估计值排序,每次选择一个我们认为最好的点扩展
    特别像dijistra算法,注意A*算法能处理的图当中的边权任意即可,绝大多数情况边权非负,但不能有负权回路,不要求为1
    dijkstra算法可以看做所有估计距离都取0的算法,本质上没什么关系,只是形式上比较接近
    while(!q.empty()){
        t<-取出优先队列队头(小根堆)(排序关键字是两个距离相加)
        当终点第一次出队时,break
        除了存真实距离之外,还需要存真实距离(从起点到当前点)+估价距离(从当前点到终点的估计距离)
        开始扩展
        for t的所有邻边
            将邻边入队
    }
宽搜:
BFS:每个状态只会入队一次,所以在入队时候判重
djikstra:一旦出队就是正确的,所以可以在出队时候判重
A*:出队时候都不能判重,除了终点外,其他点出来一次,更新一次
spfa:搜索空间可能比较多,上面是尽量减少搜索空间
*/
/*
朴素见基础课
八数码有解<=>逆序对数量为偶数
估价函数如何设计:
观察每次移动只会把一个数移动一次,因此股价函数可以取每一个数与它真实位置之间的曼哈顿距离
当前状态中每个数与它的目标位置的曼哈顿距离之和
*/
#include 
#include 
#include 
#include 
#include 

using namespace std;

int f(string state)
{//估计函数,求曼哈顿距离之和
    int res = 0;
    for (int i = 0; i < state.size(); i ++ )
        if (state[i] != 'x')
        {
            int t = state[i] - '1';
            //转换成数值,减去1是因为题目中从给定的数从1开始,−‘1′后代表数字1映射后是0,数字2是映射后是1.....依次类推即可。
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }
    return res;
}

string bfs(string start)
{
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    char op[4] = {'u', 'r', 'd', 'l'};

    string end = "12345678x";
    unordered_map dist;//记录距离
    unordered_map> prev;//记录前驱状态,由哪个状态经过什么操作转移过来的
    priority_queue, vector>, greater>> heap;
    //优先队列中存的是pair,第一维是估计距离,第二维是状态,因为是小根堆,所以加两个参数

    heap.push({f(start), start});
    dist[start] = 0;

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        string state = t.second;

        if (state == end) break;

        int step = dist[state];
        int x, y;
        for (int i = 0; i < state.size(); i ++ )
            if (state[i] == 'x')
            {
                x = i / 3, y = i % 3;
                break;
            }//找到xy的横纵坐标
        //扩展
        string source = state;//为了方便恢复现场先把原状态存下来
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < 3 && b >= 0 && b < 3)
            {
                swap(state[x * 3 + y], state[a * 3 + b]);//先交换看一下,判断新的状态距离是否更小
                if (!dist.count(state) || dist[state] > step + 1)
                //如果它之前没有被扩展过,或者虽然被扩展过,但距离较大,需要被更新优化
                {
                    dist[state] = step + 1;
                    prev[state] = {source, op[i]};
                    heap.push({dist[state] + f(state), state});
                }
                swap(state[x * 3 + y], state[a * 3 + b]);
            }
        }
    }

    string res;//方案
    while (end != start)
    {
        res += prev[end].second;
        end = prev[end].first;
    }
    reverse(res.begin(), res.end());
    return res;
}

int main()
{
    string g, c, seq;
    while (cin >> c)
    {
        g += c;//初始状态
        if (c != "x") seq += c;//序列
    }

    int t = 0;//求逆序对数量
    for (int i = 0; i < seq.size(); i ++ )
        for (int j = i + 1; j < seq.size(); j ++ )
            if (seq[i] > seq[j])
                t ++ ;

    if (t % 2) puts("unsolvable");//逆序对为奇数一定无解
    else cout << bfs(g) << endl;

    return 0;
}

DFS

DFS之连通性模型

AcWing 1112. 迷宫

/*
BFS:横向扩展,一层一层搜 队列
DFS:纵向扩展,一条道走到底 栈
连通性模型(两个方法都可以做):Flood Fill(搜连通块),图与树的遍历
这道题BFS,DFS都可以,BFS还可以知道最短距离,DFS只能判断是否连通(但是代码短很多,BFS需要手写队列,DFS可以直接用系统栈)
注意是否会爆栈,递归改成非递归
*/
#include 
#include 
#include 

using namespace std;

const int N = 110;
//不需要写pair
int n;
char g[N][N];
int xa, ya, xb, yb;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool st[N][N];//判重数组

bool dfs(int x, int y)
{
    if (g[x][y] == '#') return false;
    if (x == xb && y == yb) return true;

    st[x][y] = true;//不需要恢复现场,标记保证每个点只遍历一次

    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= n) continue;
        if (st[a][b]) continue;
        if (dfs(a, b)) return true;
    }

    return false;
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);
        scanf("%d%d%d%d", &xa, &ya, &xb, &yb);

        memset(st, 0, sizeof st);
        if (dfs(xa, ya)) puts("YES");
        else puts("NO");
    }

    return 0;
}

AcWing 1113. 红与黑

/*
求当前这个点的连通块 Flood Fill DFS实现
DFS缺点:搜索层数较多时可能爆栈
*/
#include 
#include 
#include 

using namespace std;

const int N = 25;

int n, m;
char g[N][N];
bool st[N][N];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dfs(int x, int y)
{
    int cnt = 1;

    st[x][y] = true;
    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= m) continue;
        if (g[a][b] != '.') continue;
        if (st[a][b]) continue;

        cnt += dfs(a, b);
    }

    return cnt;
}

int main()
{
    while (cin >> m >> n, n || m)//先输入列数,再输入行数
    {
        for (int i = 0; i < n; i ++ ) cin >> g[i];

        int x, y;//确定起点 
        //DFS两种模型:人体内部从哪儿到哪儿;人当做一个整体,人能到哪儿。这个影响到深搜时是否要恢复现场
        //如果是第一种人体内部的话,为了保证每个点只能搜一次,不能恢复现场
        //第二种的话,一定要恢复原状
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ )
                if (g[i][j] == '@')
                {
                    x = i;
                    y = j;
                }

        memset(st, 0, sizeof st);
        cout << dfs(x, y) << endl;
    }

    return 0;
}

DFS之搜索顺序

AcWing 1116. 马走日

/*
按照哪种顺序搜可以把全部状态搜出来
最开始在起点,尝试分别枚举最多可跳的八个方向,如果哪个方向可以走的话,继续
*/
#include 
#include 
#include 

using namespace std;

const int N = 10;

int n, m;
bool st[N][N];//每个位置是否走过
int ans;
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

void dfs(int x, int y, int cnt)
{
    if (cnt == n * m)
    {
        ans ++ ;
        return;
    }
    st[x][y] = true;

    for (int i = 0; i < 8; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= m) continue;
        if (st[a][b]) continue;//判断这一个点在这条路径上是否被走过,但是要恢复现场是因为这个点可以被不同的路径走过
        dfs(a, b, cnt + 1);
    }

    st[x][y] = false;
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        int x, y;
        scanf("%d%d%d%d", &n, &m, &x, &y);

        //memset(st, 0, sizeof st);//这里初始化可以不用做,因为每次都要恢复现场
        ans = 0;
        dfs(x, y, 1);//当前正在搜第几个点

        printf("%d\n", ans);
    }

    return 0;
}

AcWing 1117. 单词接龙

/*
先判断任意两个单词能否接到一块,可以的话就表示两者之间有一条边,先预处理一下二维的邻接矩阵,表示点之间的邻接关系
预处理之后就可以暴搜,枚举所有情况,树的最大深度为40,因为每个单词最多用两次,单词数最多20 
*/
#include 
#include 
#include 

using namespace std;

const int N = 21;

int n;
string word[N];
int g[N][N];//初始化任意两个单词是否有边,如果相连,存储最小重合长度
int used[N];//每个单词用了多少次
int ans;

void dfs(string dragon, int last)
{
    ans = max((int)dragon.size(), ans);

    used[last] ++ ;

    for (int i = 0; i < n; i ++ )//下一个单词填哪个
        if (g[last][i] && used[i] < 2)//如果第last个单词后面可以接第i个单词,并且,第i个单词没用到两次
            dfs(dragon + word[i].substr(g[last][i]), i);

    used[last] -- ;//恢复现场
}

int main()
{
    cin >> n;//测试数据单组
    for (int i = 0; i < n; i ++ ) cin >> word[i];
    char start;
    cin >> start;

    for (int i = 0; i < n; i ++ )//初始化任意两个单词是否有边
        for (int j = 0; j < n; j ++ )
        {
            string a = word[i], b = word[j];
            for (int k = 1; k < min(a.size(), b.size()); k ++ )//重合长度必须大于等于1,且严格小于两个串的长度
                if (a.substr(a.size() - k, k) == b.substr(0, k))
                {
                    g[i][j] = k;
                    break;//要接龙最长,重合部分越短越好,所以从小到大枚举
                }
        }

    for (int i = 0; i < n; i ++ )
        if (word[i][0] == start)
            dfs(word[i], i);//i表示当前接龙的最后一个单词是第i个单词

    cout << ans << endl;

    return 0;
}

AcWing 1118. 分成互质组

/*
转化成图论问题,如果两个数之间不互质连一条边,表示有关系,转化成图当中最少分为多少组,使得每一组内没有边
像仇恨,讨厌关系,小孩之间闹矛盾,有矛盾就连边,比较经典问题
简单做法:核心是如何枚举所有情况,找到最优解
一个组一个组枚举,第一个组中该选谁
第一种方案:把某个数加到最后一组(之前的组已经全部枚举完,尽可能让分支数量少,搜索空间小)
第二种方案:新开一个组
(一般出成暴搜的都是np完全问题,否则考虑DP,贪心解法)
优化:
1.当可以做两个操作时,就做第一个操作,只有当所有数都不能加到最后一个组时再新开辟一个组
2.考虑哪些数要放到当前组时,这是一个组合问题,不是排列问题,要按照组合方式搜索,与排列顺序无关,为了不重复搜索
  像先搜到123,再搜到132这种,我们人为定义一个组内顺序,按照下标从小到大顺序往组里添数,只需要在搜索时传入一个参数
  表示当前从哪个位置开始搜,很多时候只需要枚举组合情况,不需要枚举排列情况
*/
#include 
#include 
#include 

using namespace std;

const int N = 10;

int n;
int p[N];
int group[N][N];//最多N组,每一组要分别记下来
bool st[N];//判重数组,当前每个数是否被用过
int ans = N;//答案最坏情况是N,每个数单独一组

int gcd(int a, int b)
{//判断两个数互质:欧几里得算法
    return b ? gcd(b, a % b) : a;
}

bool check(int group[], int gc, int i)
{
    for (int j = 0; j < gc; j ++ )
        if (gcd(p[group[j]], p[i]) > 1)
            return false;//不是互质
    return true;
}
//第几组,当前组内下标,当前一共搜了多少个元素,当前可以从哪个元素开始搜
void dfs(int g, int gc, int tc, int start)
{
    if (g >= ans) return;//如果当前组数量已经大于之前的一个可能的解,那这样继续搜索一定不是最优解,提前结束
    if (tc == n) ans = g;//全部搜完

    bool flag = true;//先假设不能添加元素,再探索每个元素,只要有一个可以添加,就加进去,然后设为false
    for (int i = start; i < n; i ++ )//如果当前遍历元素没有被用过,并且当前组内所有元素与第i个数是互质的,就可以用
        if (!st[i] && check(group[g], gc, i))
        {
            st[i] = true;
            group[g][gc] = i;
            dfs(g, gc + 1, tc + 1, i + 1);
            st[i] = false;//恢复现场

            flag = false;//表示当前组内能否添加元素,能添加就设为false
        }

    if (flag) dfs(g + 1, 0, tc, 0);//每个元素都遍历完了都添加不了,只能新开一组,从下标为0开始探索
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> p[i];

    dfs(1, 0, 0, 0);//先搜第一组,组内有0个元素,当前已经搜了0个元素,从0号下标开始搜

    cout << ans << endl;

    return 0;
}

DFS之剪枝与优化

AcWing 165. 小猫爬山

/*
深搜与宽搜不管内部还是外部搜索都可以对应一条搜索树,树种节点表示方案,搜索剪枝是不一定要走到头才能知道方案不合法
可能走到一半就能知道方案不合法,提前判断退出,对应子树不用搜索,提高效率
常用的剪枝方式:
    1.优化搜索顺序:一般情况下优先搜索分支较少(选择,策略较少)的节点
    2.排除等效冗余:比如搜索组合数(不考虑顺序,不搜索重复状态,按照组合方式搜索)
    3.可行性剪枝:搜索过程中搜到一半发现不合法,提前退出
    4.最优性剪枝:搜索过程中搜到一半发现无论如何都会比当前搜到的最优解要差
    5.记忆化搜索(DP)
n比较小,数据范围提示用暴力搜索来做
暴搜第一点是考虑搜索顺序枚举所有方案
从前往后依次枚举每只小猫,每次枚举当前小猫应该放在哪辆车上(已有的车或新开一辆),递归n层 
每递归一层,把一只小猫安排好,放完之后看下当前用了几辆车,全局所有方案取最小
时间复杂度不太好估计,暴力搜索都这样,指数级别,具体多少不太好算
这道题如何剪枝:
优化搜索顺序:按照重量排序,优先选择较重的猫
排除等效冗余:这道题搜索过程中已经没有枚举冗余情况,冗余是指先放这只还是那只,但这道题的搜索本来就是一只一只搜索
可行性剪枝:当前猫放入超出最大承受重量,直接砍掉,这个可以用
最优性剪枝:当前车的数量已经大于ans,砍掉直接退出
记忆化搜索:不考虑
综上所述,这道题可用优化搜索顺序,可行性剪枝,最优性剪枝
*/
#include 
#include 
#include 

using namespace std;

const int N = 20;

int n, m;
int w[N];
int sum[N];//每辆车放的小猫总重量
int ans = N;//最坏情况下每只猫一辆车

void dfs(int u, int k)
{
    // 最优性剪枝,当前车的数量大于最优解,不会是答案,提前结束
    if (k >= ans) return;
    if (u == n)
    {
        ans = k;
        return;
    }

    for (int i = 0; i < k; i ++ )//枚举当前第u只猫可以放哪辆车上去
        if (sum[i] + w[u] <= m) // 可行性剪枝
        {
            sum[i] += w[u];
            dfs(u + 1, k);
            sum[i] -= w[u]; // 恢复现场
        }

    // 新开一辆车
    sum[k] = w[u];
    dfs(u + 1, k + 1);
    sum[k] = 0; // 恢复现场
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) cin >> w[i];

    // 优化搜索顺序
    sort(w, w + n);
    reverse(w, w + n);//最终要从大到小排序

    dfs(0, 0);//当前搜到第0只猫,当前车的数量是0

    cout << ans << endl;

    return 0;
}

AcWing 166. 数独

/*
先考虑搜索顺序,再考虑优化剪枝
先随意选择一个空格子,枚举这个空格子可以填哪些数字,这里做个可行性剪枝,然后再随意选一个空格子
剪枝(用了两个):
优化搜索顺序:优先选择分支数较少的,也就是这个格子可填的数较少的,可行方案最少的
排除等效冗余:没有,这道题搜索顺序固定
可行性剪枝:当前所填数字不能与行,列,九宫格有重复
最优性剪枝:没有,因为题目本身找的就是可行解
位运算优化:
用三个九位二进制数分别表示一行,一列,九宫格的状态,哪些用了,哪些没用,并且都没有才能选(与运算求交集)
lobit运算可以在O(1)时间复杂度内返回二进制最后一个1
*/
#include 
#include 
#include 

using namespace std;

const int N = 9, M = 1 << N;//M:行,列,九宫格都是一个二进制数,二进制1表示能用

int ones[M], map[M];//两个数组做优化:一个是快速求出来一个状态有多少个1,一个用于lowbit
int row[N], col[N], cell[3][3];
char str[100];

void init()
{
    for (int i = 0; i < N; i ++ )
        row[i] = col[i] = (1 << N) - 1;//最开始每个格子都可以填,都为1

    for (int i = 0; i < 3; i ++ )
        for (int j = 0; j < 3; j ++ )
            cell[i][j] = (1 << N) - 1;
}

void draw(int x, int y, int t, bool is_set)
{//辅助函数,在(x,y)这个位置上填一个数字t,布尔值表示在当前位置填一个数(dfs)还是删掉(恢复现场)
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';

    int v = 1 << t;
    if (!is_set) v = -v;

    row[x] -= v;
    col[y] -= v;
    cell[x / 3][y / 3] -= v;
}

int lowbit(int x)
{
    return x & -x;
}

int get(int x, int y)
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}

bool dfs(int cnt)
{
    if (!cnt) return true;

    int minv = 10;//先找一个分支数量最少的空格,最多只有九种选择,取10
    int x, y;
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j < N; j ++ )
            if (str[i * N + j] == '.')
            {
                int state = get(i, j);
                if (ones[state] < minv)
                {
                    minv = ones[state];
                    x = i, y = j;
                }
            }

    int state = get(x, y);//枚举当前状态所有的1
    for (int i = state; i; i -= lowbit(i))
    {//二进制中比如第0到8位中,第3位为1代表4没填,可以填填看
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if (dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }

    return false;
}

int main()
{
    for (int i = 0; i < N; i ++ ) map[1 << i] = i;//map就是log2
    for (int i = 0; i < 1 << N; i ++ )
        for (int j = 0; j < N; j ++ )
            ones[i] += i >> j & 1;//枚举每个二进制有多少个1

    while (cin >> str, str[0] != 'e')
    {
        init();//预处理行,列,九宫格

        int cnt = 0;//表示有多少个空格
        for (int i = 0, k = 0; i < N; i ++ )
            for (int j = 0; j < N; j ++, k ++ )
                if (str[k] != '.')
                {
                    int t = str[k] - '1';
                    draw(i, j, t, true);
                }
                else cnt ++ ;

        dfs(cnt);

        puts(str);
    }

    return 0;
}

AcWing 167. 木棒

/*
问题是给定n个数,把n个数分为若干组使每组总和相等,最多分为多少组
木棒:表示每组总和  木棍表示切好的
搜索顺序:先从小到大枚举木棒长度,对于固定长度依次从前往后拼接每根木棒
剪枝:
长度一定能整除所有木棒的和sum,只枚举sum的约数
优化搜索顺序:从大到小枚举
排除等效冗余:
1.枚举的是组合数,而不是排列数,顺序无所谓,下标从小到大,拼接一根木棒时内部传一个参数表示下一根木棍从哪儿开始枚举
2.如果当前木棍加到当前木棒中失败,则直接略过后面所有长度相等的木棍
3.如果木棒的第一根木棍放置失败,则当前方案一定失败
4.如果木棒的最后一根木棍放置失败,则当前方案一定失败
*/
#include 
#include 
#include 

using namespace std;

const int N = 70;

int n;
int w[N];
int sum, length;//所有木棍总和,枚举的每根木棒长度
bool st[N];//每根小棍是否被用过

bool dfs(int u, int cur, int start)
{//当前枚举到第几根大棍,当前大棍的长度,小棍开始的位置
    if (u * length == sum) return true;//枚举是从第0根大棍枚举到第u-1根大棍
    if (cur == length) return dfs(u + 1, 0, 0);//第u根大棍拼接好了,开新棍

    for (int i = start; i < n; i ++ )
    {//当前大棍里枚举每一根小棍
        if (st[i] || cur + w[i] > length) continue;//可行性剪枝

        st[i] = true;
        if (dfs(u, cur + w[i], i + 1)) return true;
        st[i] = false;//恢复现场

        if (!cur || cur + w[i] == length) return false;//如果没有成功的小棍是第一根或最后一根

        int j = i;//排除与这根失败的小棍相邻的,长度相等的小棍
        while (j < n && w[j] == w[i]) j ++ ;
        i = j - 1;
    }

    return false;
}

int main()
{
    while (cin >> n, n)
    {
        memset(st, 0, sizeof st);
        sum = 0;

        for (int i = 0; i < n; i ++ )
        {
            cin >> w[i];
            sum += w[i];
        }

        sort(w, w + n);
        reverse(w, w + n);//小棍从大到小放置

        length = 1;
        while (true)
        {//从小到大枚举每根大棍长度
            if (sum % length == 0 && dfs(0, 0, 0))//从前往后枚举每根大棍,当前大棍是0,当前从0开始枚举
            {
                cout << length << endl;
                break;
            }
            length ++ ;
        }
    }

    return 0;
}

AcWing 168. 生日蛋糕

/*
从下往上看,每层高度,半径严格递减

搜索顺序:
自底向上搜,给之后搜索留的余地越来越少越好,每一层从大到小先枚举最底层半径再枚举高度(因为在总面积中,半径是^2级别)

可行性剪枝:(枚举的r,h有范围)
当前枚举第u层,u<=r(u)<=min( r(u+1)-1 , sqrt(n-v) )  ,   u<=h(u)<=min( h(u+1)-1 , (n-v)/(r(u)^2)  )
v是下面层数的总体积,n是给定总体积,n-v>= r(u)^2 *h(u)

预处理前u层体积与表面积最小值,第一层半径与高取1,第二层半径与高取2,要保证
v+min(u)<=n(可行性剪枝)  s+min(u)
#include 
#include 
#include 

using namespace std;

const int N = 25, INF = 1e9;

int n, m;
int minv[N], mins[N];
int R[N], H[N];
int ans = INF;

void dfs(int u, int v, int s)
{
    if (v + minv[u] > n) return;
    if (s + mins[u] >= ans) return;
    if (s + 2 * (n - v) / R[u + 1] >= ans) return;

    if (!u)
    {
        if (v == n) ans = s;
        return;
    }

    for (int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r -- )
        for (int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h -- )
        {
            int t = 0;
            if (u == m) t = r * r;
            R[u] = r, H[u] = h;
            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}

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

    for (int i = 1; i <= m; i ++ )
    {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }

    R[m + 1] = H[m + 1] = INF;

    dfs(m, 0, 0);

    if (ans == INF) ans = 0;
    cout << ans << endl;

    return 0;
}

迭代加深

AcWing 170. 加成序列

/*
迭代加深:在搜索时,搜索树可能比较深,答案在比较浅的层中,搜索浪费很多时间,迭代加深就是针对这个情况
与宽搜类似,但会定义一个层数上限,逐步扩大范围,宽搜时间复杂度是指数级别,每次会把上一层时间节点全部记录下来
迭代加深本质还是DFS,每次会记录一条路径上的信息,它的时间复杂度是O(n),与高度成正比
DFS加剪枝后很难去算具体的时间复杂度,粗略估计即可

这道题最坏情况下搜索树深度为100层,但是答案深度估计不大

剪枝:
优化搜索顺序:优先枚举较大的数会更快的涨到n
排除等效冗余:
比如1+4或2+3求和结果相同,没必要重复枚举,可以在每一个节点里面开一个布尔数组,判断每个数是否之前被枚举过
*/
#include 
#include 
#include 

using namespace std;

const int N = 110;

int n;
int path[N];

bool dfs(int u, int k)//当前层数,最大层数
{
    if (u == k) return path[u - 1] == n;

    bool st[N] = {0};//定义布尔数组,排除等效冗余
    for (int i = u - 1; i >= 0; i -- )//从大到小枚举当前所有分支
        for (int j = i; j >= 0; j -- )
        {
            int s = path[i] + path[j];//s是这一层可能会放的数,它是前任意两层的数之和
            if (s > n || s <= path[u - 1] || st[s]) continue;
            st[s] = true;//这两层循环是在枚举这一层尝试放哪些数
            path[u] = s;//这里没有必要恢复现场,因为会被重复赋值
            if (dfs(u + 1, k)) return true;
        }

    return false;
}

int main()
{
    path[0] = 1;
    while (cin >> n, n)//多组测试数据,直到读到0为止
    {
        int k = 1;//从小到大枚举迭代加深的范围
        while (!dfs(1, k)) k ++ ;//1表示当前层数为第一层

        for (int i = 0; i < k; i ++ ) cout << path[i] << ' ';
        cout << endl;
    }

    return 0;
}

双向DFS

AcWing 171. 送礼物

/*
双向DFS和双向BFS原理一样
这道题看着很像背包问题,但注意数据范围,背包问题一定会超时,所以不能DP,但是由于n比较小,所以尝试暴搜
问题是所有总重量不超过w的选法里面,重量最大值是多少,所以只需要想要个方法枚举所有方案,取最大值
暴力搜索是依次枚举每件物品选与不选 2^n(这里n最多为46,必然超时)
双向DFS,从两个方向来搜
空间换时间 把n个物品看成一个区间,先暴力搜索遍历由前一半物品能凑出来的所有重量的集合,n最多为46
所有前一半可以凑出来最多2^23种不同情况(这里可以加个小优化,排序判重)
然后再枚举后一半每种物品的选择,也是有2^23种不同情况,对于每种情况,比如当前重量为s,我们需要在前面一半得到的情况中
找到小于等于(w-s)的最大值(二分) 
拆分一下,先把前一半算出来,再暴力搜索最后一半,然后查表,不需要重新暴力搜索
时间复杂度:2^(K)+2^(N-K)*K 这里不一定非要取一半,比如可以取k=25尽可能是总和最小

优化:
搜索顺序:从大到小排序搜索,优先搜索较大的数,更加接近上限,层数很短,节点更少,这样也可以更快进行可行性剪枝

总结:
1.将所有物品按照重量从大到小排序
2.先将前k件物品能凑出的所有重量打表,然后排序判重
3.搜索剩下的N-K件物品的选择方式,然后在表中二分出不超过w的最大值
*/
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 46;

int n, m, k;
int w[N];//存储每件物品的重量
int weights[1 << 25], cnt = 1;
//k最大是25,是要存2^25个数,cnt从1开始是因为0也是一个可以枚举的重量,cnt表示当前正在枚举哪个数
int ans;

void dfs1(int u, int s)//当前枚举到哪个数,第二项是当前的和
{
    if (u == k)
    {
        weights[cnt ++ ] = s;
        return;
    }

    dfs1(u + 1, s);//当前不选这个物品
    if ((LL)s + w[u] <= m) dfs1(u + 1, s + w[u]);//如果可以选的话,选
}

void dfs2(int u, int s)
{
    if (u == n)
    {//如果某种选择方案已经枚举到了第n个数
        int l = 0, r = cnt - 1;
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if ((LL)s + weights[mid] <= m) l = mid;
            else r = mid - 1;
        }
        ans = max(ans, s + weights[l]);
        return;
    }

    dfs2(u + 1, s);//不选
    if ((LL)s + w[u] <= m) dfs2(u + 1, s + w[u]);//选
}

int main()
{
    cin >> m >> n;
    for (int i = 0; i < n; i ++ ) cin >> w[i];

    sort(w, w + n);
    reverse(w, w + n);//优化搜索顺序,从大到小排序

    k = n / 2 + 2;//前k件物品打表,这个是有所给数据范围估计来的,后半段有个二分,平衡两端计算量
    dfs1(0, 0);

    sort(weights, weights + cnt);//排序
    cnt = unique(weights, weights + cnt) - weights;//判重,删掉数组中重复元素,返回值是剩下不同元素个数

    dfs2(k, 0);//dfs后半段,从k开始,当前和是0

    cout << ans << endl;

    return 0;
}

IDA*

AcWing 180. 排书


AcWing 181. 回转游戏


你可能感兴趣的:(刷题,算法)