WHUT第六周训练整理

WHUT第六周训练整理

写在前面的话:我的能力也有限,错误是在所难免的!因此如发现错误还请指出一同学习!

索引

(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)

零、基础知识过关

一、easy:01、05、06、10、13、14、15、16、20

二、medium:02、04、07、08、09、11、12、18、19、21、22、23、24

三、hard:03、17

本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。

零、基础知识过关

本周是搜索场,用到的知识全都是两种基础的搜索方法( d f s b f s dfs、bfs )再加上一些细节、剪枝等。

d f s dfs​ :深度优先搜索,对于一颗搜索树先往更深的地方进行搜索,如果找到了答案或者无法继续往更深的地方走则返回,适合于用于寻找是否存在可行解的情况。

b f s bfs​ :广度优先搜索,对于一颗搜索树“一层一层”地搜索,如果找到答案则返回当前的层数,如果无法继续往下走则该答案不存在。

剪枝:在搜索的过程中存在很多不必要的步骤,如果我们明知道这样走没有作用,那么就不需要走了,即剪枝。常见的剪枝有:可行性剪枝、最优性剪枝、奇偶性剪枝等等。

还没有掌握的同学先自行在网上学习,基础的知识必须要熟悉!

总而言之 d f s dfs 就是不撞南墙不回头,一条路走到黑,而 b f s bfs 则是像波纹一样从内向外一层层展开。

https://visualgo.net/zh/dfsbfs 这个网站可以看到这两种搜索算法的执行过程,可以帮助理解。

(该网站中不仅仅只有这两种算法,还有很多其他的算法,提供了可视化的效果,建议收藏!)

一、easy

1001:Robot Motion(模拟)

题意:给一个二维 N M N*M​ 的字符矩阵,字符只有 N S W E NSWE​ 四种,分别代表上下左右,现在一个人在 ( 1 , y ) (1, y)​ 处,问多少步之后会走出矩阵或者进行死循环。

范围: N , M 10   ;   y < = M N,M \le 10~;~y <= M

分析:用不到搜索,只需要按照题目的意思模拟就可以了,对于死循环可以使用二维数组 s t e p s steps​ 记录每个格子的第一次进入时的步数,如果走的过程中遇到已经走过的格子就说明进入循环。

Code

const int MAXN = 100 + 10;

int n, m, x, y;
char g[MAXN][MAXN], steps[MAXN][MAXN];

void dfs(int x, int y, int step)  // x和y表示当前位置,step表示当前的步数
{
    steps[x][y] = step;  // 首次进入,记录下来
    char current = g[x][y];
    // 按照格子的内容行进
    if (current == 'N')
    {
        x--;
    }
    else if (current == 'S')
    {
        x++;
    }
    else if (current == 'W')
    {
        y--;
    }
    else
    {
        y++;
    }
    // 如果走出了矩阵,输出答案并返回
    if (x <= 0 || x > n || y <= 0 || y > m)
    {
        cout << step << " step(s) to exit" << endl;
        return;
    }
    // 如果遇到了走过的点,进入死循环,输出答案并返回
    if (steps[x][y])
    {
        cout << steps[x][y] - 1 << " step(s) before a loop of " << step - steps[x][y] + 1 << " step(s)" << endl;
        return;
    }
    dfs(x, y, step + 1);  // 否则继续进行模拟
}

int main()
{
    while (cin >> n >> m, n + m)
    {
        cin >> y;
        memset(steps, 0, sizeof(steps));  // 多组输入注意清空数组
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= m; j++)
            {
                cin >> g[i][j];
            }
        }
        x = 1;  // 初始位置在 (1, y)
        dfs(x, y, 1);
    }
    return 0;
}

1005:Accepted Necklace(dfs)

题意:给 N N​ 块石头,每块石头有自身的价值 a a​ 以及重量 b b​ ,需要选择其中 K K​ 块石头在做成一条项链,要求总重量不超过 W W​ 的最大价值。

范围: N 20   ;   a , b , W 100 N \le 20~;~a,b,W \le 100

分析:数据很小,使用普通的 d f s dfs 搜索就可以了,注意条件判断即可。详见代码。

Code

const int MAXN = 20+10;

int n, k, maxV, ans;  // maxV是最大重量,ans是最大价值
int necklace[MAXN][2];  // 0表示价值,1表示重量

// cur:当前层数 pick:已选个数 w:当前重量 v:当前价值
void dfs(int cur, int pick, int w, int v){
    // 如果遍历完就进行答案检查并返回
    if(cur == n){
        // 必修准确挑选了k个并且当前重量不超过总重量
        if(pick == k && w <= maxV){
            ans = max(ans, v);  // 更新答案
        }
        return;
    }
    dfs(cur+1, pick+1, w+necklace[cur][1], v+necklace[cur][0]);  // 选择这个石头
    dfs(cur+1, pick, w, v);  // 不选择这个石头
}

int main()
{
    int T;
    cin >> T;
    while(T--){
        cin >> n >> k;
        for(int i = 0; i < n; i++){
            cin >> necklace[i][0] >> necklace[i][1];
        }
        cin >> maxV;
        ans = 0;
        dfs(0, 0, 0, 0);
        cout << ans << endl;
    }
    return 0;
}

1006:变形课(dfs)

题意:给多个字符串,每个字符串都能从第一个字符转换成最后一个字符,问是否能通将 b b 转换成 m m​

范围:无明确指出。

分析:

  1. 只需要关心首尾字母,使用 26 26 v e c t o r vector​ 来保存每个字母能够转移到的字母。
  2. 这样从字母 b b 开始 d f s dfs 搜索,看是否能够转移到字母 m m
  3. 同时注意本题是多组套多组,每一组输入里面都有多个字符串。
  4. 注意使用 v i s vis 数组保存已经尝试过的转移,避免出现 a b   b a ab~ba​ 这样的死循环。

Code

const int MAXN = 26+10;

vector<int> vec[MAXN];  // 表示每个字母能够转移到的字母表
int vis[MAXN][MAXN];

// u是当前的字符
int dfs(int u)
{
    // 如果搜到m,则返回成功
    if (u == 'm' - 'a')  
        return 1;
    // 尝试可以转移到的所有字符
    for (int i = 0; i < vec[u].size(); i++)
    {
        int v = vec[u][i];
        // 如果自己到自己或者已经搜索过了就不进行了
        if (u == v || vis[u][v])
            continue;
        vis[u][v] = 1;  // 标记
        // 如果有可行解,返回成功
        if (dfs(v))
            return 1;
    }
    return 0;  // 找不到可行解
}

int main()
{
    string str;
    while (cin >> str)
    {
        // 注意清空vis数组以及26个vector
        memset(vis, 0, sizeof(vis));  
        for (int i = 0; i < 30; i++)
            vec[i].clear();
        do
        {
            // 因为已经读入了一个string,所以要先判断是不是0
            if (str == "0")
                break;
            // 因为vector的索引是int,所以把字符转换成对应的数字
            int u = str[0] - 'a', v = str[str.length() - 1] - 'a';
            vec[u].push_back(v);
        } while (cin >> str);
        // 判断是否存在可行解
        if (dfs('b' - 'a'))
        {
            cout << "Yes." << endl;
        }
        else
        {
            cout << "No." << endl;
        }
    }
    return 0;
}

1010:Beat(dfs)

题意:一个人想要做尽可能多的题目,现在给二维矩阵 T T​ T [ i ] [ j ] T[i][j]​ 表示解决完问题 i i​ 之后解决问题 j j​ 需要花费的时间,但是要选择的题目的花费时间不能低于上一个题目花费的时间,求最多的题目数。

范围: 2 < n < 15   ;   0 T [ i ] [ j ] < 10 2 < n < 15~;~0 \le T[i][j] < 10

分析: n n 很小,考虑进行搜索。从 0 0 开始,从耗时大于等于之前耗时的题目中进行选择并且标记,在搜索的过程中更新答案。

Notice:注意回溯的时候要取消标记!

Code

const int MAXN = 15 + 10;

int n, ans;
int arr[MAXN][MAXN], vis[MAXN];

