DFS学习1

1.烤鸡

题目背景

猪猪 Hanke 得到了一只鸡。

题目描述

猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 种配料(芥末、孜然等),每种配料可以放 1 到 3 克,任意烤鸡的美味程度为所有配料质量之和。

现在, Hanke 想要知道,如果给你一个美味程度 n ,请输出这 10 种配料的所有搭配方案。

输入格式

一个正整数 n,表示美味程度。

输出格式

第一行,方案总数。

第二行至结束,10个数,表示每种配料所放的质量,按字典序排列。

如果没有符合要求的方法,就只要在第一行输出一个 0。

样例 #1

样例输入 #1

11

样例输出 #1

10
1 1 1 1 1 1 1 1 1 2 
1 1 1 1 1 1 1 1 2 1 
1 1 1 1 1 1 1 2 1 1 
1 1 1 1 1 1 2 1 1 1 
1 1 1 1 1 2 1 1 1 1 
1 1 1 1 2 1 1 1 1 1 
1 1 1 2 1 1 1 1 1 1 
1 1 2 1 1 1 1 1 1 1 
1 2 1 1 1 1 1 1 1 1 
2 1 1 1 1 1 1 1 1 1

思路与代码

全排列问题,对于每一种调料,我们要选1g,2g或3g

利用dfs(),第一个参数存调料的种类,第二个存美味值总和。

如果这个美味值总和恰好到了所给的n值,且每种调料都用到了,此时即终止搜索。

否则,三种情况继续进入dfs(), 即美味值总和+1, +2 或 +3.

但是,当搜到第10层,如果不符合sum为所给的n值,就要剪枝处理。

#include<bits/stdc++.h>
using namespace std;
​
int n, ans = 0;
int a[100];
int b[100000][100];
​
void dfs(int k, int sum)
{
    if(sum > n) return;
    if(k == 11 && sum == n)
    {
        ans++;
        for(int i = 1; i <= 10; i++)
            b[ans][i] = a[i];
        return;
    }
    for(int i = 1; i <= 3; i++)
    {
        a[k] = i;
        dfs(k + 1, sum + a[k]);
        a[k] = 0;
    }
}
​
int main()
{
    cin >> n;
    if(n < 10 || n > 30) 
    {
        cout << 0;
        return 0;
    }
    dfs(1, 0);  cout << ans << endl;
    for(int i = 1; i <= ans; i++)
    {
        for(int j = 1; j <= 10; j++)
            cout << b[i][j] << " ";
        cout << endl;
    }
}

2.火星人

题目描述

人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学家,科学家破解这个数字的含义后,再把一个很小的数字加到这个大数上面,把结果告诉火星人,作为人类的回答。

火星人用一种非常简单的方式来表示数字――掰手指。火星人只有一只手,但这只手上有成千上万的手指,这些手指排成一列,分别编号为 1,2,3,\cdots。火星人的任意两根手指都能随意交换位置,他们就是通过这方法计数的。

一个火星人用一个人类的手演示了如何用手指计数。如果把五根手指――拇指、食指、中指、无名指和小指分别编号为 1,2,3,4 和 5,当它们按正常顺序排列时,形成了 5 位数 12345,当你交换无名指和小指的位置时,会形成 5 位数 12354,当你把五个手指的顺序完全颠倒时,会形成 54321,在所有能够形成的 120 个 5 位数中,12345 最小,它表示 1;12354 第二小,它表示 2;54321 最大,它表示 120。下表展示了只有 3 根手指时能够形成的 6 个 3 位数和它们代表的数字:

三进制数代表的数字
1231
1322
2133
2314
3125
3216

现在你有幸成为了第一个和火星人交流的地球人。一个火星人会让你看他的手指,科学家会告诉你要加上去的很小的数。你的任务是,把火星人用手指表示的数与科学家告诉你的数相加,并根据相加的结果改变火星人手指的排列顺序。输入数据保证这个结果不会超出火星人手指能表示的范围。

输入格式

共三行。 第一行一个正整数 N,表示火星人手指的数目(1 \le N \le 10000)。 第二行是一个正整数 M,表示要加上去的小整数(1 \le M \le 100)。 下一行是 1 到 N 这 N 个整数的一个排列,用空格隔开,表示火星人手指的排列顺序。

输出格式

N 个整数,表示改变后的火星人手指的排列顺序。每两个相邻的数中间用一个空格分开,不能有多余的空格。

样例 #1

样例输入 #1

5
3
1 2 3 4 5

样例输出 #1

1 2 4 5 3

思路与代码

这也是一种全排列,不过无重复数字。

最开始的想法是,直接全排列出来,然后找到和火星人的数字相同的排列,然后再向后推。

如下是代码,用一个二维数组,把所有的存进去。而且如果这个数访问过了,就不排列了。

不过,看一下数据范围,n <= 10000,方案数一共是(n!)个,如果开二位数组存,肯定会超空间的。

