哈希函数与哈希表等
1、设计RandomPool结构
【题目】
设计一种结构,在该结构中有如下三个功能:
insert(key):将某个key加入到该结构,做到不重复加入
delete(key):将原本在结构中的某个key移除
getRandom(): 等概率随机返回结构中的任何一个key。
【要求】
Insert、delete和getRandom方法的时间复杂度都是O(1)
public static class Pool<K> {
private HashMap<K, Integer> keyIndexMap; //字符串-index
private HashMap<Integer, K> indexKeyMap; //反之
private int size;
public Pool() {
this.keyIndexMap = new HashMap<K, Integer>();
this.indexKeyMap = new HashMap<Integer, K>();
this.size = 0;
}
public void insert(K key) {
if (!this.keyIndexMap.containsKey(key)) {
this.keyIndexMap.put(key, this.size);
this.indexKeyMap.put(this.size++, key);
}
}
public void delete(K key) {
if (this.keyIndexMap.containsKey(key)) {
int deleteIndex = this.keyIndexMap.get(key);
int lastIndex = --this.size;
K lastKey = this.indexKeyMap.get(lastIndex);
this.keyIndexMap.put(lastKey, deleteIndex);
this.indexKeyMap.put(deleteIndex, lastKey);
this.keyIndexMap.remove(key);
this.indexKeyMap.remove(lastIndex);
}
}
public K getRandom() {
if (this.size == 0) {
return null;
}
int randomIndex = (int) (Math.random() * this.size); // 0 ~ size -1
return this.indexKeyMap.get(randomIndex);
}
}
2、详解布隆过滤器
位图:
int a = 0;
//a 32 bit
int[] arr = new int[10];
//arr[0] int 0 ~31
int i = 178;
int numIndex = 178 / 32;
int bitIndex = 178 % 32;
//拿到178位的状态
int s = ((arr[numIndex] >> (bitIndex)) & 1);
//请把178位的状态改为1
arr[numIndex] = arr[numIndex] | (1 << (bitIndex));
i = 178;//请把178位的状态改为1
arr[numIndex] = arr[numIndex] &(~ (1 << bitIndex)
public static class BitMap {
private long[] bits;
public BitMap(int max) {
bits = new long[(max + 64) >> 6];
}
public void add(int num) {
bits[num >> 6] |= (1L << (num & 63));
}
public void delete(int num) {
bits[num >> 6] &= ~(1L << (num & 63));
}
public boolean contains(int num) {
return (bits[num >> 6] & (1L << (num & 63))) != 0;
}
}
public static void main(String[] args) {
// 表示 0~ 31 谁出现了,谁没出现
// int a = 0;
// int num = 7;
// // 请把7位描黑!
// // 0000000000010000000
// // 0000000000010000000
// a |= 1 << 7;
// a |= 1 << 13;
// a |= 1 << 29;
// // 7 13 29
// // 请告诉我,7有没有进去
// boolean has =( a & (1 << 7)) != 0;
//
//
int[] set = new int[10];
// set : 10个数
// 每个数,32位
// 0~319
int num = 176;
// set[0] : 0~31
// set[1] : 32~
int team = num / 32;
set[team] |= 1 << (num % 32);
// System.out.println("测试开始!");
// int max = 2000000;
// BitMap bitMap = new BitMap(max);
// HashSet<Integer> set = new HashSet<>();
// int testTime = 6000000;
// for (int i = 0; i < testTime; i++) {
// int num = (int) (Math.random() * (max + 1));
// double decide = Math.random();
// if (decide < 0.333) {
// bitMap.add(num);
// set.add(num);
// } else if (decide < 0.666) {
// bitMap.delete(num);
// set.remove(num);
// } else {
// if (bitMap.contains(num) != set.contains(num)) {
// System.out.println("Oops!");
// break;
// }
// }
// }
// for (int num = 0; num <= max; num++) {
// if (bitMap.contains(num) != set.contains(num)) {
// System.out.println("Oops!");
// }
// }
// System.out.println("测试结束!");
}
考虑样本量(n)与失误率(p)
需要空间:m = - ( n * lnp) / (ln2)平方 哈希函数个数:k = ln2 * m / n ≈ 0.7 * m / n
3、详解一致性哈希原理—虚拟节点技术
有序表与并查集等
1、岛问题
一个矩阵中只有0和1两种值,每一个位置都和自己的上下左右四个位置相连如果有一片1连在一起这个部分叫做岛,这个矩阵岛屿的数目
public static int isLands(int[][] m)
{
if(m == null || m[0] == null)
{
return 0;
}
int res = 0;
int M = m.length;
int N = m[0].length;
for(int i = 0;i < M;i++)
{
for(int j = 0;j < i;j++)
{
if(a[i][j] == 1)
{
res++;
infect(m,i,j,N,M);
}
}
}
return res;
}
public static void infect(int[][] m,int i,int j,int N,int M)
{
if(i < 0 || i > N|| j < 0 || j > M || m[i][j] != 1)
{
return ;
}
m[i][j] = 2;
infect(m,i + 1,j,N,M);
infect(m,i - 1,j,N,M);
infect(m,i,j + 1,N,M);
infect(m,i,j - 1,N,M);
}
2、并查集:
public static class Node<V> {
V value;
public Node(V v) {
value = v;
}
}
public static class UnionFind<V> {
public HashMap<V, Node<V>> nodes;
public HashMap<Node<V>, Node<V>> parents;
public HashMap<Node<V>, Integer> sizeMap;
public UnionFind(List<V> values) {
nodes = new HashMap<>();
parents = new HashMap<>();
sizeMap = new HashMap<>();
for (V cur : values) {
Node<V> node = new Node<>(cur);
nodes.put(cur, node);
parents.put(node, node);
sizeMap.put(node, 1);
}
}
// 给你一个节点,请你往上到不能再往上,把代表返回
public Node<V> findFather(Node<V> cur) {
Stack<Node<V>> path = new Stack<>();
while (cur != parents.get(cur)) {
path.push(cur);
cur = parents.get(cur);
}
while (!path.isEmpty()) {
parents.put(path.pop(), cur);
}
return cur;
}
public boolean isSameSet(V a, V b) {
return findFather(nodes.get(a)) == findFather(nodes.get(b));
}
public void union(V a, V b) {
Node<V> aHead = findFather(nodes.get(a));
Node<V> bHead = findFather(nodes.get(b));
if (aHead != bHead) {
int aSetSize = sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
Node<V> small = big == aHead ? bHead : aHead;
parents.put(small, big);
sizeMap.put(big, aSetSize + bSetSize);
sizeMap.remove(small);
}
}
public int sets() {
return sizeMap.size();
}
}
3、KMP算法
3.1 题目一
字符串str1和字符串str2,str1是否包含str2,若包含返回str2在str1中的开始位置
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int x = 0;
int y = 0;
// O(M) m <= n
int[] next = getNextArray(str2);
// O(N)
while (x < str1.length && y < str2.length) {
if (str1[x] == str2[y]) {
x++;
y++;
} else if (next[y] == -1) { // y == 0
x++;
} else {
y = next[y];
}
}
return y == str2.length ? x - y : -1;
}
public static int[] getNextArray(char[] str2) {
if (str2.length == 1) {
return new int[] { -1 };
}
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
while (i < next.length) {
if (str2[i - 1] == str2[cn]) { // 配成功的时候
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
KMP和Manacher算法
1、Manacher算法
1.1 字符串str中,最长回文子串的长度如何求解?
public static int manacher(String s) {
if (s == null || s.length() == 0) {
return 0;
}
// "12132" -> "#1#2#1#3#2#"
char[] str = manacherString(s);
// 回文半径的大小
int[] pArr = new int[str.length];
int C = -1;
// 讲述中:R代表最右的扩成功的位置
// coding:最右的扩成功位置的,再下一个位置
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i < str.length; i++) { // 0 1 2
// R第一个违规的位置,i>= R
// i位置扩出来的答案,i位置扩的区域,至少是多大。
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;//不用验的区域
while (i + pArr[i] < str.length && i - pArr[i] > -1) {
if (str[i + pArr[i]] == str[i - pArr[i]])
pArr[i]++;
else {
break;
}
}
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);
}
return max - 1;
public static char[] manacherString(String str) {
char[] charArr = str.toCharArray();
char[] res = new char[str.length() * 2 + 1];
int index = 0;
for (int i = 0; i != res.length; i++) {
res[i] = (i & 1) == 0 ? '#' : charArr[index++];
}
return res;
}
/*
伪代码
*/
for(int i = 0;i < str.length;i++)
{
if(i在外部){
i暴力扩
}else{
if(i 的回文区域在L - R内)
{
pArr[i] = 某个表达式
}
else if(回文区域有一部风在外部){
pArr[i] = 某个表达式
}else{i的回文区域L-R左边界重合
以R外侧字符扩增
}
}
}
2、窗口的最大值最小值更新结构
由一个代表题目,引出一种结构
【题目】
有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。
例如,数组为[4,3,5,4,3,3,6,7门,窗口大小为3时:
[4 3 5 ] 4 3 3 6 7
4 [ 3 5 4 ] 3 3 6 7
4 3 [ 5 4 3 ] 3 6 7
4 3 5 [ 4 3 3 ] 6 7
4 3 5 4 [ 3 3 6 ] 7
4 3 5 4 3 [ 3 6 7 ]
窗口中最大值为5窗口中最大值为5窗口中最大值为5窗口中最大值为4窗口中最大值为6窗口中最大值为7
如果数组长度为 n ,窗ロ大小为 w ,则一共产生 n - w +1个窗口的最大值。
请实现一个函数。输入:整型数组 arr ,窗口大小为 W 。
输出:一个长度为 n - w +1的数组 res , res [ i ]表示每一种窗口状态下的以本题为例,结果应该返回(5,5,5,4,6.7}。
// 暴力的对数器方法
public static int[] right(int[] arr, int w) {
if (arr == null || w < 1 || arr.length < w) {
return null;
}
int N = arr.length;
int[] res = new int[N - w + 1];
int index = 0;
int L = 0;
int R = w - 1;
while (R < N) {
int max = arr[L];
for (int i = L + 1; i <= R; i++) {
max = Math.max(max, arr[i]);
}
res[index++] = max;
L++;
R++;
}
return res;
}
public static int[] getMaxWindow(int[] arr, int w) {
if (arr == null || w < 1 || arr.length < w) {
return null;
}
// qmax 窗口最大值的更新结构
// 放下标
LinkedList<Integer> qmax = new LinkedList<Integer>();
int[] res = new int[arr.length - w + 1];
int index = 0;
for (int R = 0; R < arr.length; R++) {
while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) {
qmax.pollLast();
}
qmax.addLast(R);
if (qmax.peekFirst() == R - w) {
qmax.pollFirst();
}
if (R >= w - 1) {
res[index++] = arr[qmax.peekFirst()];
}
}
return res;
}
3、单调栈结构
在数组(有重复值的与无重复值的)中想找到一个数,左边和右边比这个数小(大)、且离这个数最近的位置。
如果对每一个数都想求这样的信息,能不能整体代价达到O(N)?需要使用到单调栈结构
public static int[][] getNearLessNoRepeat(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
int popIndex = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
res[popIndex][0] = leftLessIndex;
res[popIndex][1] = i;
}
stack.push(i);
}
while (!stack.isEmpty()) {
int popIndex = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
res[popIndex][0] = leftLessIndex;
res[popIndex][1] = -1;
}
return res;
}
public static int[][] getNearLess(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<List<Integer>> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
List<Integer> popIs = stack.pop();
// 取位于下面位置的列表中,最晚加入的那个
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
stack.peek().size() - 1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = i;
}
}
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
stack.peek().add(Integer.valueOf(i));
} else {
ArrayList<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
while (!stack.isEmpty()) {
List<Integer> popIs = stack.pop();
// 取位于下面位置的列表中,最晚加入的那个
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
stack.peek().size() - 1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = -1;
}
}
return res;
}
3.1题目一
定义:正数数组中累积和与最小值的乘积,假设叫做指标A。给定一个数组,请返回子数组中,指标A最大的值。
public static int max1(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
int minNum = Integer.MAX_VALUE;
int sum = 0;
for (int k = i; k <= j; k++) {
sum += arr[k];
minNum = Math.min(minNum, arr[k]);
}
max = Math.max(max, minNum * sum);
}
}
return max;
}
public static int max2(int[] arr) {
int size = arr.length;
int[] sums = new int[size];
sums[0] = arr[0];
for (int i = 1; i < size; i++) {
sums[i] = sums[i - 1] + arr[i];
}
int max = Integer.MIN_VALUE;
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < size; i++) {
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
}
return max;
}
public static int[] gerenareRondomArray() {
int[] arr = new int[(int) (Math.random() * 20) + 10];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 101);
}
return arr;
}
滑动窗口单调栈等
第八节课 二叉树递归套路里有—1.1与1.2都有
1、树形dp套路
1.1二叉树节点间的最大距离问题
从二叉树的节点 a 出发,可以向上或者向下走,但沿途的节点只能经过一次,到达节点 b 时路径上的节点个数叫作 a 到 b 的距离,那么二叉树任何两个节点之间都有距离,求整棵树上的最大距离
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public static int maxDistance(Node head) {
int[] record = new int[1];
return posOrder(head, record);
}
public static class ReturnType{
public int maxDistance;
public int h;
public ReturnType(int m, int h) {
this.maxDistance = m;;
this.h = h;
}
}
public static ReturnType process(Node head) {
if(head == null) {
return new ReturnType(0,0);
}
ReturnType leftReturnType = process(head.left);
ReturnType rightReturnType = process(head.right);
//三个可能最大值
int includeHeadDistance = leftReturnType.h + 1 + rightReturnType.h;
int p1 = leftReturnType.maxDistance;
int p2 = rightReturnType.maxDistance;
//
int resultDistance = Math.max(Math.max(p1, p2), includeHeadDistance);
//头结点所以+
int hitself = Math.max(leftReturnType.h, leftReturnType.h) + 1;
return new ReturnType(resultDistance, hitself);
}
public static int posOrder(Node head, int[] record) {
if (head == null) {
record[0] = 0;
return 0;
}
int lMax = posOrder(head.left, record);
int maxfromLeft = record[0];
int rMax = posOrder(head.right, record);
int maxFromRight = record[0];
int curNodeMax = maxfromLeft + maxFromRight + 1;
record[0] = Math.max(maxfromLeft, maxFromRight) + 1;
return Math.max(Math.max(lMax, rMax), curNodeMax);
}
1.2 派对最大快乐值
派对的最大快乐值
员工信息的定义如下:
class Employee {
public int happy; // 这名员工可以带来的快乐值
List subordinates; // 这名员工有哪些直接下级
}
公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、没有环的多叉树。树的头节点是公司唯一的老板。除老板之外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每个员工都有一个或多个直接下级。
这个公司现在要办party,你可以决定哪些员工来,哪些员工不来。但是要遵循如下规则。
1.如果某个员工来了,那么这个员工的所有直接下级都不能来
2.派对的整体快乐值是所有到场员工快乐值的累加
3.你的目标是让派对的整体快乐值尽量大
给定一棵多叉树的头节点boss,请返回派对的最大快乐值。
增强版解释:
两种情况:
- 自己来:自己的快乐值 + 下级各个不来参加的情况下每个树的最大快乐值
- 自己不来:0 + Math.max(下级在来的情况下整棵树的最大值,下级在不来情况下整棵树的最大值)
public static int maxHappy(int[][] matrix) {
int[][] dp = new int[matrix.length][2];
boolean[] visited = new boolean[matrix.length];
int root = 0;
for (int i = 0; i < matrix.length; i++) {
if (i == matrix[i][0]) {
root = i;
}
}
process(matrix, dp, visited, root);
return Math.max(dp[root][0], dp[root][1]);
}
public static void process(int[][] matrix, int[][] dp, boolean[] visited, int root) {
visited[root] = true;
dp[root][1] = matrix[root][1];
for (int i = 0; i < matrix.length; i++) {
if (matrix[i][0] == root && !visited[i]) {
process(matrix, dp, visited, i);
dp[root][1] += dp[i][0];
dp[root][0] += Math.max(dp[i][1], dp[i][0]);
}
}
}
2、Morris遍历
一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)
通过利用原树中大量空闲指针的方式,达到节省空间的目的
2.1遍历细节
Morris遍历细节
假设来到当前节点cur,开始时cur来到头节点位置
1)如果cur没有左孩子,cur向右移动(cur = cur.right)
2)如果cur有左孩子,找到左子树上最右的节点mostRight:
a.如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
b.如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
3)cur为空时遍历停止
2.2遍历
- 先序遍历: 只过一次 直接打印 ; 过两次 第一次打印
- 中序遍历 : 只过一次 直接打印 ; 两次第二次打印
- 后序遍历: 逆序打印左树右边界 ; 单打整棵树右边界(逆序)
搜索二叉树:左树的值小于节点,右树的值大于节点 ; 中序遍历这棵树是升序就是的
public static class Node {
public int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
//中序
public static void morrisIn(Node head) {
if (head == null) {
return;
}
Node cur1 = head;
Node cur2 = null;
//3情况
while (cur1 != null) {
cur2 = cur1.left;
//2情况
if (cur2 != null) {
//不断向右侧 在有指针为空 或者有指针已经指向cur停止 否则循环
while (cur2.right != null && cur2.right != cur1) {
cur2 = cur2.right;
}
//a情况
if (cur2.right == null) {
cur2.right = cur1;
cur1 = cur1.left;
continue;
} else {
//b情况
cur2.right = null;
}
}
System.out.print(cur1.value + " ");
//1情况
cur1 = cur1.right;
}
System.out.println();
}
//先序
public static void morrisPre(Node head) {
if (head == null) {
return;
}
Node cur1 = head;
Node cur2 = null;
while (cur1 != null) {
cur2 = cur1.left;
if (cur2 != null) {
while (cur2.right != null && cur2.right != cur1) {
cur2 = cur2.right;
}
if (cur2.right == null) {
cur2.right = cur1;
System.out.print(cur1.value + " ");
cur1 = cur1.left;
continue;
} else {
cur2.right = null;
}
} else {//无左子树
System.out.print(cur1.value + " ");
}
cur1 = cur1.right;
}
System.out.println();
}
//后序遍历
public static void morrisPos(Node head) {
if (head == null) {
return;
}
Node cur1 = head;
Node cur2 = null;
while (cur1 != null) {
cur2 = cur1.left;
if (cur2 != null) {
while (cur2.right != null && cur2.right != cur1) {
cur2 = cur2.right;
}
if (cur2.right == null) {
cur2.right = cur1;
cur1 = cur1.left;
continue;
} else {
cur2.right = null;
printEdge(cur1.left);
}
}
cur1 = cur1.right;
}
printEdge(head);
System.out.println();
}
public static void printEdge(Node head) {
Node tail = reverseEdge(head);
Node cur = tail;
while (cur != null) {
System.out.print(cur.value + " ");
cur = cur.right;
}
reverseEdge(tail);
}
public static Node reverseEdge(Node from) {
Node pre = null;
Node next = null;
while (from != null) {
next = from.right;
from.right = pre;
pre = from;
from = next;
}
return pre;
}
3、总结
所想方法需要做第三次的信息的强整合则用二叉树的递归套路
所想方法不需要第三次 最优解则可以Morris
4、大数据题目的解题技巧
1)哈希函数可以把数据按照种类均匀分流
2)布隆过滤器用于集合的建立与查询,并可以节省大量空间
3)一致性哈希解决数据服务器的负载管理问题
4)利用并查集结构做岛问题的并行计算
5)位图解决某一范围上数字的出现情况,并可以节省大量空间
6)利用分段统计思想、并进一步节省大量空间
7)利用堆、外排序来做多个处理单元的结果合并
之前的课已经介绍过前4个内容,本节内容为介绍解决大数据题目的后3个技巧
4.1题目一
32位无符号整数的范围是0~4,294,967,295,现在有一个正好包含40亿个无符号整数的文件,所以在整个范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有未出现过的数?
【进阶】 内存限制为 10MB,但是只用找到一个没出现过的数即可
6 大数据题目等
1、暴力递归到动态规划
动态规划就是暴力尝试减少重复计算的技巧整,而已
这种技巧就是一个大型套路
先写出用尝试的思路解决问题的递归函数,而不用操心时间复杂度
这个过程是无可替代的,没有套路的,只能依靠个人智慧,或者足够多的经验
但是怎么把尝试的版本,优化成动态规划,是有固定套路的,大体步骤如下
1)找到什么可变参数可以代表一个递归状态,也就是哪些参数一旦确定,返回值就确定了
2)把可变参数的所有组合映射成一张表,有 1 个可变参数就是一维表,2 个可变参数就是二维表,…
3)最终答案要的是表中的哪个位置,在表中标出
4)根据递归过程的 base case,把这张表的最简单、不需要依赖其他位置的那些位置填好值
5)根据递归过程非base case的部分,也就是分析表中的普遍位置需要怎么计算得到,那么这张表的填写顺序也就确定了
6)填好表,返回最终答案在表中位置的值
1.1机器人达到指定位置方法数
【题目】
假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于2。开始时机器人在其中的M位置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到1位置,那么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到N-1位置。规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是1~N 中的一个)的方法有多少种。给定四个参数 N、M、K、P,返回方法数。 【举例】
N=5,M=2,K=3,P=3
上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在2 位置上,必须经过3步,最后到达 3 位置。走的方法只有如下 3 种:
1)从2到1,从1到2,从2到3
2)从2到3,从3到2,从2到3
3)从2到3,从3到4,从4到3
所以返回方法数 3。 N=3,M=1,K=3,P=3
上面的参数代表所有位置为 1 2 3。机器人最开始在 1 位置上,必须经过3 步,最后到达3位置。怎么走也不可能,所以返回方法数 0。
//最直接的暴力尝试 递归
public static int ways1(int N, int M, int K, int P) {
// 参数无效直接返回0
if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
return 0;
}
// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
return walk(N, M, K, P);
}
// N : 位置为1 ~ N,固定参数
// cur : 当前在cur位置,可变参数
// rest : 还剩res步没有走,可变参数
// P : 最终目标位置是P,固定参数
// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回
public static int walk(int N, int cur, int rest, int P) {
// 如果没有剩余步数了,当前的cur位置就是最后的位置
// 如果最后的位置停在P上,那么之前做的移动是有效的
// 如果最后的位置没在P上,那么之前做的移动是无效的
if (rest == 0) {
return cur == P ? 1 : 0;
}
// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
// 后续的过程就是,来到2位置上,还剩rest-1步要走
if (cur == 1) {
return walk(N, 2, rest - 1, P);
}
// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
if (cur == N) {
return walk(N, N - 1, rest - 1, P);
}
// 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右
// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走
// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走
// 走向左、走向右是截然不同的方法,所以总方法数要都算上
return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
}
//计划搜索 加入傻缓存
// 1-N的位置 目标E 剩余步数S 当前位置K
public static int walkway(int N,int E,int S,int K)
{
int[][] dp = new int[K + 1][N + 1];
for(int i = 0;i <= K;i++)
{
for(int j = 0; <= N;j++)
{
dp[i][j] = -1;
}
}
return f1(N,E,S,K,dp);
}
public static int f1(int N,int E,int rest,int cur,int[][] dp)
{
if(dp[rest][cur] != -1)
{
return dp[rest][cur];
}
if(rest == 0)
{
dp[rest][cur] = cur ==E ? 1 :0;
return cur ==E ? 1 :0;
}
if(cur == 1)
{
dp[rest][cur] = f1(N,E,rest - 1,2,dp);
}else if(cur == N)
{
dp[rest][cur] = f1(N,E,rest - 1,cur - 1,dp);
}else
{
dp[rest][cur] = f1(N,E,rest - 1,cur + 1,dp) + f1(N,E,rest - 1,cur - 1,dp);
}
return dp[rest][cur];
}
public static int ways2(int N, int M, int K, int P) {
// 参数无效直接返回0
if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
return 0;
}
int[][] dp = new int[K + 1][N + 1];
dp[0][P] = 1;
for (int i = 1; i <= K; i++) {
for (int j = 1; j <= N; j++) {
if (j == 1) {
dp[i][j] = dp[i - 1][2];
} else if (j == N) {
dp[i][j] = dp[i - 1][N - 1];
} else {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
}
}
}
return dp[K][M];
}
public static int ways3(int N, int M, int K, int P) {
// 参数无效直接返回0
if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
return 0;
}
int[] dp = new int[N + 1];
dp[P] = 1;
for (int i = 1; i <= K; i++) {
int leftUp = dp[1];// 左上角的值
for (int j = 1; j <= N; j++) {
int tmp = dp[j];
if (j == 1) {
dp[j] = dp[j + 1];
} else if (j == N) {
dp[j] = leftUp;
} else {
dp[j] = leftUp + dp[j + 1];
}
leftUp = tmp;
}
}
return dp[M];
}
2、换钱的最少货币数
2.1题目一
【题目】
给定数组 arr,arr 中所有的值代表硬币的面值可以重复。每一个值代表一枚硬币,给定一个整数 aim,代表要找的钱数,求组成aim的最少硬币数。
暴力递归:
public static int minCoins1(int[] arr, int aim) {
process(arr,0,aim);
}
public static int process(int [] arr,int index,int rest)
{
if(rest < 0)
{
return -1;
}
if(rest == 0)
{
return 0;
}
if(index == arr.length)
{
return -1;
}
//rest > 0而且有银币
int p1 = process(arr,index + 1,rest);
int p2Next = process(arr,index + 1,rest - arr[index]);
if(p1 == -1 && p2Next == -1)
{
return -1;
}else{
if(p1 == -1)
{
return p2Next + 1;
}
if(p2Next == -1)
{
return p1;
}
return Math.min(p1,p2Next+ 1);
}
}
计划搜索:
public static int minCoins1(int[] arr, int aim) {
int[][] dp = new int[arr.length + 1][aim + 1];
for(int i = 0;i <= arr.length;i++)
{
for(int j = 0; <= aim;j++)
{
dp[i][j] = -2;
}
}
process2(arr,0,aim);
}
public static int process2(int [] arr,int index,int rest,int[][] dp)
{
if(rest < 0)
{
return -1;
}
if(dp[index][rest] != -2)
{
return dp[index][rest];
}
if(rest == 0)
{
dp[index][rest] = 0;
}else if(index == arr.length){
dp[index][rest] = -1;
}else{
int p1 = process2(arr,index + 1,rest,dp);
int p2Next = process2(arr,index + 1,rest - arr[index],dp);
if(p1 == -1 && p2Next == -1)
{
dp[index][rest] = -1;
}else{
if(p1 == -1)
{
dp[index][rest] = p2Next + 1;
}
if(p2Next == -1)
{
dp[index][rest] = p1;
}else
{
dp[index][rest] = Math.min(p1,p2Next+ 1);
}
}
}
return dp[index][rest];
}
dp:
public static int minCoins1(int[] arr, int aim) {
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
for(int row = 0; row <= N;row++)
{
dp[row][0] = 0;
}
for(int col = 1;col <= aim;col++)
{
dp[N][col] = -1;
}
for(int index = N - 1;index >= 0;index--)
{
for(int rest = 1;rest <= aim;rest++)
{
int p1 = dp[index + 1][rest];
int p2Next = -1;
if(rest - arr[index] >= 0)
{
p2Next = dp[index + 1][rest - arr[index]]
}
if(p1 == -1 && p2 Next == -1)
{
dp[index][rest] = -1;
}else{
if(p1 == -1)
{
dp[index][rest] = p2Next + 1;
}
if(p2Next == -1)
{
dp[index][rest] = p1;
}
dp[index][rest] = Math.min(p1,p2Next + 1);
}
}
}
return dp[0][aim];
}
2.2题目二
题目】
给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数 aim,代表要找的钱数,求组成aim的最少货币数。
【举例】
arr=[5,2,3],aim=20。
4 张 5 元可以组成 20 元,其他的找钱方案都要使用更多张的货币,所以返回4。
arr=[5,2,3],aim=0。
不用任何货币就可以组成 0 元,返回 0。
arr=[3,5],aim=2。
根本无法组成 2 元,钱不能找开的情况下默认返回-1。
public static int minCoins1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return -1;
}
return process(arr, 0, aim);
}
// 当前考虑的面值是arr[i],还剩rest的钱需要找零
// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest
// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数
public static int process(int[] arr, int i, int rest) {
// base case:
// 已经没有面值能够考虑了
// 如果此时剩余的钱为0,返回0张
// 如果此时剩余的钱不是0,返回-1
if (i == arr.length) {
return rest == 0 ? 0 : -1;
}
// 最少张数,初始时为-1,因为还没找到有效解
int res = -1;
// 依次尝试使用当前面值(arr[i])0张、1张、k张,但不能超过rest
for (int k = 0; k * arr[i] <= rest; k++) {
// 使用了k张arr[i],剩下的钱为rest - k * arr[i]
// 交给剩下的面值去搞定(arr[i+1..N-1])
int next = process(arr, i + 1, rest - k * arr[i]);
if (next != -1) { // 说明这个后续过程有效
res = res == -1 ? next + k : Math.min(res, next + k);
}
}
return res;
}
public static int minCoins2(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return -1;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
// 设置最后一排的值,除了dp[N][0]为0之外,其他都是-1
for (int col = 1; col <= aim; col++) {
dp[N][col] = -1;
}
for (int i = N - 1; i >= 0; i--) { // 从底往上计算每一行
for (int rest = 0; rest <= aim; rest++) { // 每一行都从左往右
dp[i][rest] = -1; // 初始时先设置dp[i][rest]的值无效
if (dp[i + 1][rest] != -1) { // 下面的值如果有效
dp[i][rest] = dp[i + 1][rest]; // dp[i][rest]的值先设置成下面的值
}
// 左边的位置不越界并且有效
if (rest - arr[i] >= 0 && dp[i][rest - arr[i]] != -1) {
if (dp[i][rest] == -1) { // 如果之前下面的值无效
dp[i][rest] = dp[i][rest - arr[i]] + 1;
} else { // 说明下面和左边的值都有效,取最小的
dp[i][rest] = Math.min(dp[i][rest],
dp[i][rest - arr[i]] + 1);
}
}
}
}
return dp[0][aim];
}
7、暴力递归上
1.1排成一条线的纸牌博弈问题
【题目】
给定一个整型数组 arr,代表数值不同的纸牌排成一条线。玩家A 和玩家B 依次拿走每张纸牌,规定玩家 A 先拿,玩家 B 后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家 B 都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]。
开始时,玩家 A 只能拿走 1 或 4。如果玩家 A 拿走 1,则排列变为[2,100,4],接下来玩家B可以拿走 2 或 4,然后继续轮到玩家 A。如果开始时玩家A 拿走4,则排列变为[1,2,100],接下 来玩家 B 可以拿走 1 或 100,然后继续轮到玩家 A。玩家A 作为绝顶聪明的人不会先拿4,因为 拿 4 之后,玩家 B 将拿走 100。所以玩家 A 会先拿1,让排列变为[2,100,4],接下来玩家 B 不管 怎么选,100 都会被玩家 A 拿走。玩家 A 会获胜,分数为101。所以返回101。arr=[1,100,2]。
开始时,玩家 A 不管拿 1 还是 2,玩家 B 作为绝顶聪明的人,都会把100 拿走。玩家B会获胜,分数为 100。所以返回 100。
首先博弈的先后手问题 是比较难以考虑的,先手函数调用的后手函数,后手函数调用的先手函数,而且 要明白先后手的情况是相对而言的,继而我们在改动暴力递归到dp的时候就要考虑缓存的先手dp的数组缓存后手信息,反之一样。
他也是范围性的尝试 正方形,左下半部分 无效
//暴力尝试
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));
}
public static int win2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int[][] f = new int[arr.length][arr.length];
int[][] s = new int[arr.length][arr.length];
for (int j = 0; j < arr.length; j++) {
f[j][j] = arr[j];
for (int i = j - 1; i >= 0; i--) {
f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
}
}
return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
}
1.2象棋中马的跳法
【题目】
请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种?
考虑:由于象棋的特殊性,他必须以特定的位置才能到达x,y 那么以x,y 反推需要到达那个位置才能到达x,y 则下述的函数的八个位置才能到达。base case 很好确定 递归暴力很好实现
public static int getWays(int x, int y, int step) {
return process(x, y, step);
}
//目的地:x,y位置 步数step
//返回方法数目
public static int process(int x, int y, int step) {
if (x < 0 || x > 8 || y < 0 || y > 9) {
return 0;
}
if (step == 0) {
return (x == 0 && y == 0) ? 1 : 0;
}
return process(x - 1, y + 2, step - 1)
+ process(x + 1, y + 2, step - 1)
+ process(x + 2, y + 1, step - 1)
+ process(x + 2, y - 1, step - 1)
+ process(x + 1, y - 2, step - 1)
+ process(x - 1, y - 2, step - 1)
+ process(x - 2, y - 1, step - 1)
+ process(x - 2, y + 1, step - 1);
}
public static int dpWays(int x, int y, int step) {
if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
return 0;
}
int[][][] dp = new int[9][10][step + 1];
dp[0][0][0] = 1;
for (int h = 1; h <= step; h++) {
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 10; c++) {
dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
}
}
}
return dp[x][y][step];
}
public static int getValue(int[][][] dp, int row, int col, int step) {
if (row < 0 || row > 8 || col < 0 || col > 9) {
return 0;
}
return dp[row][col][step];
}
1.3Bob的生存概率
【题目】
给定五个参数n,m,i,j,k。表示在一个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、下、左、右四个方向移动一步,Bob必须走K步。如果走完之后,Bob还停留在这个区域上,就算Bob存活,否则就算Bob死亡。请求解Bob的生存概率,返回字符串表示分数的方式。
public static String bob1(int N, int M, int i, int j, int K) {
long all = (long) Math.pow(4, K);
long live = process(N, M, i, j, K);
long gcd = gcd(all, live);
return String.valueOf((live / gcd) + "/" + (all / gcd));
}
public static long process(int N, int M, int row, int col, int rest) {
if (row < 0 || row == N || col < 0 || col == M) {
return 0;
}
if (rest == 0) {
return 1;
}
long live = process(N, M, row - 1, col, rest - 1);
live += process(N, M, row + 1, col, rest - 1);
live += process(N, M, row, col - 1, rest - 1);
live += process(N, M, row, col + 1, rest - 1);
return live;
}
public static long gcd(long m, long n) {
return n == 0 ? m : gcd(n, m % n);
}
public static String bob2(int N, int M, int i, int j, int K) {
int[][][] dp = new int[N + 2][M + 2][K + 1];
for (int row = 1; row <= N; row++) {
for (int col = 1; col <= M; col++) {
dp[row][col][0] = 1;
}
}
for (int rest = 1; rest <= K; rest++) {
for (int row = 1; row <= N; row++) {
for (int col = 1; col <= M; col++) {
dp[row][col][rest] = dp[row - 1][col][rest - 1];
dp[row][col][rest] += dp[row + 1][col][rest - 1];
dp[row][col][rest] += dp[row][col - 1][rest - 1];
dp[row][col][rest] += dp[row][col + 1][rest - 1];
}
}
}
long all = (long) Math.pow(4, K);
long live = dp[i + 1][j + 1][K];
long gcd = gcd(all, live);
return String.valueOf((live / gcd) + "/" + (all / gcd));
}
2、第六节课的2.2题目
出现枚举类型的优化题目