算法与数据结构基础课第十一节笔记

暴力递归

暴力递归就是尝试
1,把问题转化为规模缩小了的同类问题的子问题
2,有明确的不需要继续进行递归的条件(base case)
3,有当得到了子问题的结果之后的决策过程
4,不记录每一个子问题的解

熟悉什么叫尝试?

例1:打印n层汉诺塔从最左边移动到最右边的全部过程

1、1 ~ n-1 左到中

2、n 左到右

3、1~n-1 中到右

模拟手动移的过程:

        public static void hanoi1(int n) {
		leftToRight(n);
	}

	// 把 1到N层圆盘 从左到右
	public static void leftToRight(int n) {
		if (n == 1) {
			System.out.println("Move 1 from left to right");
			return;
		}
		// 1 到 n-1 层圆盘 从左移到中间 腾路
		leftToMid(n - 1);
		// 把 n层 从左移到右
		System.out.println("Move " + n + " from left to right");
		// 1 到 n-1 层圆盘从中间移到右边 
		midToRight(n - 1);
	}

	public static void leftToMid(int n) {
		if (n == 1) {
			System.out.println("Move 1 from left to mid");
			return;
		}
		leftToRight(n - 1);
		System.out.println("Move " + n + " from left to mid");
		rightToMid(n - 1);
	}

	public static void rightToMid(int n) {
		if (n == 1) {
			System.out.println("Move 1 from right to mid");
			return;
		}
		rightToLeft(n - 1);
		System.out.println("Move " + n + " from right to mid");
		leftToMid(n - 1);
	}

	public static void midToRight(int n) {
		if (n == 1) {
			System.out.println("Move 1 from mid to right");
			return;
		}
		midToLeft(n - 1);
		System.out.println("Move " + n + " from mid to right");
		leftToRight(n - 1);
	}

	public static void midToLeft(int n) {
		if (n == 1) {
			System.out.println("Move 1 from mid to left");
			return;
		}
		midToRight(n - 1);
		System.out.println("Move " + n + " from mid to left");
		rightToLeft(n - 1);
	}

	public static void rightToLeft(int n) {
		if (n == 1) {
			System.out.println("Move 1 from right to left");
			return;
		}
		rightToMid(n - 1);
		System.out.println("Move " + n + " from right to left");
		midToLeft(n - 1);
	}

可以把三个状态左,中,右,精简成 from , other,to

一堆盘子想要从from到to上,那就要先把n-1个挪到中间的other上去,然后把最大的盘子n挪到to,再把n-1从other挪到右边

每次想要从中取出最底层,最大的盘子,就需要借助一个辅助的other来放他上面的这些盘子

        public static void hanoi2(int n) {
		if (n > 0) {
			func(n, "left", "right", "mid");
		}
	}

	// 1~i 圆盘 目标是from -> to, other是另外一个
	public static void func(int N, String from, String to, String other) {
		if (N == 1) { // base
			System.out.println("Move 1 from " + from + " to " + to);
		} else {
			func(N - 1, from, other, to);
			System.out.println("Move " + N + " from " + from + " to " + to);
			func(N - 1, other, to, from);
		}
	}

例2:打印一个字符串的全部子序列

子串是字符串中连续的一部分

子序列是字符的相对次序不变的一部分,不要求连续。相当于每个位置有两种状态,要和不要

        public static List<String> subs(String s) {
		char[] str = s.toCharArray();
		String path = "";
		List<String> ans = new ArrayList<>();
		process1(str, 0, ans, path);
		return ans;
	}

	/**
	 * 
	 * @param str
	 * @param index 现在来到的位置,要 or 不要
	 * @param ans 如果index来到了str的终止位置,把沿途路径所形成的答案,保存在ans里
	 * @param path 之前做出的选择
	 */
	public static void process1(char[] str, int index, List<String> ans, String path) {
		if (index == str.length) {
			ans.add(path);
			return;
		}
		String no = path;
		process1(str, index + 1, ans, no);
		String yes = path + String.valueOf(str[index]);
		process1(str, index + 1, ans, yes);
	}

例3:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列

       public static List<String> subsNoRepeat(String s) {
		char[] str = s.toCharArray();
		String path = "";
		HashSet<String> set = new HashSet<>();
		process2(str, 0, set, path);
		List<String> ans = new ArrayList<>();
		for (String cur : set) {
			ans.add(cur);
		}
		return ans;
	}

	
	public static void process2(char[] str, int index, HashSet<String> set, String path) {
		if (index == str.length) {
			set.add(path);
			return;
		}
		String no = path;
		process2(str, index + 1, set, no);
		String yes = path + String.valueOf(str[index]);
		process2(str, index + 1, set, yes);
	}