而且时间复杂度也是n!,也会超。只好想办法优化。

void dfs(int k)
{
    if(k == n + 1)
    {
        ans++;
        for(int i = 1; i <= n; i++)
            d[ans][i] = b[i];
        return;
    }
    
    for(int i = 1; i <= n; i++)     //!!!!!!!!!!
    {
        if(vi[i] == 0)
        {
            vi[i] = 1;
            b[k] = i;
            dfs(k + 1);
            vi[i] = 0;
        }
    }
}

其实,可以从火星人所给的数字开始往后排列,找到第m个,就停下来了。用!注释的代码处,用dis存储方案数,此处修改可以把火星人的数字作为排列的起点,然后dis += 1; //由于for循环是按字典序进行的,所以接下来的排列符合预期的排列。

到了这一步,看似很完美。但实际上,当前的已经找到了,其他的dfs还在继续往后走,还是会导致超时。

void dfs(int k)
{
    if(k == n + 1)
    {
        dis++;                  //!!!!!!!!!!
        if(dis == m + 1)        //!!!!!!!!!!
        {
            for(int i = 1; i <= n; i++)
                cout << b[i] << " ";
        }
        return;
    }
    
    for(int i = 1; i <= n; i++) //!!!!!!!!!!
    {
        if(dis == 0)            //!!!!!!!!!!
            i = a[k];           //!!!!!!!!!!
        if(vi[i] == 0)
        {
            vi[i] = 1;
            b[k] = i;
            dfs(k + 1);
            vi[i] = 0;
            b[k] = 0;
        }
    }
}

函数里的return,只能打断当前函数的进行,不能使整个程序终止,所以用全局变量havefind来说明已经找到了。当找到答案后,将havefind设置为true,从而使得其他正在进行dfs函数终止。

void dfs(int k)
{
    if(havefind) return;        //!!!!!!!!
    if(k == n + 1)
    {
        dis++;
        if(dis == m + 1)
        {
            havefind = true;    //!!!!!!!
            for(int i = 1; i <= n; i++)
                cout << b[i] << " ";
        }
        return;
    }
    //。。。。。

最后,附上全代码

#include<bits/stdc++.h>
using namespace std;
​
int n, m, ans = 0;
int a[10002] ,b[10002];
int vi[10001];
​
int dis = 0;
bool havefind = false;
void dfs(int k)
{
    if(havefind) return;
    if(k == n + 1)
    {
        dis++;
        if(dis == m + 1)
        {
            havefind = true;
            for(int i = 1; i <= n; i++)
                cout << b[i] << " ";
        }
        return;
    }
    
    for(int i = 1; i <= n; i++)
    {
        if(dis == 0)
            i = a[k];
        if(vi[i] == 0)
        {
            vi[i] = 1;
            b[k] = i;
            dfs(k + 1);
            vi[i] = 0;
            b[k] = 0;
        }
    }
}
​
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    dfs(1);
    return 0;
}

3.火柴棒等式

题目描述

给你 n 根火柴棍,你可以拼出多少个形如 A+B=C 的等式?等式中的 A、B、C 是用火柴棍拼出的整数(若该数非零,则最高位不能是 0)。用火柴棍拼数字 0\sim9 的拼法如图所示:

注意:

  1. 加号与等号各自需要两根火柴棍;

  2. 如果 A\neq B,则 A+B=C 与 B+A=C 视为不同的等式(A,B,C\geq0);

  3. n 根火柴棍必须全部用上。

输入格式

一个整数 n(1 \leq n\leq 24)。

输出格式

一个整数,能拼成的不同等式的数目。

样例 #1

样例输入 #1

14

样例输出 #1

2

样例 #2

样例输入 #2

18

样例输出 #2

9

思路与代码

还是全排列,遍历三个数的不同可能,保证两个条件,前两个数相加为第三个数,三个数所用的火柴棒的和为n-4,减去的4为摆加号和等号所需的火柴棒。

此处火柴棒最多给24条,最多是711+0=711这个算式,所以搜索时,要从0搜到1000,一般来说是会超时的,所以需要剪枝。

关于剪枝,需要注意的地方,在于判断条件的优先级。

比如,从 bfs(1, 0)开始,搜完第三个后,将进行bfs(4, sum),第一个参数到了4,当前搜索就可以停止了。

但是第一次写的时候,将 k == 4 与另外两个条件写成并列关系,如果都符合才return,这就导致 k > 4 会一直搜下去,直到sum > n。

另外,存不同数字所需火柴棒时,还需要计算两位数乃至三位数所需的火柴棒。使用递推,把棒棒存到数组里就很方便。