// now:当前已经解决的问题 t:上一个耗时 sum:解决的问题数量
void dfs(int now, int t, int sum)
{
    ans = max(ans, sum);  // 随时更新答案
    for (int i = 0; i < n; i++)
    {
        // 已经选过的题目以及耗时不满足要求的题目跳过
        if (vis[i] || arr[now][i] < t)
            continue;
        vis[i] = 1;
        dfs(i, arr[now][i], sum + 1);
        vis[i] = 0;  // 注意回溯时取消标记
    }
}

int main()
{
    while (cin >> n)
    {
        memset(vis, 0, sizeof(vis));  // 多组输入,注意清空
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                cin >> arr[i][j];
            }
        }
        ans = 0;  
        vis[0] = 1; 
        dfs(0, 0, 1);
        cout << ans << endl;
    }
    return 0;
}

1013:Prime Ring Problem(dfs)

题意:有一个 N N 个点的环,把 1 1 n n 这些数字放在点中,按字典序输出所有让环上相邻两个点的和是质数的方案。

范围: 0 < n < 20 0<n<20

分析:数据量小,且要求输出所有合法方案,考虑 d f s dfs 搜索。为了保证答案的字典序,在搜索的时候从 1 1 n n​ 枚举就可以满足。同时需要记录每个数字是否出现过,并且注意第一个数字一定要是1,素数判断应该都会了吧。

Code

const int MAXN = 20 + 10;

int n;
int ans[MAXN], vis[MAXN];  // ans保存可行解

// 判断素数
int isPrime(int x)
{
    for (int i = 2; i <= sqrt(x); i++)
    {
        if (x % i == 0)
            return 0;
    }
    return 1;
}

// cur:当前的层数
void dfs(int cur)
{
    // 当全部填完时,还要判断头尾之和是否是素数
    if (cur == n && isPrime(ans[0] + ans[n - 1]))
    {
        // 输出答案
        for (int i = 0; i < n; i++)
        {
            if (i)
                cout << " ";
            cout << ans[i];
        }
        cout << endl;
        return;
    }
    // 从1-n枚举
    for (int i = 1; i <= n; i++)
    {
        // 用过不选
        if (vis[i])
            continue;
        // 不满足条件不选
        if (!isPrime(i + ans[cur - 1]))
            continue;
        ans[cur] = i;  // 记录答案
        vis[i] = 1;  // 进行标记
        dfs(cur + 1);
        vis[i] = 0;  // 回溯注意取消标记
    }
}

int main()
{
    int kase = 1;
    while (cin >> n)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cout << "Case " << kase++ << ":" << endl;
        ans[0] = 1;  // 以1打头
        vis[1] = 1;  // 标记1已经用过
        dfs(1);
        cout << endl;  // 输出块之间有空行
    }
    return 0;
}

1014:A strange lift(bfs)

题意:坐电梯,有 N N​ 个楼层,每个楼层都有标志 K i K_i​ 表示在该层能够往上 K i K_i​ 或者往下 K i K_i​ 层,电梯层数 1 \ge1​ N \le N​ 。现在问从 A A​ 层到 B B​ 层至少需要按几次按钮,如果不能到则输出 1 -1​

范围: 1 N , A , B 200 1 \le N,A,B \le 200​

分析:典型的 b f s bfs 搜索求最小的步骤数,大家多练练手。

Notice:坑爹,这个 K i K_i 可以是负数!

Code

const int MAXN = 200 + 10;

int n, a, b;
int k[MAXN], vis[MAXN];  // bfs搜索必备vis标记数组

// bfs搜索必备结构体,内容视题目而定
struct Node
{
    int floor, step;  // 结构体中step不可少
} temp, q;  // 必备的两个实例

int bfs()
{
    queue<Node> now;  // bfs搜索必备队列
    // 初始化
    q.floor = a;  
    q.step = 0;
    now.push(q); 
    vis[a] = 1;
    // 只要非空就一直处理
    while (!now.empty())
    {
        q = now.front();  // 取队首
        now.pop();  // 弹队首
        // 到达目标层数,直接输出步骤数,肯定是最小步骤数
        if (q.floor == b)
        {
            return q.step;
        }
        // 利用i是-1和1来控制是往上还是往下
        for (int i = -1; i <= 1; i += 2)
        {
            temp = q;  // 备份,避免影响q
            int newFloor = q.floor + i * k[q.floor];  // 新层数
            // 访问过或者越界 跳过
            if (vis[newFloor] || newFloor < 1 || newFloor > n)
                continue;
            // 标记加入队列继续搜索
            vis[newFloor] = 1;
            temp.floor = newFloor;
            temp.step++;
            now.push(temp);
        }
    }
    return -1;  // 搜完了还没到,则返回不可能
}

int main()
{
    while (cin >> n, n)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cin >> a >> b;
        for (int i = 1; i <= n; i++)
        {
            cin >> k[i];
        }
        cout << bfs() << endl;
    }
    return 0;
}

1015:Knight Moves(bfs)

题意:在 8 8 8*8​ 的棋盘上,可以 8 8​ 个方向日字格行进,问从 ( s x , s y ) (sx,sy)​ ( e x , e y ) (ex,ey)​ 所需要的最少步骤。

范围:均小于 8 8​

分析:简单经典 b f s bfs​ 搜索,大家继续练手。

Code

const int MAXN = 8 + 10;

int n;
string s1, s2;
int sx, sy, ex, ey;  // 起点与终点位置
int direct[8][2] = {{-1, -2}, {-1, 2}, {-2, -1}, {-2, 1}, {1, -2}, {1, 2}, {2, -1}, {2, 1}};  // 八个方向
int vis[MAXN][MAXN];

struct Node
{
    int x, y;  // 当前位置
    int step;
} temp, q;

void bfs()
{
    queue<Node> now;
    q.x = sx, q.y = sy;
    q.step = 0;
    vis[sx][sy] = 1;
    now.push(q);
    while (!now.empty())
    {
        q = now.front();
        now.pop();
        // 到达终点,输出答案并返回
        if (q.x == ex && q.y == ey)
        {
            cout << "To get from " << s1 << " to " << s2 << " takes " << q.step << " knight moves." << endl;
            return;
        }
        // 八个方向,满足条件就标记然后加入队列
        for (int i = 0; i < 8; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            if (newx <= 0 || newx > 8 || newy <= 0 || newy > 8)
                continue;
            if (vis[newx][newy])
                continue;
            vis[newx][newy] = 1;
            temp.step++;
            temp.x = newx, temp.y = newy;
            now.push(temp);
        }
    }
}

int main()
{
    while (cin >> s1 >> s2)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        // 处理得到起始位置以及终点位置
        sx = s1[0] - 'a' + 1;
        sy = s1[1] - '0';
        ex = s2[0] - 'a' + 1;
        ey = s2[1] - '0';
        bfs();
    }
    return 0;
}

1016:Catch That Cow(bfs)

题意:农夫在位置 N N ,奶牛在位置 K K ,农夫可以选择移动 ( X X 1     X X + 1 ) (X\rightarrow X-1~||~X\rightarrow X+1) 或者传送 ( X 2 X ) (X\rightarrow 2*X)​ ,假设奶牛不动,问农夫至少需要多少步才能抓到奶牛。

范围: N [ 1 , 1 e 5 ]   ;   K [ 0 , 1 e 5 ] N \in [1,1e5]~;~K\in [0, 1e5]

分析:依旧是简单的 b f s bfs 搜索,练手。

Code

const int MAXN = 1e5 + 10;

int n, k;
int vis[MAXN];

struct Node
{
    int x, step;  // x:当前位置
} temp, q;

int bfs()
{
    queue<Node> que;
    q.x = n;
    q.step = 0;
    vis[n] = 1;
    que.push(q);
    while (!que.empty())
    {
        q = que.front();
        que.pop();
        // 如果抓到奶牛,则返回步骤数
        if (q.x == k)
        {
            return q.step;
        }
        // 同样是利用i为1或者-1来控制左右移动
        for (int i = -1; i <= 1; i += 2)
        {
            temp = q;
            int newx = temp.x + i;
            if (newx < 0 || newx > 100000)
                continue;
            if (vis[newx])
                continue;
            vis[newx] = 1;
            temp.x = newx;
            temp.step++;
            que.push(temp);
        }
        // 还有传送的情况
        temp = q;
        int newx = temp.x * 2;
        if (newx < 0 || newx > 100000)
            continue;
        if (vis[newx])
            continue;
        vis[newx] = 1;
        temp.x = newx;
        temp.step++;
        que.push(temp);
    }
    return -1;  // 肯定能抓到,所以这一句并没有作用
}