例4:打印一个字符串的全排列     可以通过交换数组位置来实现,记得用完交换回去

       public static ArrayList<String> permutation(String str) {
		ArrayList<String> res = new ArrayList<>();
		if (str == null || str.length() == 0) {
			return res;
		}
		char[] chs = str.toCharArray();
		process(chs, 0, res);
		return res;
	}

	/**
	 * str[0...i-1]已经做好决定
	 * str[i...]都有机会来到i位置
	 * i 是终止位置,str当前的样子,就是一种结果 -> ans
	 * @param str
	 * @param i
	 * @param res
	 */
	public static void process(char[] str, int i, ArrayList<String> res) {
		if (i == str.length) {
			res.add(String.valueOf(str));
		}
		// i 还没到终止位置,i往后的所有位置,都可以来到i位置
		for (int j = i; j < str.length; j++) { // j 就是 i后面的字符都有机会
			swap(str, i, j);
			process(str, i + 1, res);
			// 一定要恢复现场
			swap(str, i, j);
		}
	}

例5:打印一个字符串的全排列,要求不要出现重复的排列

分支限界法之间砍掉重复的分支

       public static ArrayList<String> permutationNoRepeat(String str) {
		ArrayList<String> res = new ArrayList<>();
		if (str == null || str.length() == 0) {
			return res;
		}
		char[] chs = str.toCharArray();
		process2(chs, 0, res);
		return res;
	}


	public static void process2(char[] str, int i, ArrayList<String> res) {
		if (i == str.length) {
			res.add(String.valueOf(str));
		}
		// 限制都是小写字母,有其他的可以换成HashMap
		boolean[] visit = new boolean[26]; // visit[0 1 .. 25]
		for (int j = i; j < str.length; j++) {
			if (!visit[str[j] - 'a']) {
				visit[str[j] - 'a'] = true;
				swap(str, i, j);
				process2(str, i + 1, res);
				swap(str, i, j);
			}
		}
	}

仰望好的尝试?

给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。 如何实现?

        public static void reverse(Stack<Integer> stack) {
		if (stack.isEmpty()) {
			return;
		}
		int i = f(stack);
		reverse(stack);
		stack.push(i);
	}

	/**
	 * 在stack中去掉底,并返回这个底
	 * @param stack
	 * @return
	 */
	public static int f(Stack<Integer> stack) {
		int result = stack.pop();
		if (stack.isEmpty()) {
			return result;
		} else {
			int last = f(stack);
			stack.push(result);
			return last;
		}
	}

从左往右的尝试模型1

规定1和A对应、2和B对应、3和C对应...那么一个数字字符串比如"111”,就可以转化为:"AAA"、"KA"和"AK",给定一个只有数字字符组成的字符串str,返回有多少种转化结果

        public static int number(String str) {
		if (str == null || str.length() == 0) {
			return 0;
		}
		return process(str.toCharArray(), 0);
	}

	// str[0 .. i-1]已经转化完了,固定了
	// i之前的位置,如何转化已经做过决定了, 不用再关心
	// i... 有多少种转化的结果
	public static int process(char[] str, int i) {
		// str.length - 1位置完成了转化,返回一种转换方式
		if (i == str.length) { // base case
			return 1;
		}
		// i没有到终止位置,题目限制0无法转化
		if (str[i] == '0') {
			return 0;
		}
		// str[i]字符不是‘0’
		if (str[i] == '1') {
			int res = process(str, i + 1); // i自己作为单独的部分,后续有多少种方法
			if (i + 1 < str.length) {
				res += process(str, i + 2); // (i和i+1)作为单独的部分,后续有多少种方法
			}
			return res;
		}
		if (str[i] == '2') {
			int res = process(str, i + 1); // i自己作为单独的部分,后续有多少种方法
			// (i和i+1)作为单独的部分并且没有超过26,后续有多少种方法
			if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) {
				res += process(str, i + 2); // (i和i+1)作为单独的部分,后续有多少种方法
			}
			return res;
		}
		// str[i] == '3' ~ '9' 只有一种选择,30已经大于所有转换码了
		return process(str, i + 1);
	}

给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?   要最大价值,装的时候要累加自身的价值和不装做比较