#include<bits/stdc++.h>
using namespace std;
​
int n, ans = 0;
int dx[10000] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int b[10000];
​
void dfs(int k, int sum)
{
    if(sum > n) return;     //超时减枝
    if(k == 4)
    {
        if(sum == n && b[1] + b[2] == b[3] )
        {
            ans++;
            cout << b[1] << " + " << b[2] << " = " << b[3] << endl;
        }       
        return;
    }
        
    for(int i = 0; i <= 1000; i++)
    {
        b[k] = i;
        dfs(k + 1, sum + dx[i]);
    }
}
​
int main()
{
    cin >> n;
    n -= 4;
    for(int i = 10; i <= 1000; i++)
        dx[i] = dx[i % 10] + dx[i / 10];
    dfs(1, 0);
    cout << ans;
}

4.PERKET

题目描述

Perket 是一种流行的美食。为了做好 Perket,厨师必须谨慎选择食材,以在保持传统风味的同时尽可能获得最全面的味道。你有 n 种可支配的配料。对于每一种配料,我们知道它们各自的酸度 s 和苦度 b。当我们添加配料时,总的酸度为每一种配料的酸度总乘积;总的苦度为每一种配料的苦度的总和。

众所周知,美食应该做到口感适中,所以我们希望选取配料,以使得酸度和苦度的绝对差最小。

另外,我们必须添加至少一种配料,因为没有任何食物以水为配料的。

输入格式

第一行一个整数 n,表示可供选用的食材种类数。

接下来 n 行,每行 2 个整数 s_i 和 b_i,表示第 i 种食材的酸度和苦度。

输出格式

一行一个整数,表示可能的总酸度和总苦度的最小绝对差。

样例 #3

样例输入 #3

4
1 7
2 6
3 8
4 9

样例输出 #3

1

提示

数据规模与约定

对于 100\% 的数据,有 1 \leq n \leq 10,且将所有可用食材全部使用产生的总酸度和总苦度小于 1 \times 10^9,酸度和苦度不同时为 1 和 0。

思路与代码

二选一的全排列,对于每种调料,要么加,要么不加,所以还是比较简单

所以不需要回溯,直接向下搜索就是,

如果前n个搜完了,则到了第n + 1个,如果是清水,即没有加任何配料,就剪掉。

不过我第一次剪枝把清水的判断放到了第一行,应该放到 if(k > n)里,还是判断条件优先级的问题。刚开始选配料是,也是清水

然后用x保存最小值,由于求的是差值的绝对值,所以最小值 >= 0, 此处x = -1表示未被搜索过。

#include<bits/stdc++.h>
using namespace std;
int x = -1, n;
int s[10000], b[10000];
​
void dfs(int k, int sum_s, int sum_b)
{
    if(k > n)
    {   
        if(sum_s == 1 && sum_b == 0)    return;
        if(x == -1)
            x = abs(sum_s - sum_b);
        else
            x = min( x, abs(sum_s - sum_b) );
        return;
    }
    dfs(k + 1, sum_s * s[k], sum_b + b[k]);
    dfs(k + 1, sum_s, sum_b);
}
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> s[i] >> b[i];
    dfs(1, 1, 0);
    cout << x;
}

5.奇怪的电梯

题目描述

呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第 i 层楼(1 \le i \le N)上有一个数字 K_i(0 \le K_i \le N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 K_i(K_1=3,K_2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

输入格式

共二行。

第一行为三个用空格隔开的正整数,表示 N, A, B(1 \le N \le 200,1 \le A, B \le N)。

第二行为 N 个用空格隔开的非负整数,表示 K_i。

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

样例 #1

样例输入 #1

5 1 5
3 3 1 2 5

样例输出 #1

3

思路与代码

本质还是一个无重复数的全排列问题,

对于每一次选择,只能从没走过的电梯选择,因为上下电梯,如果你从某一楼开始,走了几次又绕回来了,说明走重复路了,舍掉。

虽然说是全排列,但是每次搜索可以做的选择是有限的,只有上和下,如果上下可以走到已经标记的路,就不走。

额外的剪枝处理,如果当前按过按钮的次数大于等于最小值,就打断。

对于感叹号注释的一行,最开始我的假想是,如果这一层的电梯移动的数 是0,那么就只能原地转圈,所以把这个也剪掉了,反而错的更多了

原来,是因为下面的 if 已经处理了,如果是0,那么v[t1]和v[t2]都已经访问过了,就不会出现原地转圈的情况。

void dfs(int k, int place)  // k 表示次数, place表示楼层位置 
{
    //if(x[place] == 0) return;     !!!!!!!!
    if(ans != -1 && k >= ans) return;
    if(place == b)
    {
        if(ans == -1) ans = k;
        else ans = min(k, ans);
        cout << ans;
        return;
    }
    
    int t1 = place - x[place], t2 = place + x[place];
    if( !v[t1] && t1 >= 1)
    {
        v[t1] = 1;
        dfs( k + 1, t1);
        v[t1] = 0;
    }
    if( !v[t2] && t2 <= n)
    {
        v[t2] = 1;
        dfs( k + 1, t2);
        v[t2] = 0;
    }
    
}

image-20231210175105949

等学了高级算法以后再说吧

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值