int main()
{
    while (cin >> n >> k)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cout << bfs() << endl;
    }
    return 0;
}

1020:Asteroids!(三维bfs)

题意:有一个 N N N N*N*N​ 的空间,空间中有陨石不能进入,每秒可以上下左右前后六个方向行进,求从 A A​ B B​ 的最短时间。

范围: 1 N 10 1\le N\le 10​

分析:三维的 b f s bfs​ ,跟普通的 b f s bfs​ 一样写就可以了。

Code

const int MAXN = 10 + 10;

int n;
int sx, sy, sz, ex, ey, ez;
int direct[6][3] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}};  // 上下左右前后
int g[MAXN][MAXN][MAXN], vis[MAXN][MAXN][MAXN];  // 标记数组自然也是三维的

struct Node
{
    int x, y, z;  // 表示当前位置
    int step;
} temp, q;

void bfs()
{
    queue<Node> now;
    q.x = sx, q.y = sy, q.z = sz;
    q.step = 0;
    vis[sz][sx][sy] = 1;
    now.push(q);
    while (!now.empty())
    {
        q = now.front();
        now.pop();
        // 到了终点就输出最短时间
        if (q.x == ex && q.y == ey && q.z == ez)
        {
            cout << n << " " << q.step << endl;
            return;
        }
        // 六个方向进行判断
        for (int i = 0; i < 6; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1], newz = temp.z + direct[i][2];
            if (newx < 0 || newx >= n || newy < 0 || newy >= n || newz < 0 || newz >= n)
                continue;
            if (vis[newz][newx][newy])
                continue;
            int current = g[newz][newx][newy];
            if (current)
                continue;
            vis[newz][newx][newy] = 1;
            temp.step++;
            temp.x = newx, temp.y = newy, temp.z = newz;
            now.push(temp);
        }
    }
    cout << "NO ROUTE" << endl;  // 无法到达
}

signed main()
{
    string str;
    while (cin >> str >> n)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        for (int z = 0; z < n; z++)
        {
            for (int x = 0; x < n; x++)
            {
                for (int y = 0; y < n; y++)
                {
                    char ch;
                    cin >> ch;
                    if (ch == 'O')  // 把空间进行转换
                        g[z][x][y] = 0;
                    else
                        g[z][x][y] = 1;
                }
            }
        }
        cin >> sx >> sy >> sz;
        cin >> ex >> ey >> ez;
        cin >> str;
        bfs();
    }
    return 0;
}

二、medium

1002:Sum It Up(dfs+map套vector)

题意:给 n n​ 个数字 X 1 . . . X n X_1 ... X_n​ ,需要使用其中的若干个数字凑成 t t​ ,求出所有的不重复方案并且按照 ”字典序“ 降序输出。

范围: t 1000   ;   1 n 12   ;   X i 1000 t\le 1000~;~1 \le n \le 12~;~X_i \le 1000

分析:

  1. 每个数字要么选,要么不选,考虑 d f s dfs​ 搜索。
  2. 需要降序输出,所以先对数组进行降序排序。
  3. 输出的答案需要保证 “字典序” 降序,所以在搜索时先搜索 “选择”,然后再搜索 “不选择”。
  4. 答案需要保证唯一性,所以使用了 v e c t o r vector​ i n t int​ 类型的映射 m a p map​ ,相同的 v e c t o r vector​ 会被标记。

Code

const int MAXN = 12+10;

int t, n, ok;  // ok标志是否有可行解
int arr[MAXN];
vector<int> ans;  // 保存一组可行解
map<vector<int>, int> vis;  // 对可行解进行标记

// cur表示当前的层数,sum表示当前选的数字之和
void dfs(int cur, int sum){
    // 如果遍历完了就可以检查答案后返回了
    if(cur == n){
        // sum必须跟t相同,并且该组解没有记录过
        if(sum == t && !vis.count(ans)){
            ok = 1;  // 有可行解
            vis[ans] = 1;  // 标记
            // 输出答案,保证是降序输出
            for(int i = 0; i < ans.size(); i++){
                if(i) cout << "+";
                cout << ans[i];
            }
            cout << endl;
        }
        return;
    }
    ans.push_back(arr[cur]);  // 先“选”
    dfs(cur+1, sum+arr[cur]);
    ans.pop_back();  // 再“不选”
    dfs(cur+1, sum);   
}

int main()
{
    while(cin >> t >> n, t+n){
        // 初始化ok,清空两个容器
        ok = 0;
        ans.clear();
        vis.clear();
        for(int i = 0; i < n; i++){
            cin >> arr[i];
        }
        // 先降序排序,greater<int>()就是降序的规则
        sort(arr, arr+n, greater<int>());
        cout << "Sums of " << t << ":" << endl;
        dfs(0, 0);
        // 如果没有可行解,则输出NONE
        if(!ok){
            cout << "NONE" << endl;
        }
    }
	return 0;
}

1004:下沙小面的(2)(去重+全排列)

题意:给 N N 个城市之间的最短距离,用矩阵表示,现在有 K K 个乘客上车,求最短的路线把这些乘客送到目的地。

范围: N [ 3 , 30 ] , K [ 1 , 7 ] N \in [3, 30], K \in [1, 7]​

分析:由于最多只有 7 7 个人, 7 ! = 5040 7! = 5040 ,所以我们可以枚举这些乘客目的地顺序的所有方案,然后取其中路线最短的就可以了。需要注意的是有的乘客的目的地可以相同,我们只需要取目的地不同的乘客当做有效乘客就可以了。(这里的去重可以学习一下,以后的离散化处理也会用到!)

Notice:通过本题希望掌握 S T L STL 库中的全排列 n e x t _ p e r m u t a t i o n next\_permutation 的使用以及 s o r t sort 搭配 u n i q u e unique 使用实现去重。

Code

const int MAXN = 30+10;
const int INF = 0x3f3f3f3f;

int n, k;
int dis[MAXN][MAXN], arr[MAXN];

int main()
{
    while(cin >> n, n){
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                cin >> dis[i][j];
            }
        }
        cin >> k;
        for(int i = 0; i < k; i++){
           cin >> arr[i];
        }
        sort(arr, arr+k);  // 先把乘客目的地排序
        int len = unique(arr, arr+k)-arr;  // 去掉多余的相同目的地的乘客
        int ans = INF;
        do{
            int sum = 0;
            int now = 0;
            for(int i = 0; i < len; i++){  // 计算这种方案下的路线长度
                sum += dis[now][arr[i]];
                now = arr[i];
            }
            ans = min(ans, sum);  // 更新答案
        }while(next_permutation(arr, arr+len));  // 全排列
        cout << ans << endl;
    }
    return 0;
}

1007:哈密顿绕行世界问题(dfs+排序)

题意:一个规则的实心十二面体,它的 20 20​ 个顶点标出世界著名的 20 20​ 个城市,输出从第 m m​ 个城市出发经过每个城市 1 1​ 次又回到 m m​ 的所有路线,如有多条路线,按字典序输出,每行 1 1​ 条路线。

范围: m [ 1 , 20 ] m \in [1, 20]​

分析: d f s dfs 搜索,因为最后需要按照字典序输出所有的路径,所以先对每个点相邻的三个点进行排序,这样搜索的时候就可以保证字典序递增。注意最后还需要回到 m m 点,使用数组或者 v e c t o r vector 来保存路径

Code

const int MAXN = 20 + 10;

int m, cnt;  // cnt保存路径的标号
int arr[MAXN][3];
int vis[MAXN];  // 保存城市是否走过
vector<int> path;  // 保存路径

// 检查20个城市否则全部都正好走过1遍
int check()
{
    for (int i = 1; i <= 20; i++)
    {
        if (vis[i] == 0)
            return 0;
    }
    return 1;
}

