深搜之抽象类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
输出
输出方案数

*/
写法一:
思想:选了i之后 从剩下的数中选 结果要除以阶层数
package 抽象深度优先搜索;
/*
 * 测试数据:
5 3 9
1 2 3 4 5
答案:2
 */
import java.util.Scanner;
public class k个数的和 {
	static int n,k,sum,cnt;
	static int[] num;
	static boolean[] xuan;
	static int ans=0;
	//搜索策略:从剩下的数中选一个数
	//在计算时 选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
	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();
		dfs(0,0);
		System.out.println(ans/6);
		
		
}
}
写法二:
思想:根据每次选不选这个数
package 抽象深度优先搜索;
/*
5 3 9
1 2 3 4 5
 */
/*
递归调用过程
0 0 0
1 0 0
2 0 0
3 0 0
4 0 0
5 0 0
: 4 0 0
5 1 5
: 3 0 0
4 1 4
5 1 4
: 4 1 4
5 2 9
: 2 0 0
3 1 3
 */
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 i,int cnt,int s) {
		//i表示当前为数组num的第几个 cnt表示当前选了几个 s为当前选出数的累计和
		System.out.println(i+" "+cnt+" "+s);
		if(s>sum||cnt>k) {//可行性剪枝
			return;}
		if(i==n) {//所有数都考虑完 不管有没有方案都要返回调用结果
			if(s==sum&&cnt==k) {
				ans++;
			}
			return;//有加return时复杂度为A(k,n)n的k排列,无时为n的阶乘
		}
		//递归交替搜		
		dfs(i+1,cnt,s);//不选 深搜下一个 i从0-4
		dfs(i+1,cnt+1,s+num[i]);//选上面的作为当前 深搜下一个 i从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 根木棍的长度 p​i​​(1≤p​i​​≤10000)。
输出格式

如果蒜头君能拼出等边三角形,输出"yes",否则输出"no"。
样例输入1

5
1 2 3 4 5

样例输出1

yes

样例输入2

4
1 1 1 1

样例输出2

no
 */

思路:这道题有些类似前面的k个数的和 从给定的n根木棍中选出若干根组成边长s 所不同的是需要组成两条边才能判断可以组成等边三角形
package 抽象深度优先搜索;

/*
测试数据:
5
1 2 3 4 5
 */
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星的观察团会有多少种国别的不同组合呢?

下面的程序解决了这个问题。
数组a[] 中既是每个国家可以派出的最多的名额。
程序执行结果为:
DEFFF
CEFFF
CDFFF
CDEFF
CCFFF
CCEFF
CCDFF
CCDEF
BEFFF
BDFFF
BDEFF
BCFFF
BCEFF
BCDFF
BCDEF
....

(以下省略,总共101行)

*/
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);

	}

}

你可能感兴趣的:(蓝桥杯算法学习)