憧憬成为dfs高手
我对于dfs的理解可以分为三种,一种是在图/二维数组里对于路径的搜索的暴力;第二种是在树里去用dfs暴力拿一些路径;最后一种就是全排列对于做事顺序的暴力排列。这里主要讲后者。
dfs是一种暴力,搜索的艺术,而且做题也可以很公式化。
以最常见的全排列为例子
java
import java.util.Scanner;
public class Main {
static int n, a[] = new int[10005], f[] = new int[10005],jg[]=new int[10005];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
dfs(0);
}
//wz是第几个数字
static void dfs(int wz) {
if(wz>n)return;
if (wz == n) {
solve();
}
for (int i = 0; i < n; i++) {
if (f[i] == 0) {
f[i] = 1;
jg[wz]=i;
dfs(wz + 1);
jg[wz]=0;
f[i] = 0;
}
}
}
//调用临界解决问题的方法
static void solve() {
for (int i = 0; i < n; i++) {
System.out.printf("%d ",jg[i]);
}
System.out.println();
}
}
可以看到这里其实就已经可以抽象为模板,利用f[i]作为标记,jg[i]存数字来枚举全排列。
那么在这个基础还做到什么呢,其实靠着这个全排列就已经解决很多问题了,这个就是最基础的暴力,我们可以把这个全排列的数字来作为a[i]的索引再在solve函数里做一些逻辑判断来解决问题。
对了这里的全排列是不考虑结果递增顺序的所以会有很多顺序变了,但是实际结果可能意指的,比如不考虑操作顺序的动作里 12 3和 2 1 3是一样的,这个时候要考虑产生递增的操作顺序结果。
java
import java.util.Scanner;
public class Main {
static int n, a[] = new int[10005], f[] = new int[10005],jg[]=new int[10005];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
dfs(0,0);
}
//wz是第几个数字
static void dfs(int wz,int ks) {
if(wz>n)return;
if (wz == n) {
solve();
}
for (int i = ks; i < 10; i++) {
if (f[i] == 0) {
f[i] = 1;
jg[wz]=i;
dfs(wz + 1,i+1);
jg[wz]=0;
f[i] = 0;
}
}
}
//调用临界解决问题的方法
static void solve() {
for (int i = 0; i < n; i++) {
System.out.printf("%d ",jg[i]);
}
System.out.println();
}
}
上面的这个是在10个数字里选3个递增的数字,主要借助ks这个保存上一次的初始值,可以做到顺序不一项结果的暴力枚举。其实这里因为确定了起点f[i]去掉也没问题了,但是好习惯还是写上。
比如n皇后使用dfs来枚举所有棋子的坐标,这就等价于dfs来实现无法完成的n重for循环,因为实际写代码做不到写n重for循环。
java
import java.util.Scanner;
public class Main {
static int n, a[] = new int[10005], f[] = new int[10005],jg[]=new int[10005],sl;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
sl=n*n;
dfs(0,0);
}
//wz是第几个数字
static void dfs(int wz,int qs) {
if (wz == n) {
solve();
}
for (int i = qs; i < sl; i++) {
if (f[i] == 0) {
f[i] = 1;
jg[wz]=i;
dfs(wz + 1,i+1);
f[i] = 0;
}
}
}
//调用临界解决问题的方法
static void solve() {
for (int i = 0; i < n; i++) {
System.out.printf("%d %d\n",jg[i]/n,jg[i]%n);
}
System.out.println();
}
}
我们只需要修改 for (int i = 0; i < sl; i++)这里就完成了n重for循环,再借助qs就完成了对n*n枚举n个递增的数再映射为坐标,接下里只需要对 System.out.printf("%d %d\n",jg[i]/n,jg[i]%n);就能实现得到每个皇后的x,y坐标。这只是完成了暴力对所有皇后位置的放,还需要在solve里进行横竖左斜右斜的判断来看这个结果是否符合,不过这样通常是会时间复杂度用的更多,其实更理想的是在
jkava
for (int i = 0; i < sl; i++) {
if (f[i] == 0) {
f[i] = 1;
jg[wz]=i;
dfs(wz + 1);
f[i] = 0;
}
}
这个期间就进行剪枝判断,而不是全枚举完再判断,这里主要是提供思路。
那我们接下来看一下组合总和:
给定一个无重复元素 的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
可以发现区别是一个数字可以重复选还有判断条件不再是根据数字的数量而是总和。那么我们先看数字重复选怎么实现,这里要在f[i]上动手。
java
import java.util.Scanner;
public class Main {
static int n, a[] = new int[10005], f[] = new int[10005], jg[] = new int[10005];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
dfs(0, 0);
}
//wz是第几个数字
static void dfs(int wz, int ks) {
if(wz>n)return;
if (wz == n) {
solve();
}
for (int i = ks; i < n; i++) {
jg[wz] = i;
dfs(wz + 1, i );
jg[wz] = 0;
}
}
//调用临界解决问题的方法
static void solve() {
for (int i = 0; i < n; i++) {
System.out.printf("%d ", jg[i]);
}
System.out.println();
}
}
去掉f[i],再把qs不加1传参就可以得到有重复数字的递增序列了。
那么针对这道问题,需要把递归临界从数量改为总和。
java
import java.util.Scanner;
public class Main {
static int n, a[] = new int[10005],mub,f[] = new int[10005], jg[] = new int[10005],f1;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
mub=sc.nextInt();
for (int i = 0; i < n; i++) {
a[i]=sc.nextInt();
}
dfs(0, 0);
}
static void dfs(int sum, int ks) {
if(sum>mub)return;
if (sum == mub) {
solve();
}
for (int i = ks; i < n; i++) {
jg[f1++] = a[i];
dfs(sum+a[i], i );
jg[f1--] = 0;
}
}
//调用临界解决问题的方法
static void solve() {
for (int i = 0; i < f1; i++) {
System.out.printf("%d ", jg[i]);
}
System.out.println();
}
}
这样就完成了,遇到问题的时候思路不要乱,大胆去想。