// u表示当前城市序号
void dfs(int u)
{
    // 如果回到了m城市并且所有城市正好所有城市走过1遍则输出答案
    if (check() && u == m)
    {
        cout << cnt++ << ":  " << m;
        for (int i = 0; i < path.size(); i++)
        {
            cout << " " << path[i];
        }
        cout << endl;
    }
    // 每个城市都有3个相邻的城市
    for (int i = 0; i < 3; i++)
    {
        int v = arr[u][i];
        // 如果已经走过就不走了
        if (vis[v])
            continue;
        vis[v] = 1;  // 标记
        path.push_back(v);  // 记录路径
        dfs(v);
        vis[v] = 0;  // 取消标记
        path.pop_back();  // 从该路径中删除
    }
}

int main()
{
    for (int i = 1; i <= 20; i++)
    {
        cin >> arr[i][0] >> arr[i][1] >> arr[i][2];
        sort(arr[i], arr[i] + 3);  // 先对3个下个城市排序,保证字典序
    }
    // 注意有多个m
    while (cin >> m, m)
    {
        // 初始化路径的序号、保存路径的vector以及vis数组
        cnt = 1;
        path.clear();
        memset(vis, 0, sizeof(vis));
        dfs(m);
    }
    return 0;
}

1008:N皇后问题(dfs+打表)

题意:在 N N N*N 的方格棋盘放置了 N N 个皇后,使得它们不相互攻击(即任意 2 2 个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成 45 45​ 角的斜线上)

你的任务是,对于给定的 N N​ ,求出有多少种合法的放置方法。

范围: N 10 N \le 10

分析: N N 皇后问题用 d f s dfs 搜索本来是可以直接解决的,但是这道题目坑的地方在于测试用例太多了,所以要打表。

每一行只需要放一个皇后,注意判断该位置是否与前面的皇后冲突。

注意判断两个皇后是否在同一个对角线上的方法:

皇后 a a​ 的坐标 x 1 , y 1 x_1,y_1​ ,皇后 b b​ 坐标 x 2 , y 2 x_2,y_2​

主对角线: y = x y = x​ ,那么满足 y 2 y 1 x 2 x 1 = 1 \frac{y_2-y_1}{x_2-x_1}=1​ ,即 x 1 + y 2 = x 2 + y 1 x_1+y_2=x_2+y_1​

副对角线: y = x y=-x ,那么满足 y 2 y 1 x 2 x 1 = 1 \frac{y_2-y_1}{x_2-x_1}=-1 ,即 x 1 + y 1 = x 2 + y 2 x_1+y_1=x_2+y_2

Code

const int MAXN = 10 + 10;

int n, ans;  // ans保存方案数
int pos[MAXN], res[MAXN];  // pos保存前面皇后的位置,res保存打表的结果

// 检查在cur行now列放置皇后是否会引发冲突
int check(int cur, int now)
{
    // 取出之前的每一个皇后
    for (int i = 0; i < cur; i++)
    {
        // 如果在同列或者两者在同一个对角线上,则冲突
        if (pos[i] == now || cur + now == i + pos[i] || cur + pos[i] == i + now)
        {
            return 0;
        }
    }
    return 1;  // 返回无冲突
}

// cur代表当前的层数
void dfs(int cur)
{
    // 如果成功放置了n个皇后,那么方案数++
    if (cur == n)
    {
        ans++;
        return;
    }
    // 尝试在该行每一个格子上放皇后
    for (int i = 0; i < n; i++)
    {
        // 如果这个位置放皇后跟前面的皇后不冲突,则继续搜索
        if (check(cur, i))
        {
            pos[cur] = i;  // 记录皇后的位置
            dfs(cur + 1);
            pos[cur] = 0;  // 删除皇后的位置记录
        }
    }
}

int main()
{
    // 1~10打表
    for (n = 1; n <= 10; n++)
    {
        // 注意初始化
        memset(pos, 0, sizeof(pos));  
        ans = 0;
        dfs(0);
        res[n] = ans;
    }
    while (cin >> n, n)
    {
        cout << res[n] << endl;  // 直接输出答案
    }
    return 0;
}

1009:非常可乐(bfs+优化)

题意:有一瓶可乐以及两个容器,容量分别为 S , A , B S, A, B​ ,但是都没有刻度,它们三个之间能够互相倒可乐,问能否平分可乐,能则输出最少步骤。

范围: 0 < S < 101   ;   S = N + M   ;   N , M > 0 0<S<101~;~S=N+M~;~N,M>0

分析:问最小的步骤,那么自然想到 b f s bfs ,这也是 b f s bfs 的经典题目,需要仔细理解!每次操作选择一个非空的容器向另一个未满的容器倒水,有三个容器,所以每次最多只有六种倒的方法。因此将每次倒水的状态转移在 b f s bfs 中实现就可以了,需要注意一下写法。详见代码。

加了一个小优化,就是可乐的容量是奇数的时候肯定是不能均分的,直接返回 N O NO​ 就可以了

Notice:因为包含多组输入,而每次输入都需要初始化 v i s vis​ 数组需要花费很多时间,注意数组大小!

Code

const int MAXN = 105;

int maxV[3];  // 0,1保存两个容器大小,2保存可乐的大小
int vis[MAXN][MAXN][MAXN];  // bfs必备数组vis,保存搜索过的状态,遇到时不再搜索

// bfs必备结构体,内容视题目而定
struct Node
{
    int cap[3];  // 分别保存各个容器当前可乐的容量
    int step;  // 步骤数,必备
} temp, q;  // temp和q,必备

// bfs的大致框架都差不多,多写写就记住了
void bfs()
{
    queue<Node> now;  // bfs必备队列,先进先出,保证先搜索步骤少的再搜索步骤多的
    // 初始化q为搜索的初态
    q.cap[0] = q.cap[1] = 0;  
    q.cap[2] = maxV[2];
    q.step = 0;
    vis[q.cap[0]][q.cap[1]][q.cap[2]] = 1;  // 标记这个状态
    now.push(q);  // 加入队列开始搜索
    while (!now.empty())  // 只要队列里面还有状态就继续搜索
    {
        q = now.front();  // 取出队首
        now.pop();  // 弹出队首
        // 下面检查是否已经有两个容器中有均分的可乐了,有则返回结果
        for (int i = 0; i < 3; i++)
        {
            for (int j = i + 1; j < 3; j++)
            {
                // 容量相同,且相加的结果就是原本可乐的容量,即均分
                if (q.cap[i] == q.cap[j] && q.cap[i] + q.cap[j] == maxV[2])
                {
                    cout << q.step << endl;  // 因为队列,此时的步数一定是最小的步骤数
                    return;
                }
            }
        }
        // 下面尝试将容器i的可乐倒进容器j
        for (int i = 0; i < 3; i++)
        {
            // 要保证有可乐才能倒
            if (!q.cap[i])
                continue;
            for (int j = 0; j < 3; j++)
            {
                // 如果是自己给自己倒,或者j已经满了,那么就不用倒了
                if (i == j || q.cap[j] == maxV[j])
                    continue;
                temp = q;  // 当需要加入新状态的时候用temp复制一份q,避免改变q
                temp.step++;  // 步骤数++
                // 如果容器i可乐+容器j可乐还装不满容器j
                if (q.cap[i] + q.cap[j] <= maxV[j])
                {
                    temp.cap[j] += temp.cap[i];  // 容器j的可乐加上容器i的可乐
                    temp.cap[i] = 0;  // 容器i的可乐清空
                }
                else  // 否则将容器j装满,容器i的可乐减去相应的容量
                {
                    temp.cap[i] -= maxV[j] - temp.cap[j];
                    temp.cap[j] = maxV[j];
                }
                // 如果当前的状态还没有搜索过,那么加入队列,并且进行标记
                if (!vis[temp.cap[0]][temp.cap[1]][temp.cap[2]])
                {
                    now.push(temp);
                    vis[temp.cap[0]][temp.cap[1]][temp.cap[2]] = 1;
                }
            }
        }
    }
    cout << "NO" << endl;  // 如果所有状态搜索完还是没有找到答案,那么就不可能均分
}

