深搜之抽象类DFS
抽象类DFS
上次总结了dfs迷宫类,详见我博客:深搜之迷宫类
最近刷了一些蓝桥杯的真题,这次就总结一下没有图的抽象类的深搜下面是从刷的蓝桥杯真题和计蒜客中总结的一些抽象dfs题型
抽象深搜常见有以下两种类型:
1.凑算式类
2.排列组合类
(一)、凑算式类
九数组分数
【问题描述】
1,2,3…9 这九个数字组成一个分数,其值恰好为1/3,如何组法?
答案:
5823 17469
5832 17496
【思路】
九位数字组成一个分数1/3 只能是分母5位,分子4位
package 凑算式类;
public class 九数组分数 {
//分子为四位数 分母为五位数
static int[] a=new int[9];
static boolean[] vis=new boolean[10];
static boolean f;
static void dfs(int index) {//表示试填第index位
// if(f) {
// return;
// }
//在这里输出结果否则存放在数组中的结果可能在return后被修改了
if(index == 9) { //凑到了最后一位
int s = (a[0]*1000+a[1]*100+a[2]*10+a[3]);
if(s*3 == a[4]*10000+a[5]*1000+a[6]*100+a[7]*10+a[8]) {//目标是要算式成立
System.out.println(s+" "+s*3);
// f=true;
}
return;
}
for(int i=1;i<=9;i++) {//枚举1-9填入index位
if(!vis[i]) {//该数字没有用过
//标记为用过
vis[i]=true;
a[index]=i;
//System.out.println(i+" "+index);
dfs(index+1);
vis[i]=false;
}
}
}
public static void main(String[] args) {
dfs(0);
}
}
四平方和
【题目描述】
四平方和定理,又称为拉格朗日定理:
每个正整数都可以表示为至多4个正整数的平方和。
如果把0包括进去,就正好可以表示为4个数的平方和。
【示例】
比如:
5 = 0^2 + 0^2 + 1^2 + 2^2
7 = 1^2 + 1^2 + 1^2 + 2^2
(^符号表示乘方的意思)
对于一个给定的正整数,可能存在多种平方和的表示法。
要求你对4个数排序:
0 <= a <= b <= c <= d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法
程序输入为一个正整数N (N<5000000)
要求输出4个非负整数,按从小到大排序,中间用空格分开
例如,输入:
5
则程序应该输出:
0 0 1 2
再例如,输入:
12
则程序应该输出:
0 2 2 2
再例如,输入:
773535
则程序应该输出:
1 1 267 838
思路
要凑的算式非常明显(n=a2+b2+c+d2 ) 并且要求升序(且0 <= a <= b <= c <= d) 只要在dfs函数中加入索引参数保证索引是递增的就可以了
package 凑算式类;
import java.util.Scanner;
public class 四平方和 {
static int a[]=new int[4];
static double n;
static int x;
static void dfs(int index,int pos) {
//加入参数pos保证下次dfs延续本次dfs搜索的下标继续从而保证递增
//System.out.println(index+" "+pos);
if(index==4) {//凑齐四个数了
if(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]+a[3]*a[3]==n) {//目标是要算式成立
System.out.println(a[0]+" "+a[1]+" "+a[2]+" "+a[3]);
System.exit(0); // 加上这句,只打印一组解 打印完直接结束程序
}
return;
}
for(int i=pos;i<=x;i++) {
a[index]=i;
dfs(index+1,i);
}
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextDouble();
x=(int)Math.sqrt(n);
System.out.println(x);;
dfs(0,0);
}
}
纸牌三角形
【题目描述】
A,2,3,4,5,6,7,8,9 共9张纸牌排成一个正三角形(A按1计算)。要求每个边的和相等。
下面就是一种排法
A
9 6
4 8
3 7 5 2
这样的排法可能会有很多。
如果考虑旋转、镜像后相同的算同一种,一共有多少种不同的排法呢?
【思路】
等价为用9个数字凑出相等的三个数 其中有三个数字是可以共用
package 凑算式类dfs;
public class 纸牌三角形 {
static int a[]= new int[9];
static boolean[] vis=new boolean[10];
static int ans;
static void dfs(int index) {
if(index==9) {
int s1=a[0]+a[1]+a[2]+a[3];
int s2=a[3]+a[4]+a[5]+a[6];
int s3=a[6]+a[7]+a[8]+a[0];
if(s1==s2&&s1==s3) {
ans++;
}
return;
}
for(int i=1;i<=9;i++) {
if(!vis[i]) {
a[index]=i;
vis[i]=true;
dfs(index+1);//index为9 走完所有的i就走不动了 因此可以在出口那加return提前结束
vis[i]=false;
}
}
}
public static void main(String[] args) {
dfs(0);
//考虑镜像、旋转
System.out.println(ans/6);
}
}
总结:
第一件事,就是设数组
第二件事,数字能否重复使用?能,略过这条;不能,设标记数组,或者交换(我个人偏向设标记数组)
第三件事,定义dfs()方法
第四件事,判断递归结束条件,通常是index越界,并进行等式判断
第五件事,还未凑齐数,深度优先搜索
第六件事,写main()方法
完事
(二)、排列组合类
牌型种数
【题目描述】
小明被劫持到X赌城,被迫与其他3人玩牌。一副扑克牌(去掉大小王牌,共52张),
均匀发给4个人,每个人13张。这时,小明脑子里突然冒出一个问题:如果不考虑
花色,只考虑点数,也不考虑自己得到的牌的先后顺序,自己手里能拿到的初始牌
型组合一共有多少种呢?
答案是: 3598180
写法一:
package 排列组合;
//总共13种牌A到K,每种可以选0到4张,总共选出13张,两个13如果简单表示的话就是2 13
public class 牌型种数 {
public static int count = 0 ;
public static void dfs(int type, int sum) {
// 结束条件
if(type == 13) { //列举完牌数种类 A到K 13类
if(sum == 13) { //目标要凑够13张
count++;
}
return;
}
// 搜索
for(int i=0; i<=4; i++) {
dfs(type+1, sum+i); // 此解法的关键,就在于sum+i 而不是sum+1
}
}
public static void main(String[] args) {
dfs(0,0);
System.out.println(count);
}
}
写法二:
package 排列组合;
public class 牌型种数2 {
public static int count = 0 ;
public static int[] a = new int[13];
public static void dfs(int index) {
if(index == 13) {
int sum = 0;
for(int i : a) {
sum += i;
}
if(sum == 13) {//牌总数达到13张
count++;
}
return;
}
// 搜索
for(int i=0; i<=4; i++) {
a[index] = i;//i个index类的牌
dfs(index+1);
}
}
public static void main(String[] args) {
dfs(0);
System.out.println(count);
}
}
k个数的和
【题目描述】
给定n个整数,要求选出k个数,使得选出来的k个数之和为sum
【输入】
第一行输入空格隔开三个数字 分别表示n k sum
【输出】
输出方案数
【测试数据】
5 3 9
1 2 3 4 5
答案:2
【思路】
选定a[i]之后,从剩下的数中选,直至个数cntk。因为递归的出口是cntk,所有数字都被轮过一遍之后,又重新开始轮。得到的是A(n,k)。出现重复计数,因此结果要除以K阶层数,例如,输入上面测试数据,选1时 1 3 5(1 5 3) 选3时3 1 5(3 5 1) 选5时5 1 3 (5 3 1)被记为6种不一样的结果3!=6 实际上是只有1种组合。所以结果要除以k的阶乘 即6
详细过程看图。
package 抽象深度优先搜索;
import java.util.Scanner;
public class k个数的和 {
static int n,k,sum,cnt;
static int[] num;
static boolean[] xuan;
static int ans=0;
private static void dfs(int cnt,int s) {//复杂度 A(k n)
//cnt表示当前选出数的个数 s是为选出的数的和
System.out.println(":"+cnt+" "+s);
if(s>sum||cnt>k) {
return;}
if(s==sum&&cnt==k) {
ans++;
return;
//可以不加return for循环走完之后就走不动了
}
for(int i=0;i<n;i++) {
if(!xuan[i]) {
xuan[i]=true;//选num[i]
System.out.println(num[i]);
dfs(cnt+1,s+num[i]);
//return回到这里(函数中第一个if语句return的结果)说明上一步不行要(该数加上去s超过sum)
//回溯 将vis标记取消 cnt+1失败还是cnt s+num[i]失败还是s
//1 2 3 4;1 2 3 5;
//1 2 4 5;1 2 5 4;
//1 3 2 4;1 3 2 5;
//1 3 4 2;1 3 4 5;1 3 5
xuan[i]=false;
}
}
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
k=reader.nextInt();
sum=reader.nextInt();
num=new int[n];
xuan=new boolean[n];
for(int i=0;i<n;i++)
num[i]=reader.nextInt();
//k的全排列
int A=1;
for(int i=1;i<=k;i++)
A*=i;
dfs(0,0);
System.out.println(ans/A);
}
}
【思路】
根据每次选不选这个数
package 抽象深度优先搜索;
import java.util.Scanner;
public class k个数的和2 {
static int n,k,sum,cnt;
static int[] num;
static int ans=0;
//搜索策略:是否选第i个数
private static void dfs(int index,int cnt,int s) {
//index表示当前为数组num的第几个 cnt表示当前选了几个 s为当前选出数的累计和
System.out.println(index+" "+cnt+" "+s);
if(s>sum||cnt>k) {//可行性剪枝
return;}
if(index==n) {//所有数都考虑完 不管有没有方案都要返回调用结果
if(s==sum&&cnt==k) {
ans++;
}
return;//有加return时复杂度为A(k,n)n的k排列,无时为n的阶乘
}
//递归交替搜
dfs(index+1,cnt,s);//不选 深搜下一个 index从0-4
dfs(index+1,cnt+1,s+num[i]);//选上面的作为当前 深搜下一个 index从4-0
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
k=reader.nextInt();
sum=reader.nextInt();
num=new int[n];
for(int i=0;i<n;i++)
num[i]=reader.nextInt();
dfs(0,0,0);
System.out.println(ans);
}
}
等边三角形
(1)判断是否能够构成三角形
【题目描述】
蒜头君手上有一些小木棍,它们长短不一,蒜头君想用这些木棍拼出一个等边三角形,并且每根木棍都要用到。
例如,蒜头君手上有长度为 1,2,3,3 的4根木棍,他可以让长度为1,2 的木棍组成一条边,
另外 2 根分别组成 2条边,拼成一个边长为 3 的等边三角形。蒜头君希望你提前告诉他能不能拼出来,免得白费功夫。
【输入格式】
首先输入一个整数 n(3 \le n \le 20)n(3≤n≤20),表示木棍数量,接下来输入 n 根木棍的长度 pi(1≤pi≤10000)。
【输出格式】
如果蒜头君能拼出等边三角形,输出"yes",否则输出"no"。
【样例输入1】
5
1 2 3 4 5
样例输出1
yes
【样例输入2】
4
1 1 1 1
样例输出2
no
【思路】
这道题有些类似前面的k个数的和,从给定的n根木棍中选出若干根组成边长s ,所不同的是需要组成两条边才能判断可以组成等边三角形
package 抽象深度优先搜索;
import java.util.Scanner;
public class 等边三角形 {
static int[] nums,len;
static int n,s;
static boolean[]f;
static boolean ok;
static void dfs(int i,int cnt) {
//当前选第i条木棍 已经选出了cnt条边
//System.out.println(i+" "+cnt );
if(ok) {return;}
//当选出两条边时 剩下的木棍自然可以组成第三条边
if(cnt==2) {
ok=true;
return;
}
//要凑的数是s
if(len[cnt]==s) {
dfs(i,cnt+1);
}
else{
for(int k=0;k<n;k++) {
if(!f[k]) {
f[k]=true;//选了之后标记为true
len[cnt]+=nums[k];
dfs(k+1,cnt);
len[cnt]-=nums[k];
f[k]=false;
}
}
}
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
nums=new int[n];
len=new int[3];
f=new boolean[n];
for (int i = 0; i < n; i++) {
nums[i]=reader.nextInt();
s+=nums[i];
}
//总长度不是3的倍数自然不能组成等边三角形
if(s%3!=0) {
System.out.println("no");
}
else {
s=s/3;
dfs(0,0);
if(ok)
System.out.println("yes");
else
System.out.println("no");
}
}
}
(2)、求能构成三角形的方案数
题目条件同上,只不过改成求能构成三角形的方案数。
【思路】
凑成边为固定值的方案数(其实等效为从n根有数值的木棍中选择若干根符合固定值的组合数)
package 抽象深度优先搜索;
/*
测试数据:
5
1 2 3 4 5
答案:yes
2种
(1 4)(2 3) 5
4
5 1 20 4
*/
import java.util.Scanner;
public class 等边三角形2 {
static int[] nums,len;
static int n,s,ans;
static boolean[]f;
static boolean ok;
static void dfs(int k,int cnt,int sum) {
//当前选第k条木棍 已经选出了cnt条边
System.out.println(k+" "+cnt+" "+sum);
if(sum>s) {return;}
//if(ok) {return;}
if(cnt==2) {//cnt==3
ok=true;
ans++;
}
if(sum==s) {
dfs(k,cnt+1,0);//dfs(0,cnt+1,0)
}
else{
for(int i=k;i<n;i++) {//从k开始 保证编号递增避免重复搜索 k=0 16 k=i 2
if(!f[i]) {
f[i]=true;//选了之后标记为false
System.out.println(i+" "+nums[i]);
dfs(i+1,cnt,sum+nums[i]);//当前选2时发现 4+3>5 所以3不能选
System.out.println();
f[i]=false;
}
}
}
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
nums=new int[n];
len=new int[3];
f=new boolean[n];
for (int i = 0; i < n; i++) {
nums[i]=reader.nextInt();
s+=nums[i];
}
if(s%3!=0) {
System.out.println("no");
}
else {
s=s/3;
dfs(0,0,0);
System.out.println(ans);
// if(ok)
// System.out.println("yes");
// else
// System.out.println("no");
}
}
}
抽签
【题目描述】
X星球要派出一个5人组成的观察团前往W星。
其中:
A国最多可以派出4人。
B国最多可以派出2人。
C国最多可以派出2人。
D国最多可以派出1人。
E国最多可以派出1人。
F国最多可以派出3人。
那么最终派往W星的观察团会有多少种国别的不同组合呢?
package 排列组合类;
public class 抽签 {
//数组a[]表示每个国家可以派出的最多名额 0-a[k]
static int a[]={4,2,2,1,1,3};
static int ans;
static void dfs(int index,int cnt,String s) {
//当前选a[index]类 还有cnt个名额 当前已经选的人有s
System.out.println(index+" "+cnt+" "+s);
if(cnt<0) {return;}
if(index==6) {
if(cnt==0) {
ans++;
System.out.println(s);
}
return;
}
//{4,2,2,1,1,3}
//{AAAA,BB,CC,D,E,FFF}
String s2=s;
for(int i=0;i<=a[index];i++) {
dfs(index+1,cnt-i,s2);
s2=s2+(char)('A'+index);
}
}
public static void main(String[] args) {
dfs(0,5,"");
System.out.println(ans);
}
}
(三) 无根树DFS
【问题描述】
很久以前,T王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。
为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。
J是T国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了J最常做的事情。他有一个钱袋,用于存放往来城市间的路费。
聪明的J发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第x千米到第x+1千米这一千米中(x是整数),他花费的路费是x+10这么多。也就是说走1千米花费11,走2千米要花费23。
J大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?
【输入格式】
输入的第一行包含一个整数n,表示包括首都在内的T王国的城市数
城市从1开始依次编号,1号城市为首都。
接下来n-1行,描述T国的高速路(T国的高速路一定是n-1条)
每行三个整数Pi, Qi, Di,表示城市Pi和城市Qi之间有一条高速路,长度为Di千米。
输出格式
输出一个整数,表示大臣J最多花费的路费是多少。
样例输入1
5
1 2 2
1 3 1
2 4 5
2 5 4
样例输出1
135
输出格式
大臣J从城市4到城市5要花费135的路费。
【思路】
以每一个城市为起点所能到达的最远距离暴力搜,可以AC75%的数据。
注意
由于是无向图,以给出样例为例,1与2为相邻结点,存储的时候1关联了2,2也关联了1。若与往常dfs一样将标记写在for循环内,dfs(1,0)会出现从1结点进入2结点,然后2结点遍历1结点的情况,1结点城市走了两遍,这是不允许的,不能保证遍历的方向。标记应该在进入递归函数时。
package _2013;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author JohnnyLin
* @version Creation Time:2020年10月13日 下午12:45:32
* https://www.dotcpp.com/oj/problem1438.html3
*/
public class _大臣的旅费 {
static int n,ans;
static List<Integer> [] node;
static int map[][];
static boolean vis[];
static void dfs(int father,int dist) {
//System.out.println(father+" "+dist);
vis[father]=true;
for(int i=0;i< node[father].size();i++) {
int son=node[father].get(i);
if(!vis[son]) {
dist+=map[father][son];
if(dist>ans) {
ans=dist;
}
dfs(son, dist);
dist-=map[father][son];
}
}
vis[father]=false;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
/*
0 1 2
11 12 13 14
*/
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
map=new int[n+1][n+1];
node=new ArrayList[n+1];
vis=new boolean[n+1];
for(int i=1;i<=n;i++)
node[i]=new ArrayList<>();
for (int i = 1; i < n; i++) {
int p=reader.nextInt();
int q=reader.nextInt();
int d=reader.nextInt();
node[p].add(q);
node[q].add(p);
map[p][q]=d;
map[q][p]=d;
}
for (int i = 1; i <=n; i++) {
dfs(i, 0);
vis=new boolean[n+1];
}
System.out.println((21+ans)*ans/2);
}
}
【思路】
这题其实就是求一颗树上两个点的最远的距离
最长路径问题就是:
假设 s-t这条路径为树的直径,或者称为树上的最长路。现有结论,从任意一点u出发搜到的最远的点一定是s、t中的一点,然后在从这个最远点开始搜,就可以搜到另一个最长路的端点,即用两遍广=深搜就可以找出树的最长路。
package _2013;
/**
* @author JohnnyLin
* @version Creation Time:2020年10月13日 下午2:52:17
*/
import java.util.ArrayList;
import java.util.Scanner;
public class _大臣的旅费1 {
public static int n;
static int Max=Integer.MIN_VALUE;
//代表最长距离的起点城市
static int point;
//城市结构体
//声明动态数组 ArrayList<edge> map1=new ArrayList<>();看成A数据类型 类似于Integer
//数组:A [] map2=new A();
//两句合在一起就相当于:声明了一个数组map2里面的数据元素是A类型的 而A类型的数据是edge类的
static ArrayList<edge>[] map;
static class edge{
public int P,Q,D;
public edge(int p,int q,int d) {
P=p;
Q=q;
D=d;
}
}
private static void getResult() {
boolean [] vis=new boolean[n+1];
//进行两遍深搜 第一遍深搜搜索出最长路径的起点,第二次深搜走出最长路径
dfs(1,vis,0);
//布尔数组再次初始化
vis=new boolean[n+1];
dfs(point,vis,0);
int res=(11+10+Max)*Max/2;
System.out.println(res);
}
private static void dfs(int start,boolean[] vis,int dis ) {
vis[start]=true;
//枚举从start出发 可以到达的下一个城市
for(int i=0;i<map[start].size();i++) {
edge temp=map[start].get(i);
if(!vis[temp.Q]) {//若这两个城市的高速公路没有访问过 则访问
dis+=temp.D;
if(dis>Max) {//若这条路的距离大于max 更新max
Max=dis;
point=temp.Q;//并且存储该城市
}
dfs(temp.Q,vis,dis);//沿着该点往下搜索
//需要恢复状态 因为在该条高速路走到黑之后进行下一条高速路的深搜
dis-= temp.D;
}
}
}
public static void main(String[] args) {
Scanner reader=new Scanner(System.in);
n=reader.nextInt();
//由n个城市 每个城市都有自己的map
map=new ArrayList[n+1];
for(int i=1;i<=n;i++)
//将每个城市的map都实例化为存储edge(高速路)对象的链表
map[i]=new ArrayList<edge>();
for(int j=1;j<n;j++) {
int p=reader.nextInt();
int q=reader.nextInt();
int d=reader.nextInt();
map[p].add(new edge(p,q,d));
map[q].add(new edge(q,p,d));
}
reader.close();
getResult();
}
}