从空包开始装,看他是不是装满了

        public static int getMaxValue(int[] w, int[] v, int bag) {
		return process(w, v, 0, 0, bag);
	}

	/**
	 * index... 最大价值
	 * @param w
	 * @param v
	 * @param index
	 * @param alreadyW 0..index -1 上做了货物选择,使得你已经达到的重量是多少
	 * @param bag
	 * @return 不为-1,认为是返回的真实价值
	 */
	public static int process(int[] w, int[] v, int index, int alreadyW, int bag) {
		if (alreadyW > bag) {
			return -1;
		}
		// 重量没超,但是货没了,就返回0
		if (index == w.length) {
			return 0;
		}
		int p1 = process(w, v, index + 1, alreadyW, bag);
		// p2next 是要了index货,后续的价值
		int p2next = process(w, v, index + 1, alreadyW + w[index], bag);
		int p2 = -1;
		if (p2next != -1) {
			p2 = v[index] + p2next;
		}
		return Math.max(p1, p2);

	}

从空包开始装,看他是不是还有空间能装

        public static int maxValue(int[] w, int[] v, int bag) {
		return process(w, v, 0, bag);
	}

	// 只剩下rest的空间了,
	// index...货物自由选择,但是剩余空间不要小于0
	// 返回index..货能够获得的最大价值
	public static int process(int[] w, int[] v, int index, int rest) {
		if (rest < 0) { // base case 1
			return -1;
		}
		// rest >=0
		if (index == w.length) { // base case 2
			return 0;
		}
		// 有货也有空间
		int p1 = process(w, v, index + 1, rest);
		int p2 = -1;
		int p2next = process(w, v, index + 1, rest - w[index]);
		if (p2next != -1){
			p2 = v[index] + p2next;
		}
		return Math.max(p1, p2);
	}

范围上尝试的模型

给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。 后拿的是认为先拿的拿了之后,剩下什么能挑的,因为设定是绝顶聪明,所以A只会留给B小的

        public static int win1(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
	}

	public static int f(int[] arr, int i, int j) {
		if (i == j) {
			return arr[i];
		}
		return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
	}

	public static int s(int[] arr, int i, int j) {
		if (i == j) {
			return 0;
		}
		return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
	}

N皇后

面试一般不会有,主要作为了解

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列, 也不在同一条斜线上,给定一个整数n,返回n皇后的摆法有多少种。

n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92

        public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		// record[0] ?  record[1]  ?  record[2]
		int[] record = new int[n]; // record[i] -> i行的皇后,放在了第几列
		return process1(0, record, n);
	}

	// 潜台词:record[0..i-1]的皇后,任何两个皇后一定都不共行、不共列,不共斜线
	// 目前来到了第i行
	// record[0..i-1]表示之前的行,放了的皇后位置 record[0]=7 0行皇后在第7列
	// n代表整体一共有多少行
	// 返回值是,摆完所有的皇后,合理的摆法有多少种
	public static int process1(int i, int[] record, int n) {
		if (i == n) { // 终止行
			return 1;
		}
		int res = 0;
		for (int j = 0; j < n; j++) { // 当前行在i行,尝试i行所有的列  -> j
			// 当前i行的皇后,放在j列,会不会和之前(0..i-1)的皇后,不共行共列或者共斜线,
			// 如果是,认为有效
			// 如果不是,认为无效
			if (isValid(record, i, j)) {
				record[i] = j;
				res += process1(i + 1, record, n);
			}
		}
		return res;
	}

        // record[0..i-1]你需要看,record[i...]不需要看
	// 返回i行皇后,放在了j列,是否有效
	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) { // 之前的某个k行的皇后
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
				return false;
			}
		}
		return true;
	}

位运算可以优化常数项时间

        // 请不要超过32皇后问题
	public static int num2(int n) {
		if (n < 1 || n > 32) {
			return 0;
		}
		int limit = n == 32 ? -1 : (1 << n) - 1;
		return process2(limit, 0, 0, 0);
	}

	// limit 划定了问题的规模 固定
	// colLim 列的限制,1的位置不能放皇后,0的位置可以
	// leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
	// rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
	public static int process2(int limit, 
			int colLim, 
			int leftDiaLim,
			int rightDiaLim) {
		if (colLim == limit) { // base case
			return 1;
		}
		// 所有候选皇后的位置,都在pos上
		// (colLim | leftDiaLim | rightDiaLim) 总限制
		// ~(colLim | leftDiaLim | rightDiaLim) 左侧的一坨0干扰,右侧的1表示 可尝试
		int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
		int mostRightOne = 0;
		int res = 0;
		while (pos != 0) {
			mostRightOne = pos & (~pos + 1);
			res += process2(limit, 
					colLim | mostRightOne,
					(leftDiaLim | mostRightOne) << 1,
					(rightDiaLim | mostRightOne) >>> 1);
			pos = pos - mostRightOne;
		}
		return res;
	}

N皇后的时间复杂度是O(N^{N})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值