int main()
{
    while (cin >> maxV[2] >> maxV[0] >> maxV[1], maxV[0] + maxV[1] + maxV[2])
    {
        // 如果可乐的容量是奇数,肯定是不能均分的
        if (maxV[2] % 2 == 1)
        {
            cout << "NO" << endl;
            continue;
        }
        // 这个数组只要稍微开大一点就可能过不了了...
        memset(vis, 0, sizeof(vis));
        bfs();
    }
    return 0;
}

1011:Tempter of the Bone(dfs+奇偶性剪枝)

题意:给一个 N M N*M​ 迷宫,问是否能够从当前位置在正好 T T​ 秒时到达出口,每秒可以上下左右走,但是不能停在原地或者返回走过的地方。

范围: 1 < N , M < 7   ;   0 < T < 50 1<N,M<7~;~0<T<50

分析:范围依旧很小,考虑 d f s dfs 搜索,但是这道题目仅仅使用普通的搜索会超时,需要加上奇偶性剪枝。

如果剩余时间与最短距离的奇偶性不同,那么这种情况就不可能在 T T 秒时到出口,因为你不可以停在原地,如果到达终点的时候时间还差 1 1 秒,你走到其他地方再走回来的时间必定是偶数,因此无法逃出。

为什么是最短距离,不是其他的距离呢?动手画画,可以发现在当前位置如果不走最短路线的话,不管怎么走到达出门处的距离,其奇偶性与最短距离一致。所以只要奇偶性不同,就可以直接剪枝。

Code

const int MAXN = 7 + 10;

int n, m, t;
int X1, Y1,X2, Y2;  //表示起始和出口位置
char g[MAXN][MAXN];
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};  // 方向移动数组
int vis[MAXN][MAXN];  // 标记数组

// 计算最短距离与当前剩余时间的奇偶性是否相同
// x和y表示当前位置,now表示已经使用的时间
int calc(int x, int y, int now)
{
    int need = abs(x - X2) + abs(y - Y2);  // 至少需要need步才能出口
    if ((t-now) % 2 != need % 2)  // 剩余时间t-now
        return 0;
    else
        return 1;
}

// x和y:当前位置 now:已经使用的时间
int dfs(int x, int y, int now)
{
    // 在时间t时到达出口
    if (g[x][y] == 'D' && now == t)
        return 1;
    // 奇偶性不同,剪枝
    if (calc(x, y, now) == 0)
        return 0;
    vis[x][y] = 1;
    for (int i = 0; i < 4; i++)
    {
        int newx = x + direct[i][0], newy = y + direct[i][1];  // 新的位置
        // 越界 不合法
        if (newx < 0 || newx >= n || newy < 0 || newy >= m)  
            continue;
        // 走过 不合法
        if (vis[newx][newy])  
            continue;
        char current = g[newx][newy];
        // 障碍 不合法
        if (current == 'X')
            continue;
        // 如果可以在t时到达出口,返回结果1
        if (dfs(newx, newy, now + 1))
            return 1;
    }
    vis[x][y] = 0;  // 回溯取消标记
    return 0;
}

int main()
{
    while (cin >> n >> m >> t, n + m + t)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                // 记录起点
                if (g[i][j] == 'S')
                {
                    X1 = i, Y1 = j;
                }
                // 记录出口
                if (g[i][j] == 'D')
                {
                    X2 = i, Y2 = j;
                }
            }
        }
        if (dfs(X1, Y1, 0))
        {
            cout << "YES" << endl;
        }
        else
        {
            cout << "NO" << endl;
        }
    }
    return 0;
}

1012:连连看(dfs+优化)

题意:连连看背景,二维 N M N*M 的棋盘,有 Q Q 个询问,每次问两个棋子是否能够成功消去,规定只能走内部且转弯次数不能超过 2 2

范围: 0 < N 1000   ;   0 < M < 1000   ;   0 < Q < 50 0<N\le1000~;~0<M<1000~;~0<Q<50

分析:这个数据量挺大的,要是一般做题的话可能就不敢写搜索了,但是既然在这个场子里面,那么就考虑 d f s dfs 搜索了… 这样的话,肯定是需要优化的,事实也是如此,不加入优化或者优化不够那么就稳稳得 T L E TLE 了。优化是当转弯次数大于 2 2 的时候直接返回,当转弯次数为 2 2​ 且当前位置与目标位置不在同一行并且不在同一列的情况直接返回。

Code

const int MAXN = 1000 + 10;

int n, m;
int X1, Y1, X2, Y2;  // 需要消除的两个棋子坐标
int g[MAXN][MAXN], vis[MAXN][MAXN];
int direct[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

// x和y:当前位置 turn:转弯次数 way:从哪个方向来的
int dfs(int x, int y, int turn, int way)
{
    // 如果达到另一个棋子,根据转弯次数返回结果
    if (x == X2 - 1 && y == Y2 - 1)
    {
        if (turn <= 2)
            return 1;
        else
            return 0;
    }
    // 优化1,转弯次数超过2那么肯定就是不能消除了
    if (turn > 2)
        return 0;
    // 优化2,转弯次数为2但是不在同行同列,至少还需要转一次,那么就不能消除
    if (x != X2 - 1 && y != Y2 - 1 && turn == 2)
        return 0;
    vis[x][y] = 1;
    for (int i = 0; i < 4; i++)
    {
        int newx = x + direct[i][0], newy = y + direct[i][1];  // 新的位置
        // 越界
        if (newx < 0 || newx >= n || newy < 0 || newy >= m)
            continue;
        // 走过
        if (vis[newx][newy])
            continue;
        int current = g[newx][newy];
        // 碰到其他棋子
        if ((newx != X2 - 1 || newy != Y2 - 1) && current)
            continue;
        int newturn = turn + 1;
        // 如果是刚刚开始或者方向相同,则不需要增加转弯次数
        if (way == -1 || way == i)
            newturn--;
        // 继续搜索,如果成功,返回结果
        if (dfs(newx, newy, newturn, i))
            return 1;
    }
    vis[x][y] = 0;  // 回溯取消标记
    return 0;
}

int main()
{
    while (cin >> n >> m, n + m)
    {
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
            }
        }
        int q;
        cin >> q;
        for (int i = 0; i < q; i++)
        {
            memset(vis, 0, sizeof(vis));  // 清空数组!
            cin >> X1 >> Y1 >> X2 >> Y2;
            // 这里需要保证两个棋子不是同一个、两个棋子非空且相同、转弯的次数不超过2
            if ((X1 != X2 || Y1 != Y2) && g[X1 - 1][Y1 - 1] && g[X2 - 1][Y2 - 1] && g[X1 - 1][Y1 - 1] == g[X2 - 1][Y2 - 1] && dfs(X1 - 1, Y1 - 1, 0, -1))
            {
                cout << "YES" << endl;
            }
            else
            {
                cout << "NO" << endl;
            }
        }
    }
    return 0;
}

1018:Nightmare(bfs+考虑时间维)

题意:给 N M N*M 的迷宫,有一颗炸弹在 6 6​ 秒之后爆炸,迷宫中存在炸弹时间重置装置,重置的时间忽略不计,每秒钟只能往上下左右四个方向前进,问是否能够走出迷宫,如果能则输出最短时间。

范围: 1 N , M 8 1 \le N, M \le 8​

分析:因为涉及到最短时间并且数据范围很小,所以考虑使用 b f s bfs 搜索。唯一需要注意的是设计标记数组 v i s vis​ 的时候要考虑时间维度的印象,因为不同的时间代表的状态不同。

Code

const int MAXN = 8 + 10;

int n, m, ans;
int sx, sy, ex, ey;
int g[MAXN][MAXN], vis[MAXN][MAXN][6];  // 时间只有6种情况
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

struct Node
{
    int x, y, t;  // 当前的位置以及当前时间(0~6)
    int step;
} temp, p;

