9 树形dp套路
树形dp使用前提:如果题目求解目标是S规则,则求解流程可以定成以每一个节点为头节点的子树在S规则下的每一个答案,并且最终答案一定在其中。
树形dp套路第一步:
以某个节点X为头节点的子树中,分析答案有哪些可能性,并且这种分析是以X的左子树、X的右子树和X整棵树的角度来考虑可能性的
树形dp套路第二步:
根据第一步的可能性分析,列出所有需要的信息
树形dp套路第三步:
合并第二步的信息,对左子树和右树提出同样的要求,并写出信息结构
树形dp套路第四步:
设计递归函数,递归函数是处理以X为头节点的情况下的答案。
包括设计递归的basecase,默认直接得到左树和右树的所有信息,以及把可能性做整合,并且要返回第三步的信息结构着四个小步骤。
问题1:二叉树节点间的最大距离问题
从二叉树的节点a出发,可以向上或者向下走,但沿途的节点只能经过一次,到达节点b时路径上的节点个数叫做a到b的距离,那么二叉树任何两个节点之间都有距离,求整棵树上的最大距离。
public class MaxDistance {
public static class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
}
public static int getMaxDistance(Node node){
return process(node).maxDistance;
}
//对于左子树和右子树需要知道的信息:最大距离、树的高度
public static class Info{
int maxDistance; //和节点个数有关
int height;
public Info(int maxDistance,int height){
this.maxDistance = maxDistance;
this.height = height;
}
}
public static Info process(Node node){
if(node == null){
return new Info(0,0);
}
Info leftInfo = process(node.left);
Info rightInfo = process(node.right);
//当前节点的信息应该怎么处理
int p1 = leftInfo.maxDistance;
int p2 = rightInfo.maxDistance;
int p3 = leftInfo.height + rightInfo.height + 1; //经过当前节点的距离
int maxDistance = Math.max(p3,Math.max(p1,p2));
int height = Math.max(leftInfo.height,rightInfo.height) + 1;
return new Info(maxDistance,height);
}
}
问题2:派对的最大快乐值
员工信息的定义如下:
class Employee{
public int happy;//这名员工可以带来的快乐值
List<Employee> subordinates;//这名员工有哪些直接下级
}
公司的每个员工都符合Employee类的描述,整个公司的人员结构可以看作是一颗标准的。没有环的多叉树。树的头节点是公司的唯一老板。出老板之外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),出基层员工之外,每个员工都有一个或多个直接下级。
这个公司现在要办party,你可以决定哪些员工来,那些员工不来。但是要遵循以下规则。
1.如果某个员工来了,那么这个员工的所有直接下属都不能来
2.派对的整体快乐值是所有到场员工快乐值的累加
3.你的目标是让派对的整体快乐值尽量大
给定一颗多叉树的头节点boss,请返回派对的最大快乐值。
/**
* 派对的最大快乐值
*/
public class MaxPartyHappy {
public static class Employee{
public int happy;//这名员工可以带来的快乐值
List<Employee> subordinates;//这名员工有哪些直接下级
public Employee(int happy){
this.happy = happy;
}
}
public static int getPartyMaxHappy(Employee boss){
Info info = process(boss);
return Math.max(info.laiMaxHappy,info.buMaxHappy);
}
//参与和不参与对于X节点都需要获得的信息
public static class Info{
public int laiMaxHappy;
public int buMaxHappy;
public Info(int laiMaxHappy,int buMaxHappy){
this.laiMaxHappy = laiMaxHappy;
this.buMaxHappy = buMaxHappy;
}
}
public static Info process(Employee emp){
if(emp == null){
return new Info(0,0);
}
int laiHappy = emp.happy; //emp参与时,整棵树的最大快乐值要包括其快乐值
int buHappy = 0; //emp不参与,其快乐值为0
for(Employee e : emp.subordinates){
Info eInfo = process(e);
laiHappy += eInfo.buMaxHappy; //当前员工参与,其直接下级员工只能选择不参与下的最大的快乐值
buHappy += Math.max(eInfo.laiMaxHappy,eInfo.buMaxHappy);
}
return new Info(laiHappy,buHappy);
}
}
10 二叉树的Morris遍历
一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(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为空时遍历停止
public static class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
}
public static void morris(Node head){
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
mostRight = mostRight.right;
}
//mostRight变成了左树上的最右节点
if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
mostRight.right = cur;
cur = cur.left;
continue; //第一次来到过后,会直接进入下一次循环
}else { //第二次来到
mostRight.right = null;
}
}
cur = cur.right;
}
}
可以根据morris遍历进行二叉树的先序、中序、后续遍历
先序遍历:节点只经过一次的,直接打印,经过两次的,打印第一次
public static void morris_Pre(Node head){ //先序遍历:节点只来一次,直接打印,来两次,打印第一次
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
mostRight = mostRight.right;
}
//mostRight变成了左树上的最右节点
if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
System.out.println(cur.value);
mostRight.right = cur;
cur = cur.left;
continue; //第一次来到过后,会直接进入下一次循环
}else { //第二次来到
mostRight.right = null;
}
}else {
System.out.println(cur.value);
}
cur = cur.right;
}
}
中序遍历:节点只经过一次的,直接打印,经过两次的,打印第二次
public static void morris_Mid(Node head){ //中序遍历:节点只来一次,直接打印,来两次,打印第二次
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
mostRight = mostRight.right;
}
//mostRight变成了左树上的最右节点
if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
mostRight.right = cur;
cur = cur.left;
continue; //第一次来到过后,会直接进入下一次循环
}else { //第二次来到
mostRight.right = null;
}
}
System.out.println(cur.value);
cur = cur.right;
}
}
后序遍历:打印第二次经过节点的左树的右边界(逆序打印)
public static void morris_Pos(Node head){
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur != null){
mostRight = cur.left;
if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
mostRight = mostRight.right;
}
//mostRight变成了左树上的最右节点
if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
mostRight.right = cur;
cur = cur.left;
continue; //第一次来到过后,会直接进入下一次循环
}else { //第二次来到
mostRight.right = null;
printEdge(cur.left);//打印第二次经过节点的左数的右边界(逆序)
}
}
cur = cur.right;
}
printEdge(head);//整颗树跑完,单独打印整棵树左树的右边界(逆序)
System.out.println();
}
//以node为头的树,逆序打印这棵树的右边界
public static void printEdge(Node node){
Node tail = reverseEdge(node);
Node cur = tail;
while (cur != null){
System.out.print(cur.value + "\t");
cur = cur.right;
}
reverseEdge(tail);
}
public static Node reverseEdge(Node node){
Node pre = null;
Node next = null;
while(node != null){
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}
11 大数据题目解题技巧
1)哈希函数可以把数据按照种类均匀分流
2)布隆过滤器用于集合的建立与查询,并可以节省大量空间
3)一致性哈希解决数据服务器的负载管理问题
4)利用并查集结构做岛问题的并行计算
5)位图解决某一范围上数字的出现情况,并可以节省大量空间
6)利用分段统计思想,并进一步节省大量空间
7)利用堆、外排序来做多个处理单元的结果合并
问题:32位无符号整数的范围使0~4294967295,现在有一个正好包含40亿个无符号整数的文件,所以在整数范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有未出现过的数?
将0~2^32-1范围用位图表示,每一位表示范围内的一个值,然后出现过的数对应位图描黑即可。
[进阶]内存限制为10MB,但是只用找到一个没出现过的数即可。
10KB 10000/4=2500 0~2^11范围
把无符号整数个数2^32划分出2^10个数,这样数的范围就是0~2^22-1,2^22~2*2^22-1,......;然后40亿个数进行划分,一定会出现有一个范围内不满的情况。在这个不满足的范围内继续划分......
问题:有一个包含100亿URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL
哈希、布隆过滤器
[补充]某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门Top词汇的可行办法。
方法一:通过哈希,将这些词汇分配,并且重复发词频出现则加加。
方法二:通过堆的方式,词汇还是根据哈希分类,将同一文件的词频数排列大根堆的方式,那么大根堆的堆顶就当前文件出现次数最多的词汇,再将每个大根堆的堆顶都取出放进总堆(各个大根堆最大的元素进行比较),再从总堆中输出即可。
问题:32位无符号整数的范围使0~4294967295,现在有一个正好包含40亿个无符号整数的文件,所以在整数范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有出现了两次的数?
哈希函数分流、位图(用两个位表示X出现的次数)
[补充]可以使用最多10MB的内存,怎么找到40亿个整数的中位数?
10KB 整型数组一个占4字节,10KB/4=2.5KB=2500B 2^11=2048
把无符号数2^32个根据给定的内存10KB划分出2048个范围,申请一个2048长度的数组。每个数组元素存放的是该范围内出现数的个数,然后找到数组元素累加和位20亿的范围,再此范围上进行寻找第20亿个数。
12 位运算题目
问题1:给定两个有符号32位整数a和b,返回a和b中较大的。[不用做任何判断]
public class getMaxNum {
//保证参数n,不是1就是0的情况下
//n = 1 --> 0
//n = 0 --> 1
public static int flip(int n){
return n ^ 1;
}
//n是非负数,返回1
//n是负数,返回0
public static int sign(int n){
return flip( (n >> 31) & 1 ); //取出n的符号位位
}
public static int getMax1(int a,int b){
int c = a - b; //可能会溢出
int scA = sign(c); //a-b非负,scA为 1;a-b为负,scA为 0
int scB = flip(scA); //scA 为0,scB为1; scA为1,scB为0
//
return a * scA + b * scB;
}
public static int getMax2(int a,int b){
int c = a - b;
int sa = sign(a); //a的符号状态 ,非负为1,负数为0
int sb = sign(b);
int sc = sign(c);
int difSab = sa ^ sb; //a和b的符号不一样,为1;一样,为0
int sameSab = flip(difSab); //a和b的符号一样,为1,不一样,为0
int returnA = difSab * sa + sameSab * sc;
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
}
问题2:判断一个32位正数是不是2的幂、4的幂
public class twoorfourpower {
//看这个数是不是只有一个1
// 2^0 = ...00000001;2^1 = ...00000010;2^2 = ...00000100;2^3 = ...00001000
public static boolean is2Power(int n){
return ( n & (n - 1) ) == 0;//x&(x – 1)是将x的最后一个1置0
}
//一个数是4的幂一定先是2的幂
public static boolean is4Power(int n){
//......01010101 4的幂位是1
return ( n & (n - 1) ) == 0 && ( n & 0x55555555 ) != 0;
}
}
问题3:给定两个有符号32位整数a和b,不能使用算术运算符,分别实现a和b的加、减、乘、除运算[如果给定a、b执行加减乘除的运算结果就会导致数据的溢出,那么你实现的函数不必对此负责,除此之外请保证计算过程不发生溢出]
public class jiajianchengchu {
public static int add(int a,int b){
int sum = a;
while (b != 0){
sum = a ^ b; //相当于无进位相加
b = (a & b) << 1; //进位信息
a = sum;
}
return sum;
}
public static int negNum(int n){
return add(~n,1);
}
public static int minus(int a,int b){ //a - b = a + (-b) b的相反数就是b取反加1
return add(a,negNum(b));
}
public static int multi(int a,int b){
int res = 0;
while (b != 0){
if((b & 1) != 0){
res = add(res,a);
}
a <<= 1; //左移
b >>>= 1; //右移,看最后一位是不是1
}
return res;
}
public static boolean isNeg(int n){
return n < 0;
}
public static int div(int a,int b){ //b左移多少位(扩大倍数),可以减掉的话,对应位数位1
//先把数都变为正数
int x = isNeg(a) ? negNum(a) : a;
int y = isNeg(b) ? negNum(b) : b;
int res = 0;
for(int i = 31;i > -1;i--){ //只能处理正数
//右移比左移安全
if((x >> i) >= y){ //x右移多少位还比y大,说明x至少是y的 2^i 倍 (y << i) <= x可能会有溢出的情况
res |= (1 << i); //把对应位置为1
x = minus(x,y << i);
}
}
return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}
}
13 暴力递归->动态规划
问题1:给定N个数,一个机器人在起点S,可以走K步,到达E点,问有多少种方法?
暴力递归->记忆化搜索->动态规划 一定要画表分析
public class Robot {
//E:终点 S:起点 K:给定的步数
public static int getWay1(int N,int E,int S,int K){
return f1(N,E,K,S);
}
/**
*
* @param N 一共右1~N个位置,固定参数,不会改变的
* @param E 最终要走到的位置 E 也是固定参数,不会改变的
* @param rest 当前机器人还有多少步没有走
* @param cur 当前机器人所在的位置
* @return 返回方法数
*/
public static int f1(int N,int E,int rest,int cur){
if(rest == 0){ //判断走完给定的步数后,机器人是否到终点了,到了方法就加一,没到该路线就没有用
return cur == E ? 1 : 0;
}
//rest > 0 机器人还没走完
//在 1 的位置和 N 的位置机器人走向是固定的
if(cur == 1){
return f1(N,E,rest - 1,2);
}
if(cur == N){
return f1(N,E,rest - 1,N - 1);
}
//不在端点的位置
return f1(N,E,rest - 1,cur - 1) + f1(N,E,rest - 1,cur + 1);
}
//记忆化搜索:在原来的递归上加入缓存记录,这样走到的对应位置,直接拿方法数即可
public static int getWay2(int N,int E,int S,int K){
int[][] dp = new int[K+1][N+1]; //记录机器人走到不同位置的方法数
for(int i = 0;i <= K;i++){ //将数组的只都初始化位-1
for(int j = 0;j <= N;j++){
dp[i][j] = -1;
}
}
return f2(N,E,S,K,dp);
}
public static int f2(int N,int E,int rest,int cur,int[][] dp){
if(dp[rest][cur] != -1){ //不是-1,说明该位置已被走过,直接拿方法数即可
return dp[rest][cur];
}
//缓存没有命中
if(rest == 0){
dp[rest][cur] = cur == E ? 1 : 0;
return dp[rest][cur];
}
//rest > 0 没走完
if(cur == 1){
dp[rest][cur] = f2(N,E,rest - 1,2,dp);
}else if(cur == N){
dp[rest][cur] = f2(N,E,rest - 1,N - 1,dp);
}else {
dp[rest][cur] = f2(N, E, rest - 1, cur - 1, dp) + f2(N, E, rest - 1, cur + 1, dp);
}
return dp[rest][cur];
}
//动态规划,根据递归结构对缓存数组进行分析,得到各个位置之间的关系
public static int getWay3(int N,int E,int S,int K){
int[][] dp = new int[K+1][N+1]; //dp[...][0]废了不用
dp[0][E] = 1; //终点的位置为 1
for(int rest = 1;rest <= K;rest++){
for(int cur = 1;cur <= N;cur++){
if(cur == 1){
dp[rest][cur] = dp[rest - 1][2];
}else if (cur == N){
dp[rest][cur] = dp[rest - 1][N - 1];
}else {
dp[rest][cur] = dp[rest - 1][cur - 1] + dp[rest - 1][cur + 1];
}
}
}
return dp[K][S]; //根据原问题递归 f1(N,E,K,S) 可知最终要的是 K,S位置的值
}
}
问题2:给定一组数,其中每个数代表一枚硬币,找到组成aim数的最少硬币
public class Coin {
public static int getAim(int[] arr,int aim){
return process(arr,0,aim);
}
/**
*
* @param arr 硬币都在其中,固定参数
* @param index 如果自由选择arr[index.......]这些硬币
* @param rest 还有多少钱没有组成
* @return
*/
public static int process(int[] arr,int index,int rest){
if(rest < 0){
return -1;
}
if(rest == 0){
return 0;
}
//rest > 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,1 + p2Next);
}
}
//记忆化搜索
public static int getAim2(int[] arr,int aim){
int[][] dp = new int[arr.length+1][aim+1];
//初始化这张表
for(int i = 0;i < dp.length;i++){
for(int j = 0;j < dp[i].length;j++){
dp[i][j] = -2;
}
}
return process2(arr,0,aim,dp);
}
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;
return 0;
}else if(index == arr.length){ //给定的硬币无法组成
dp[index][rest] = -1;
return -1;
}else { //rest > 0 并且还有 硬币
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] = 1 + p2Next;
}
if (p2Next == -1) {
dp[index][rest] = p1;
}
dp[index][rest] = Math.min(p1,1 + p2Next);
}
}
return dp[index][rest];
}
//动态规划
public static int getAim3(int[] arr,int aim){
int[][] dp = new int[arr.length+1][aim+1];
for(int index = 0;index <= arr.length;index++){ //第一列都为0
dp[index][0] = 0;
}
for(int rest = 0;rest <= aim;rest++){ //最后一行都为0
dp[arr.length][rest] = -1;
}
for(int index = arr.length - 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 && p2Next == -1){
dp[index][rest] = -1;
}else {
if(p1 == -1){
dp[index][rest] = 1 + p2Next;
}
if(p2Next == -1){
dp[index][rest] = p1;
}
dp[index][rest] = Math.min(p1,1 + p2Next);
}
}
}
return dp[0][aim];
}
}
问题3:马走棋盘
/**
* 一只马在棋盘上走K步到end点,有多少种方法
*/
public class HouseJump {
public static int getWays(){
return process(0,0,10);
}
//默认马在 (0,0)位置
//要去往(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 - 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 + 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 getWay2(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; //第0层的一个面只有(0,0)为1,其他都为0
for(int h= 1;h <= step;h++){
for(int x1 = 0;x1 < 9;x1++){
for (int y1 = 0;y1 < 10;y1++){
dp[x1][y1][h] += getValue(dp,x1 - 1,y1 + 2,h - 1);
dp[x1][y1][h] += getValue(dp,x1 - 1,y1 - 2,h - 1);
dp[x1][y1][h] += getValue(dp,x1 - 2,y1 + 1,h - 1);
dp[x1][y1][h] += getValue(dp,x1 - 2,y1 + 1,h - 1);
dp[x1][y1][h] += getValue(dp,x1 + 1,y1 + 2,h - 1);
dp[x1][y1][h] += getValue(dp,x1 + 1,y1 - 2,h - 1);
dp[x1][y1][h] += getValue(dp,x1 + 2,y1 + 1,h - 1);
dp[x1][y1][h] += getValue(dp,x1 - 2,y1 - 1,h - 1);
}
}
}
return dp[x][y][step];
}
//防止越界,越界就是0
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];
}
}
问题4:人存活概率
/**
* 人在给定一个范围内走,只可以上下左右
*/
public class PeopleLive {
// N M:表示范围的行和列 a b:人现在所在的位置 K:要走多少步
public static String live(int N,int M,int a,int b,int K){
long live = process(N,M,a,b,K);
long all = (long)Math.pow(4,K);
long gcd = gcd(all,live);
return String.valueOf((live / gcd) + "/" + (all / gcd));
}
//获得a和b的最大公约数
public static long gcd(long a,long b){
return b == 0 ? a : gcd(a,a % b);
}
//N,M 区域(0~N-1 0~M-1),固定参数 x,y 当前位置 rest 还有多少步
public static long process(int N,int M,int x,int y,int rest){
if(x < 0 || x == N || y < 0 || y == M){
return 0;
}
//没越界
if (rest == 0){
return 1;
}
//还没走完并且没有越界
return process(N,M,x - 1,y,rest - 1) +
process(N,M,x + 1,y,rest - 1) +
process(N,M,x,y - 1,rest - 1) +
process(N,M,x,y + 1,rest - 1);
}
//动态规划
public static int live2(int N,int M,int x,int y,int step){
if(x < 0 || x == N || y < 0 || y == M || step < 0){
return 0;
}
int[][][] dp = new int[N][M][step+1];
for(int i = 0;i < N;i++){ //第0层的一个面都为1
for(int j = 0;j < M;j++){
dp[i][j][0] = 1;
}
}
for(int h= 1;h <= step;h++){
for(int x1 = 0;x1 < N;x1++){
for (int y1 = 0;y1 < M;y1++){
dp[x1][y1][h] += getValue(N,M,dp,x1 - 1,y1,h - 1);
dp[x1][y1][h] += getValue(N,M,dp,x1 + 1,y1,h - 1);
dp[x1][y1][h] += getValue(N,M,dp,x1,y1 + 1,h - 1);
dp[x1][y1][h] += getValue(N,M,dp,x1,y1 - 1,h - 1);
}
}
}
return dp[x][y][step];
}
public static int getValue(int N,int M,int[][][] dp,int row,int col,int step){
if(row < 0 || row > N-1 || col < 0 || col > M-1){
return 0;
}
return dp[row][col][step];
}
}
问题4:零钱组成数
动态规划中出现枚举行为,观察周围与自己是否有联系。
public class Coin2 {
//arr里都是正数,没有重复值,每一个值代表一种货币,每一种都可以用无线张
//最终要找零钱数 aim
//找零方法数返回
public static int way1(int[] arr,int aim){
return process(arr,0,aim);
}
//
public static int process(int[] arr,int index,int rest){
if(index == arr.length){
return rest == 0 ? 1 : 0;
}
//arr[index] 可以使用0张,一张,。。。不超过rest即可
int ways = 0;
for(int zhang = 0;arr[index] * zhang <= rest;zhang++){
ways += process(arr,index + 1,rest - arr[index] * zhang);
}
return ways;
}
//动态规划
public static int way2(int[] arr,int aim){
if(arr == null || arr.length == 0){
return 0;
}
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
dp[N][0] = 1; //初始时只有N行0列为1
for(int index = N-1;index >= 0;index--){ //从下网上计算的
for(int rest = 0;rest <= aim;rest++){
int ways = 0;
for(int zhang = 0;arr[index] * zhang <= rest;zhang++){
ways += dp[index + 1][rest - arr[index] * zhang];
}
dp[index][ways] = ways;
}
}
return dp[0][aim];
}
public static int way3(int[] arr,int aim){
if(arr == null || arr.length == 0){
return 0;
}
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
dp[N][0] = 1; //初始时只有N行0列为1
for(int index = N-1;index >= 0;index--){ //从下网上计算的
for(int rest = 0;rest <= aim;rest++){ //对枚举行为进行优化
dp[index][rest] = dp[index+1][rest];
if(rest - arr[index] >= 0){
dp[index][rest] += dp[index][rest - arr[index]];
}
}
}
return dp[0][aim];
}
}
14 有序表
删除节点、添加节点
红黑树 AVL树 SB树 跳表 时间复杂度O(logN)
红黑树、AVL树、SB树都属于搜索二叉树系列
①AVL树
二叉树的左旋和右旋(头节点倒向哪边,就是什么旋)
在二叉树在保证搜索二叉树的前提下,二叉树的左子树或者右子树可能会过长,就无法保证该二叉树是一颗平衡二叉树,因此对过长的那一颗子树进行左旋或者右旋。
LL型,进行右旋;RR型,进行左旋。对于LR型,先左旋再右旋;RL型,先右旋再左旋。
/**
* 有序表之平衡二叉树
*/
public class Map_AVLTree {
//平衡二叉树节点属性
static class AVLNode<K extends Comparable<K>,V>{
//封装平衡二叉树的节点信息
public K key;
public V value;
//二叉树的左右孩子
public AVLNode<K,V> left;
public AVLNode<K,V> right;
public int height; //树的高度
public AVLNode(K key,V value){
this.key = key;
this.value = value;
height = 1;
}
}
//有序表之平衡二叉树
public static class AVLTreeMap<K extends Comparable<K>,V>{
public AVLNode<K,V> root; //整个有序表的根节点
public int size; //已经加入的key的个数
public AVLTreeMap(){
root = null;
size = 0;
}
//获得node树的高度
private int height(AVLNode<K,V> node) {
return node == null ? 0 : node.height;
}
//更新node树的高度
private void updateNodeHeight(AVLNode<K,V> node){
if (node != null) node.height = Math.max(height(node.left),height(node.right)) + 1;
}
//对cur节点的整个树进行左旋
// 返回的是当前节点的右节点
// 当前cur节点的右节点 为 cur右节点的左节点
// 当前cur节点 为 之前cur右节点的左节点
private AVLNode<K,V> leftRotate(AVLNode<K,V> cur){ //左旋转后,当前节点cur 和 cur.right的高度都发生变化
AVLNode<K,V> temp = cur.right;
cur.right = temp.left;
temp.left = cur;
//左旋过后,cur变成子树,一定要先调整子树的高度
updateNodeHeight(cur);
updateNodeHeight(temp);
return temp;
}
//对cur节点的整棵树进行右旋
// 返回的是当前节点的左节点
// 当前节点cur的左节点 为 cur左节点的右节点
// 当前cur 节点 为 之前cur左节点的右节点
private AVLNode<K,V> rightRotate(AVLNode<K,V> cur){
AVLNode<K,V> temp = cur.left;
cur.left = temp.right;
temp.right = cur;
//右旋过后,cur变为子树,先调整子树的高度
updateNodeHeight(cur);
updateNodeHeight(temp);
return temp;
}
//平衡这颗二叉树,并返回平衡后的二叉树的根节点
private AVLNode<K,V> rebalance(AVLNode<K,V> node){
if(node == null){
return null;
}
int leftHeight = height(node.left);
int rightHeight = height(node.right);
int nodeBalance = rightHeight - leftHeight;
if(nodeBalance > 1){ // R
if(node.right.right != null){ // R RR 左移一次即可
node = leftRotate(node);
}else { // L RL 先对右节点右移,再对整棵树左移
node.right = rightRotate(node.right);
node = leftRotate(node);
}
}else if(nodeBalance < -1){ // L
if(node.left.left != null){ // L LL 右移一次即可
node = rightRotate(node);
}else {
node.left = leftRotate(node.left);
node = rightRotate(node);
}
}
return node;
}
//添加节点
// 在以cur为头的整颗子树上,加记录,并且把整棵树的头结点返回
public AVLNode<K,V> add(AVLNode<K,V> cur,K key,V value){
if(cur == null){
return new AVLNode(key,value);
}else {
if(key.compareTo(cur.key) < 0){
cur.left = add(cur.left,key,value);
}else {
cur.right = add(cur.right,key,value);
}
}
updateNodeHeight(cur);//更新cur节点树的高度
//每次添加,都需要进行一次二叉树的平衡
return rebalance(cur);
}
// 在cur这棵树上,删掉key所代表的节点
// 返回cur这棵树的新头部
public AVLNode<K,V> delete(AVLNode<K,V> cur,K key){
if(key.compareTo(cur.key) > 0){
cur.right = delete(cur.right,key);
}else if(key.compareTo(cur.key) < 0){
cur.left = delete(cur.left,key);
}else {
if(cur.left == null && cur.right == null){
cur = null;
}else if(cur.left == null && cur.right != null) {
cur = cur.right;
}else if(cur.left != null && cur.right == null){
cur = cur.left;
}else { //不是叶子节点,用右树上的最左节点代替
// 找到右树上的最左结点
AVLNode<K,V> des=cur.right;
while (des.left!=null){
des=des.left;
}
// 先在右树调一个delete()方法删掉最左结点,完成右树的平衡调整,
// 然后得到最左结点,替换要删除的结点,然后依次往上检查平衡性
cur.right=delete(cur.right,des.key);
des.left=cur.left;
des.right=cur.right;
cur=des;
}
}
updateNodeHeight(cur);
// cur会从要删除的节点处回退到根节点,每次要保证子树是平衡的
return rebalance(cur);
}
}
}
②SB树
平衡性:
每颗子树的大小,不小于其兄弟的子树大小;即每颗叔叔树的大小,不小于其任何子树的大小
[B] ≥ max{[G],[H]} [C] ≥ max{[E],[F]}
SB树的四种违规:目前所在的节点为节点A
LL违规:A节点的左孩子B的左孩子E的节点个数 > A节点的右孩子C的节点个数
LR违规:A节点的左孩子B的右孩子F的节点个数 > A节点的右孩子C的节点个数
RL违规:A节点的右孩子C的左孩子G的节点个数 > A节点的左孩子B的节点个数
RR违规:A节点的右孩子C的右孩子H的节点个数 > A节点的左孩子B的节点个数
不管是属于四种违规类型的哪种,调整方式跟AVL树一样,都是左旋或者右旋;唯一的区别就是旋转完后,哪个结点的孩子发生了变化,就要调到用平衡调整!
public class Map_SBTree {
public static class SBTNode<K extends Comparable<K>, V> {
public K key;
public V value;
public SBTNode<K, V> left;
public SBTNode<K, V> right;
public int size; // 不同的key的数量
public SBTNode(K key, V value) {
this.key = key;
this.value = value;
size = 1;
}
}
public static class SizeBalancedTreeMap<K extends Comparable<K>, V> {
private SBTNode<K, V> root;
//右旋转
private SBTNode<K, V> rightRotate(SBTNode<K, V> cur) {
SBTNode<K, V> leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
leftNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
return leftNode;
}
//左旋转
private SBTNode<K, V> leftRotate(SBTNode<K, V> cur) {
SBTNode<K, V> rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
rightNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
return rightNode;
}
private SBTNode<K, V> maintain(SBTNode<K, V> cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) { //LL
cur = rightRotate(cur);
//旋转过后,那个节点上的子节点变化了,就需要进行平衡调整
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) { //LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
//旋转过后,返回的新的头节点以及头节点的左右孩子都发生变化
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) { //RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) { //RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
// 现在,以cur为头的树上,新增,加(key, value)这样的记录
// 加完之后,会对cur做检查,该调整调整
// 返回,调整完之后,整棵树的新头部
private SBTNode<K, V> add(SBTNode<K, V> cur, K key, V value) {
if (cur == null) {
return new SBTNode<>(key, value);
} else {
cur.size++;
if (key.compareTo(cur.key) < 0) {
cur.left = add(cur.left, key, value);
} else {
cur.right = add(cur.right, key, value);
}
return maintain(cur);
}
}
// 在cur这棵树上,删掉key所代表的节点
// 返回cur这棵树的新头部
public SBTNode<K, V> delete(SBTNode<K, V> cur, K key) {
cur.size--;
if (key.compareTo(cur.key) < 0) {
cur.left = delete(cur.left, key);
} else if (key.compareTo(cur.key) > 0) {
cur.right = delete(cur.right, key);
} else {// 当前要删掉cur
if (cur.left == null && cur.right == null) {
// free cur memory -> C++
cur = null;
} else if (cur.left == null && cur.right != null) {
// free cur memory -> C++
cur = cur.right;
} else if (cur.left != null && cur.right == null) {
// free cur memory -> C++
cur = cur.left;
} else {// 左右孩子都有
// 找到cur的后继结点替换cur
SBTNode<K, V> pre = null;
SBTNode<K, V> des = cur.right;// des来到当前结点的右孩子
des.size--;
while (des.left != null) {
pre = des;
des = des.left;
des.size--;
}
// while循环结束后,des来到了cur结点的右树的最左孩子
// 并且此时的des是叶子结点,没有孩子了
// pre来到最左孩子的父亲结点
if (pre != null) {
pre.left = des.right;// 最左孩子的父亲断掉最左孩子
des.right = cur.right;// 最左孩子接管cur的右子树
}
des.left = cur.left;// 还是接管原来的左子树
des.size = des.left.size + (des.right == null ? 0 : des.right.size) + 1;
cur = des;
}
}
// return maintain(cur);
return cur;
}
}
}
④跳表
最左边的是header节点,不存值,上图的31,出现在了0,1,2,3层,其实就是一个节点。不是四个节点(这个要看具体的实现,这里是通过数组实现,可以通过下标访问,也可以通过链式实现)。这些层次信息是通过forwards(ArrayList)保存的。因此可以很快的访问到下一层。
每次插入新的数据时,如果这个数据没有,会随机生成一个等级level,表示该数据的层数,然后再插入到跳表中。
查询时,从头header的最高等级开始查询,每次查到 < key的最大一个数,然后查到对应数字,但没有查到0层,因此要以当前查到层数继续向下查 < key的最大一个数
eg:查询16,先从header最高层出发,发现31>16,不行,向下到第二层,查询到2<16,继续以第二场相后查询,发现31>16,不行,则再该数据向下层查询,在2的数据中查询第一层,查到15<16,则跳到15,继续以第一层向后查询,发现31>16,不行,则在15这个数向下一层查询,查询0层,发现16=16,满足。
import java.util.ArrayList;
public class Map_SkipList {
// 跳表的结点定义
public static class SkipListNode<K extends Comparable<K>,V>{
public K key;
public V val;
public ArrayList<SkipListNode<K,V>> nextNodes;
public SkipListNode(K k,V v){
key=k;
val=v;
nextNodes=new ArrayList<SkipListNode<K,V>>();
}
// 遍历的时候,如果是往右遍历到的null(next == null), 遍历结束
// 头(null), 头节点的null,认为最小
// node -> 头,node(null, "") node.isKeyLess(!null) true
// node里面的key是否比otherKey小,true,不是false
public boolean isKeyLess(K otherKey){
// otherKey==null -> false
return otherKey!=null && (key==null || key.compareTo(otherKey)<0);
}
public boolean isKeyEqual(K otherKey){
return (key==null && otherKey==null) ||
(key!=null && otherKey!=null && key.compareTo(otherKey)==0);
}
}
public static class SkipListMap<K extends Comparable<K>,V> {
private static final double PROBABILITY = 0.5;// <0.5继续做,>=0.5就停
private SkipListNode<K, V> head;
private int size;
private int maxLevel;
public SkipListMap() {
head = new SkipListNode<>(null, null);
head.nextNodes.add(null);
size = 0;
maxLevel = 0;
}
// 从最高层开始,一路找下去,
// 最终,找到第0层的 <key的最右的节点
private SkipListNode<K, V> mostRightLessNodeInTree(K key) {
if (key == null) {
return null;
}
int level = maxLevel;
SkipListNode<K, V> cur = head;
// 从上层跳下层
while (level >= 0) {
cur = mostRightLessNodeInLevel(key, cur, level--);
}
return cur;
}
// 在level层里,如何往右移动
// 现在来到的节点是cur,来到了cur的level层,在level层上,找到 <key最后一个节点并返回
private SkipListNode<K, V> mostRightLessNodeInLevel(K key, SkipListNode<K, V> cur, int level) {
// 上面层跳过一个,下面层就会跳过一批,优势就体现在这里
SkipListNode<K, V> next = cur.nextNodes.get(level);
while (next != null && next.isKeyLess(key)) {
cur = next;
next = cur.nextNodes.get(level);
}
return cur;
}
public boolean containsKey(K key) {
if (key == null) {
return false;
}
SkipListNode<K, V> less = mostRightLessNodeInTree(key);
SkipListNode<K, V> next = less.nextNodes.get(0);
return next != null && next.isKeyEqual(key);
}
// 新增,修改value
public void put(K key, V value) {
if (key == null) {
return;
}
// 0层上,最右一个,< key 的Node -> >key
SkipListNode<K, V> less = mostRightLessNodeInTree(key);
SkipListNode<K, V> find = less.nextNodes.get(0);
if (find != null && find.isKeyEqual(key)) {// 直接更新
find.val = value;
} else {// find==null
size++;
int newNodeLevel = 0;
while (Math.random() < PROBABILITY) {
newNodeLevel++;
}
// newNodeLevel
while (newNodeLevel > maxLevel) {
head.nextNodes.add(null);
maxLevel++;
}
SkipListNode<K, V> newNode = new SkipListNode<>(key, value);
for (int i = 0; i <= newNodeLevel; i++) {
newNode.nextNodes.add(null);
}
int level = maxLevel;
SkipListNode<K, V> pre = head;
while (level >= 0) {
// level 层中,找到最右的 < key 的节点
pre = mostRightLessNodeInLevel(key, pre, level);
if (level <= newNodeLevel) {
newNode.nextNodes.set(level, pre.nextNodes.get(level));
pre.nextNodes.set(level, newNode);
}
level--;
}
}
}
public void remove(K key) {
if (containsKey(key)) {
size--;
int level = maxLevel;
SkipListNode<K, V> pre = head;
while (level >= 0) {
pre = mostRightLessNodeInLevel(key, pre, level);
SkipListNode<K, V> next = pre.nextNodes.get(level);
// 1)在这一层中,pre下一个就是key
// 2)在这一层中,pre的下一个key是>要删除key
if (next != null && next.isKeyEqual(key)) {
// free delete node memory -> C++
// level : pre -> next(key) -> ...
// 前一个结点在level层的指针指向要删除的下一个结点
pre.nextNodes.set(level, next.nextNodes.get(level));
}
// 在level层只有一个节点了,就是默认节点head
if (level != 0 && pre == head && pre.nextNodes.get(level) == null) {
head.nextNodes.remove(level);
maxLevel--;
}
level--;
}
}
}
}
}