int bfs()
{
    queue<Node> now;
    p.x = sx, p.y = sy;
    p.t = p.step = 0;  // 一开始的时间为0
    now.push(p);
    vis[sx][sy][0] = 1;
    while (!now.empty())
    {
        p = now.front();
        now.pop();
        // 保证还没有爆炸
        if (p.t >= 6)  
            continue;
        // 如果到了终点,那么就返回最短时间
        if (p.x == ex && p.y == ey)
        {
            return p.step;
        }
        for (int i = 0; i < 4; i++)
        {
            temp = p;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                continue;
            // 在这个时间点走过这个点就不走了
            if (vis[newx][newy][temp.t + 1])
                continue;
            int current = g[newx][newy];
            if (current == 0)
                continue;
            vis[newx][newy][temp.t + 1] = 1;
            temp.x = newx, temp.y = newy;
            temp.step++;
            // 如果找到了炸弹重置装置且没爆炸则清零
            if (current == 4 && temp.t < 5)
                temp.t = 0;
            else
                temp.t++;
            now.push(temp);
        }
    }
    return -1;  // 走不出去
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cin >> n >> m;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                if (g[i][j] == 2)  // 记录起点
                {
                    sx = i, sy = j;
                }
                if (g[i][j] == 3)  // 记录终点
                {
                    ex = i, ey = j;
                }
            }
        }
        cout << bfs() << endl;
    }
    return 0;
}

1019:诡异的楼梯(bfs+考虑时间维)

题意:给 N M N*M 的矩阵,每秒钟可以上下左右行进,矩阵中有楼梯,如果方向正确的话可以从一端直接移动到另一端,但是楼梯每秒钟都会变换一次方向,现在问从 S S T T​ 的最短时间。

范围: 0 N , M 20 0\le N,M \le 20

分析:依旧是求最短时间,同时数据范围很小,考虑 b f s bfs 搜索。跟上一题一样,也需要考虑时间维度的影响,这题只需要考虑时间的奇偶性就可以了。如果到了楼梯前不能上去的话需要时间++,然后位置不动,加入队列,不然就 W A WA 了。如果楼梯可以通过的话还是要判断经过楼梯的对面位置是否是合法的。

Code

const int MAXN = 20 + 10;

int n, m;
int sx, sy, ex, ey;
char g[MAXN][MAXN];
int vis[MAXN][MAXN][2];  // 只需要考虑奇偶性
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

struct Node
{
    int x, y;  // 当前的位置
    int step;
} temp, q;

int bfs()
{
    queue<Node> now;
    q.x = sx, q.y = sy;
    q.step = 0;
    now.push(q);
    vis[sx][sy][0] = 1;
    while (!now.empty())
    {
        q = now.front();
        now.pop();
        // 到了终点就返回最短时间
        if (q.x == ex && q.y == ey)
        {
            return q.step;
        }
        for (int i = 0; i < 4; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                continue;
            // step%2判断奇偶性
            if (vis[newx][newy][temp.step % 2])
                continue;
            int current = g[newx][newy];
            if (current == '*')
                continue;
            // 如果到的地方是楼梯,另外处理
            if (current == '|' || current == '-')
            {
                // 更新奇偶性变换
                if (temp.step % 2)
                {
                    if (current == '|')
                        current = '-';
                    else
                        current = '|';
                }
                // 方向相同才能通过
                if ((i <= 1 && current == '|') || (i > 1 && current == '-'))
                {
                    newx = newx + direct[i][0], newy = newy + direct[i][1];
                    // 还是要判断楼梯对面是否是合法的
                    if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                        continue;
                    if (vis[newx][newy][temp.step % 2])
                        continue;
                    if (g[newx][newy] == '*')
                        continue;
                    vis[newx][newy][temp.step % 2] = 1;
                    temp.x = newx, temp.y = newy;
                    temp.step++;
                    now.push(temp);
                }
                else  // 否则只能站在原地了
                {
                    temp.step++;
                    now.push(temp);
                }
            }
            else  // 不是楼梯的话就走正常流程了
            {
                vis[newx][newy][temp.step % 2] = 1;
                temp.x = newx, temp.y = newy;
                temp.step++;
                now.push(temp);
            }
        }
    }
    return -1;  // 走不出去
}

signed main()
{
    while (cin >> n >> m)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                if (g[i][j] == 'S')  // 记录起点
                {
                    sx = i, sy = j;
                }
                if (g[i][j] == 'T')  // 记录终点
                {
                    ex = i, ey = j;
                }
            }
        }
        cout << bfs() << endl;
    }
    return 0;
}

1021:Rescue(bfs+优先队列)

题意:给 N M N*M​ 的矩阵,公主的朋友们要来救公主,迷宫中有守卫,有守卫的房间必须多花一个单位的时间才能进入,问援救的最短时间是多少。

范围: N , M 200 N,M \le 200​

分析:明显是 b f s bfs​ ,可以知道公主只有一个但是朋友有多个,那么可以考虑成从公主开始搜索,遇到第一个朋友就返回该时间点。

现在问题在进入守卫的房间时要多花一个时间的代价,那么就会导致队列中状态的时间并不是递增的,所以考虑使用优先队列,对加入的状态会进行排序,取出的队首元素是权值最大的(可以自己定义排序规则实现时间最少的状态在队首)。

Code

const int MAXN = 200 + 10;

int n, m;
int sx, sy;
char g[MAXN][MAXN];
int vis[MAXN][MAXN];
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

struct Node
{
    int x, y;  // 当前位置
    int step;
    // 自定义排序规则,花的时间少的权重越大
    bool operator<(Node other) const
    {
        return step > other.step;
    }
} temp, q;

void bfs()
{
    priority_queue<Node> now;  // 优先队列
    q.x = sx, q.y = sy;
    q.step = 0;
    vis[sx][sy] = 1;
    now.push(q);
    while (!now.empty())
    {
        q = now.top();  // 取出耗时最少的状态
        now.pop();
        // 如果公主遇到了朋友,得到最佳答案
        if (g[q.x][q.y] == 'r')
        {
            cout << q.step << endl;
            return;
        }
        for (int i = 0; i < 4; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                continue;
            if (vis[newx][newy])
                continue;
            char current = g[newx][newy];
            if (current == '#')
                continue;
            vis[newx][newy] = 1;
            temp.x = newx, temp.y = newy;
            temp.step++;
            // 如果有守卫的话还要多花费一个单位时间
            if (current == 'x')
                temp.step++;
            now.push(temp);
        }
    }
    cout << "Poor ANGEL has to stay in the prison all his life." << endl;  // 找不到
}

int main()
{
    while (cin >> n >> m)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                if (g[i][j] == 'a')  // 记录公主位置
                {
                    sx = i, sy = j;
                }
            }
        }
        bfs();
    }
    return 0;
}

1022:逃离迷宫(dfs+剪枝)

题意:跟 1012 1012 连连看差不多的意思,给一个 n m n*m 的矩阵,从一个地方到另一个地方而转弯次数不能超过 k k​ 次。

范围:$1 \le t \le 100;1\le n,m \le100 $

分析:一样的优化,转弯次数 > k > k 或者转弯次数 = k = k​ 而当前位置和目标位置不在同行或同列,则剪枝。

因为这题的数据更大,所以还要其他的优化,比如这里加入了最优性剪枝。这题的 v i s vis 数组,保存的是到位置 ( i , j ) (i, j) 的最小转弯次数,如果到 ( i , j ) (i, j) 时转弯次数已经大于 v i s [ i ] [ j ] vis[i][j] ,那么就可以剪枝掉了。

Code

const int MAXN = 100 + 10;

int n, m, k;
int sx, sy, ex, ey;
char g[MAXN][MAXN];
int vis[MAXN][MAXN];  // 这里的vis跟之前的不一样
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

// x和y:当前位置 turn:转弯次数 way:从哪个方向来的
int dfs(int x, int y, int turn, int way)
{
    // 将两个优化合在一起写了
    int need = turn;
    if (x != ex && y != ey)
        need++;
    if (need > k)
        return 0;
    // 到达终点则可行
    if (x == ex && y == ey)
    {
        return 1;
    }
    for (int i = 0; i < 4; i++)
    {
        int newx = x + direct[i][0], newy = y + direct[i][1], newt = turn;
        if (newx < 0 || newx >= n || newy < 0 || newy >= m)
            continue;
        // 如果不是初始并且方向不同,则转弯次数增加
        if (way != -1 && way != i)
            newt++;
        // 最优性剪枝
        if (vis[newx][newy] != -1 && vis[newx][newy] < newt) // < 不是 <=
            continue;
        int current = g[newx][newy];
        if (current == '*')
            continue;
        vis[newx][newy] = newt;  // 回溯的时候不要修改,关键!这里不是代表有没有走过,而且曾经的最优解!
        if (dfs(newx, newy, newt, i))
            return 1;
        // vis[newx][newy] = -1;
    }
    return 0;
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                vis[i][j] = -1;  // 同时初始化数组
            }
        }
        cin >> k >> sy >> sx >> ey >> ex;
        sy--, sx--, ex--, ey--;  // 统一坐标
        if (dfs(sx, sy, 0, -1))
        {
            cout << "yes" << endl;
        }
        else
        {
            cout << "no" << endl;
        }
    }
    return 0;
}

1023:Dating with girls(2)(bfs)

题意:给 N M N*M 的矩阵,其中有石头不能进入,但是石头在 k k 的整数倍时刻会消失,那么就可以进入,其余时间不可进入。问从 Y Y G G​ 的最短时间。

范围: 1 N , M 100   ;   2 k 10 1 \le N,M \le 100~;~2\le k \le 10

分析:最短时间,数据范围不大,考虑 b f s bfs 搜索,但是需要考虑时间的影响,在同一个点不同的时间最多只有 k k 种不同的状态,所以 v i s vis 数组还要加上一维考虑 k k​

Code

const int MAXN = 100 + 10;

int n, m, k;
int sx, sy, ex, ey;
char g[MAXN][MAXN];
int vis[MAXN][MAXN][11];  // 考虑k的影响
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

struct Node
{
    int x, y;  // 当前位置
    int step;
} temp, q;

void bfs()
{
    queue<Node> now;
    q.x = sx, q.y = sy;
    q.step = 0;
    vis[sx][sy][0] = 0;
    now.push(q);
    while (!now.empty())
    {
        q = now.front();
        now.pop();
        // 如果到终点那么就返回最优解
        if (q.x == ex && q.y == ey)
        {
            cout << q.step << endl;
            return;
        }
        for (int i = 0; i < 4; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            temp.step++;
            if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                continue;
            char current = g[newx][newy];
            // 如果是障碍物并且当前时间不是k的整数倍则不能进入
            if (current == '#' && temp.step % k != 0)
                continue;
            // 如果已经在该类的时间上走过这个点就跳过
            if (vis[newx][newy][temp.step % k])
                continue;
            vis[newx][newy][temp.step % k] = 1;
            temp.x = newx, temp.y = newy;
            now.push(temp);
        }
    }
    cout << "Please give me another chance!" << endl;  // 找不到女朋友咯
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cin >> n >> m >> k;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                if (g[i][j] == 'Y')  // 记录起点
                {
                    sx = i, sy = j;
                }
                if (g[i][j] == 'G')  // 记录终点
                {
                    ex = i, ey = j;
                }
            }
        }
        bfs();
    }
    return 0;
}

1024:A计划(bfs)

题意:两层 N M N*M​ 的迷宫,迷宫中有障碍不能进入,同时有传送机可以传送到另外一层,骑士在入口 ( 0 , 0 , 0 ) (0,0,0)​ 开始寻找公主,问是否能够在 T T​ 时刻找到公主。

范围: 1 N , M 10   ;   T 1\le N, M \le 10~;~T 的范围不清楚

分析:使用 b f s bfs​ 搜索,注意一些细节即可。

本题最坑的地方在于题目没有交代清楚,就是骑士可以待在原地!所以只要到达公主的时间不超过 T T​ 就可以了。

Code

const int MAXN = 10 + 10;

int n, m, t;
int ex, ey, ez;
char g[2][MAXN][MAXN];
int vis[2][MAXN][MAXN];
int direct[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

struct Node
{
    int x, y, z;  // 当前的位置
    int step;
} temp, q;

int bfs()
{
    queue<Node> now;
    q.x = q.y = q.z = q.step = 0;
    vis[0][0][0] = 1;
    now.push(q);
    while (!now.empty())
    {
        q = now.front();
        now.pop();
        // 剪枝,如果已经超过了T,那么肯定救不到了
        if (q.step > t)
            return 0;
        // 如果第一次接触到了公主并且时间满足条件则成功,否则救不到
        if (q.x == ex && q.y == ey && q.z == ez)
        {
            if (q.step <= t)
                return 1;
            else
                return 0;
        }
        for (int i = 0; i < 4; i++)
        {
            temp = q;
            int newx = temp.x + direct[i][0], newy = temp.y + direct[i][1];
            int newz = temp.z;
            if (newx < 0 || newx >= n || newy < 0 || newy >= m)
                continue;
            char current = g[newz][newx][newy];
            if (current == '*')
                continue;
            if (vis[newz][newx][newy])
                continue;
            vis[newz][newx][newy] = 1;
            temp.x = newx, temp.y = newy;
            temp.step++;
            // 如果是传送机的话要判断另一层对应位置是否是合法的
            if (current == '#')
            {
                newz = newz == 0 ? 1 : 0;
                if (g[newz][newx][newy] == '*' || g[newz][newx][newy] == '#' || vis[newz][newx][newy])
                    continue;
                vis[newz][newx][newy] = 1;
            }
            temp.z = newz;
            now.push(temp);
        }
    }
    return 0;
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(vis, 0, sizeof(vis));  // 清空数组
        cin >> n >> m >> t;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[0][i][j];
                if (g[0][i][j] == 'P')  // 记录公主的位置
                {
                    ex = i, ey = j, ez = 0;
                }
            }
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[1][i][j];
                if (g[1][i][j] == 'P')
                {
                    ex = i, ey = j, ez = 1;
                }
            }
        }
        if (bfs())
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}

三、hard

1003:Zipper(记忆化dfs)

题意:给三个字符串 A , B , C A, B, C C C 的长度为 A , B A,B 的长度之和,问 C C 是否能够通过混合 A , B A,B 字符串得到,但是要保证 C C 中的字符其顺序在源串 A A B B 中的顺序没有发生改变。例如 A = c a t , B = t r e e A=cat,B=tree ,如果 c = t c r a e t e c=tcraete 则是合法的,如果 c = c a t r t e e c=catrtee 则是不合法的,此时 t r e e > r t e e tree->rtee ,顺序发生改变。

范围: 1 T 1000   ;   A , B 200   ;   C = A + B 1\le T \le 1000~;~|A|,|B|\le200~;~|C|=|A|+|B|

分析:最多有 1000 1000 个测试集,每个测试集中 A , B A,B 字符串的长度最多为 200 200 ,即 C C 最长为 400 400

寻找可行解,考虑使用 d f s dfs 搜索,设置三个索引 i n d e x 1 , i n d e x 2 , i n d e x 3 index1, index2, index3 ,分别是三个串的当前匹配位置。

对于 C [ i n d e x 3 ] C[index3] 这个字符,如果 C [ i n d e x 3 ] A [ i n d e x 1 ] & & C [ i n d e x 3 ] B [ i n d e x 2 ] C[index3] \ne A[index1] \&\& C[index3] \ne B[index2] ,那么表示匹配失败了;

如果 C [ i n d e x 3 ] = = A [ i n d e x 1 ] C[index3] == A[index1] ,那么可以尝试假设这个字符就是从 A A 拿来的,那么 i n d e x 1 + + , i n d e x 3 + + index1++,index3++ ,继续往下搜索;

如果 C [ i n d e x 3 ] = = B [ i n d e x 2 ] C[index3] == B[index2] ,那么可以尝试假设这个字符就是从 B B 拿来的,那么 i n d e x 2 + + , i n d e x 3 + + index2++,index3++ ,继续往下搜索;

i n d e x 3 = = C . l e n g t h ( ) index3 == C.length() 时,说明匹配完成,找到可行解。

如果只按照这样的思路写代码,最后是会超时的!因为这样的搜索过程可能会出现很多重复的情况,所以考虑记忆化搜索!这是典型的利用空间换时间的办法,一定要掌握,同学们要记得自行网上学习哦。

我们可以设置三维数组 r e s [ i ] [ j ] [ k ] res[i][j][k] 表示当前 A A 匹配到 i i 位置, B B 匹配到 j j 位置, C C 匹配到 k k 位置时的匹配结果,这样的话遇到相同的情况就不需要再去搜索,而是直接返回结果。

不过问题在于这个数组大小至少需要 200 200 400 = 16000000 200*200*400=16000000 ,能开下这个数组就已经很极限了,而且在 1000 1000 个测试集开始的时候都需要对这个数组进行初始化,那么这里花的时间已经超时了,继续考虑优化!

这里有很重要的一点 C C 的长度为 A , B A,B 字符串的长度之和,那么其实我们知道当前的 i n d e x 1 , i n d e x 2 index1,index2 i n d e x 3 index3 也已经可以得知了!所以 r e s res 数组的第三维是不需要的,故 r e s [ i ] [ j ] res[i][j] 表示 A A 匹配到 i n d e x 1 index1 以及 B B 匹配到 i n d e x 2 index2 时的结果就可以了。这样就足以通过本题了!详见代码。

Code

const int MAXN = 200+10;

string a, b, c;
int res[MAXN][MAXN];

int dfs(int index1, int index2, int index3)
{
    if(res[index1][index2]) return res[index1][index2];  // 如果已经求解过,直接返回结果
    if (index3 == c.length())  // 匹配成功
    {
        return 1;
    }
    if (index1 < a.length() && a[index1] == c[index3])  // 假设从A拿来
    {
        if (dfs(index1 + 1, index2, index3 + 1) == 1)  // 匹配成功就返回
            return 1;
    }
    if (index2 < b.length() && b[index2] == c[index3])  // 假设从B拿来
    {
        if (dfs(index1, index2 + 1, index3 + 1) == 1)  // 匹配成功就返回
            return 1;
    }
    return res[index1][index2] = -1;  // 匹配失败
}

int main()
{
    int T;
    cin >> T;
    int kase = 1;
    while (T--)
    {
        memset(res, 0, sizeof(res));  // 清空数组
        cin >> a >> b >> c;
        cout << "Data set " << kase++ << ": ";
        if (dfs(0, 0, 0) != -1)
        {
            cout << "yes" << endl;
        }
        else
        {
            cout << "no" << endl;
        }
    }
    return 0;
}

1017:Knight’s Trip(思维/数学)

题意:背景与 1015 1015 相同,也是只能走日字格,问从 ( 0 , 0 ) (0,0) ( x , y ) (x, y) 最少需要多少步。

范围: x , y 1 e 9 x,y \le 1e9​

分析:绝对不是搜索了… 这个数据量吐了。 1015 1015 的数据最大值是 8 8 ,这里直接飙到 1 e 9 1e9 ,只能找规律或者看看有没有数学解法了。

首先,因为 x , y x,y 可正可负,但是不论是正是负都是等价的,因此可以把终点 ( x , y ) (x, y) 转换到第一象限方便统一处理。

分类讨论:

  1. y = 2 x y=2x​ ,那么一直走 ( 1 , 2 ) (1, 2)​ 就可以,因此步数是 x x​ ,也可以说是 x + y 3 \frac{x+y}{3}​

在这里插入图片描述

  1. y < 2 x y < 2x​

    ( 1 )   ( x + y ) % 3 = = 0 (1)~(x+y)\%3 == 0 ,那么我们可以通过控制 ( 1 , 2 ) (1, 2) ( 2 , 1 ) (2, 1) 这两种跳法达到,步数为 2 x 3 \frac{2x}{3} 2 y 3 \frac{2y}{3} ,也可以是 x + y 3 \frac{x+y}{3}

在这里插入图片描述

( 2 )   ( x + y ) % 3 = = 1 (2) ~(x+y)\%3 == 1 ,我们可以先按照上面 ( 1 ) (1) 走到 ( x , y 1 ) (x, y-1) 的位置 ( ( x 1 , y ) (x-1, y) 的情况类似,不画出来了),这个时候发现不能直接从 ( x , y 1 ) (x, y-1) 直接到 ( x , y ) (x, y) ,那么我们后撤一步,然后如图所示跳两步就可以到了,那么步数就多了一步,即 x + y 3 + 1 \frac{x+y}{3}+1

在这里插入图片描述
在这里插入图片描述

( 3 )   ( x + y ) % 3 = = 2 (3)~(x+y)\%3 == 2 ,还是按照 ( 1 ) (1) 走到 ( x 1 , y 1 ) (x-1,y-1) 的位置( ( x 2 , y ) (x-2, y) ( x , y 2 ) (x, y-2) 的情况自己动手推一下),这个时候只需要两步就可以到 ( x , y ) (x, y) ,因此步数为 x + y 3 + 2 \frac{x+y}{3}+2

在这里插入图片描述在这里插入图片描述

  1. y > 2 x y > 2x​ ,可以用 x x​ 步跳到 ( x , 2 x ) (x, 2x)​ ,那么问题就转换成从 ( 0 , 0 ) (0, 0)​ ( 0 , y 2 x ) (0, y-2x)​ 的最小步骤

    ( 1 )   ( y 2 x ) % 4 = = 0 (1)~(y-2x)\%4 == 0 ,那么可以通过控制 ( 1 , 2 ) (1,2) ( 1 , 2 ) (-1, 2) 两种跳法来到达终点,因此步数为 y 2 x 2 \frac{y-2x}{2}​

在这里插入图片描述

( 2 )   ( y 2 x ) % 4 = = 1 (2)~(y-2x)\%4 == 1 ,按照 ( 1 ) (1) 的步骤先后撤两步,然后通过三步到达,因此步数为 y 2 x 2 + 1 \frac{y-2x}{2}+1

在这里插入图片描述

( 3 )   ( y 2 x ) % 4 = = 2 (3)~(y-2x)\%4 == 2 ,在 ( 1 ) (1) 的基础上再走两步就可以到,因此步数为 y 2 x 2 + 2 \frac{y-2x}{2}+2​

在这里插入图片描述

( 4 ) ( y 2 x ) % 4 = = 3 (4) (y-2x)\%4 == 3 ,在 ( 1 ) (1) 的基础上再走三步就可以到,因此步数为 y 2 x 2 + 3 \frac{y-2x}{2}+3

在这里插入图片描述

讨论到此结束,其中还有许多细节,详见代码。

参考 https://www.cnblogs.com/YY56/p/4954115.html

Code

int main()
{
    string str;
    while (cin >> str)
    {
        if (str == "END")
            break;
        int x, y;
        x = atoi(str.c_str());  // string转int
        cin >> y;
        // 转换到第一象限
        if (x < 0)
            x = -x;
        if (y < 0)
            y = -y;
        // 要保证y>=x才能进行上述的讨论
        if (y < x)
            swap(x, y);
        if (y == 2 * x)
        {
            cout << x << endl;
        }
        else if (y < 2 * x)
        {
            // 下面的特判需要注意,上述的讨论涉及回撤,而不是所有情况都有步骤可以回撤
            if (x == 1 && y == 1)
            {
                cout << 2 << endl;
            }
            else if (x == 1 && y == 0)
            {
                cout << 3 << endl;
            }
            else if (x == 2 && y == 2)
            {
                cout << 4 << endl;
            }
            else
            {
                if ((x + y) % 3 == 1)
                {
                    cout << (x + y - 1) / 3 + 1 << endl;
                }
                else if ((x + y) % 3 == 2)
                {
                    cout << (x + y - 2) / 3 + 2 << endl;
                }
                else
                {
                    cout << (x + y) / 3 << endl;
                }
            }
        }
        else
        {
            // 特判x=0 y=1的情况,不然会出错
            if (x == 0 && y == 1)
            {
                cout << 3 << endl;
            }
            else
            {
                if ((y - 2 * x) % 4 == 1)
                {
                    cout << x + (y - 2 * x - 1) / 2 + 1 << endl;
                }
                else if ((y - 2 * x) % 4 == 2)
                {
                    cout << x + (y - 2 * x - 2) / 2 + 2 << endl;
                }
                else if ((y - 2 * x) % 4 == 3)
                {
                    cout << x + (y - 2 * x - 3) / 2 + 3 << endl;
                }
                else
                {
                    cout << x + (y - 2 * x) / 2 << endl;
                }
            }
        }
    }
    return 0;
}

【END】感谢观看!

发布了44 篇原创文章 · 获赞 17 · 访问量 9090

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/103995894