0%

P1

打广告

P2

1
2
3
4
5
6
7
8
9
1.所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。

2./* ... */ 用于注释说明。

3.printf() 用于格式化输出到屏幕。printf() 函数在 "stdio.h" 头文件中声明。

4.stdio.h 是一个头文件 (标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。

5.return 0; 语句用于表示退出程序。

P3

打印

1
2
3
printf("hello world");
print\
f("hi"})//反斜杠表示读下一行开头,可以运行,换行符\n;

p4

变量名:英文字母数字,下划线组成,开头必须是字母下划线。

c有32个关键字

第一类:数 据类型关键 字

A基本数据类 型(5个):

void: 声明函数无返回值或无参数,声明无类型指针,显式丢弃运算结果。

char: 字符型类型数据,属于整型数据的一种。

int: 整型数据,通常为编译器指定的机器字长。

float: 单精度浮点型数据,属于浮点数据的一种,小数点后保存6位。

double: 双精度浮点型数据,属于浮点数据的一种,比float保存的精度高,小数点后保存15/16位。

B类型修饰关 键字(4个):

short :修饰int,短整型数据,可省略被修饰的int。

long :修饰int,长整形数据,可省略被修饰的int。

signed :修饰整型数据,有符号数据类型。

unsigned: 修饰整型数据,无符号数据类型。

C复杂类型关 键字(5个):

struct :结构体声明。

union :共用体声明。

enum :枚举声明。

typedef :声明类型别名。

sizeof :得到特定类型或特定类型变量的大小。

D存储级别关 键字(6个):

auto :指定为自动变量,由编译器自动分配及释放。通常在栈上分配。

static :指定为静态变量,分配在静态变量区,修饰函数时,指定函数作用域为文件内部。

register :指定为寄存器变量,建议编译器将变量存储到寄存器中使用,也可以修饰函数形参,建议编译器通 过寄存器而不是堆栈传递参数。

extern :指定对应变量为外部变量,即在另外的目标文件中定义,可以认为是约定由另外文件声明的。

const :与volatile合称“cv特性”,指定变量不可被当前线程/进程改变(但有可能被系统或其他线程/进程改

volatile :与const合称“cv特性”,指定变量的值有可能会被系统或其他进程/线程改变,强制编译器每次从内存 中取得该变量的值。

第二类:流 程控制关键 字

A跳转结构(4 个):

return :用在函数体中,返回特定值(或者是void值,即不返回值)。

continue: 结束当前循环,开始下一轮循环。

break :跳出当前循环或switch结构。

goto :无条件跳转语句。

B分支结构(5 个):

if :条件语句。

else: 条件语句否定分支(与if连用)。

switch: 开关语句(多重分支语句)。

case :开关语句中的分支标记。

default: 开关语句中的“其他”分治,可选。

C循环结构(3 个):

for :for循环结构,for(1;2;3)4;的执行顺序为1->2->4->3->2…循环,其中2为循环条件。

do:do循环结构,do 1 while(2);的执行顺序是1->2->1…循环,2为循环条件。

while: while循环结构,while(1) 2;的执行顺序是1->2->1…循环,1为循环条件, 以上循环语句,当循环条件 表达式为真则继续循环,为假则跳出循环。

c99新增5个关键字

1、inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义

引入原因:C语言是一个效率很高的语言,这种宏定义在形式及使用上像一个函数,但它使用预处理器实现,没有了参数压栈,代码生成等一系列的操作

2、restrict关键字只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。

3、_Bool关键字是用于表示布尔值。包含标准头文件 stdbool.h 后,我们可以用 bool 代替 _Bool ,true 代替 1 ,false 代替 0 。

4、_Complexand_Imaginary关键字

C99标准中定义的复数类型如下:float_Complex; float_Imaginary; double_Complex; double_Imaginary; long double_Complex; long double_Imaginary.

头文件中定义了complex和imaginary宏,并将它们扩展为_Complex和_Imaginary,因此在编写新的应用程序时,应该使用头文件中的complex和imaginary宏。

C语言32个关键字(C99新增5个、C11新增7个)

按年份起始:

auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while1999年12月16日,ISO推出了C99标准,该标准新增了5个C语言关键字:

inline restrict _Bool _Complex _Imaginary2011年12月8日,ISO发布C语言的新标准C11,该标准新增了7个C语言关键字:

_Alignas _Alignof _Atomic _Static_assert _Noreturn _Thread_local _Generic

1
2
3
4
5
6
7
%d//int
%c//char
%11.9//11位,小数点后9位
gcc test.c -o test && ./test //这个-o可以改a.out为你想要的名字, 用&&可以合并编译运行两个阶段,而/ 的意思:目录级别分隔符
. 的意思:当前目录
./a 的意思就是:当前目录下文件名为“a”的文件。
Linux中还有 .. 代表上抄级目录

P5

字符常量单引号,字符串常量双引号

1
2
3
//宏定义, 标识符全大写,宏定义
#define 标识符 常量
#define pi 3.1415926535

C++ 语言可以用const 来定义常量,也可以用#define 来定义常量。但是前者比后者有更多的优点:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
(2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。规则5-2-1:在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。

2.实现机制

宏是预处理命令,即在预编译阶段进行字节替换。const常量是变量,在执行时const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态存储区的只读数据区。根据c/c++语法,当你声明该量为常量,即告诉程序和编译器,你不希望此量被修改。 程序的实现,为了保护常量,特将常量都放在受保护的静态存储区内。凡是试图修改这个区域内的值,都将被视为非法,并报错。 这不能理解为凡是字符串都是放在静态存储区域的。这个跟数据类型没有关系,而是这个量是变量还是常量的问题。例如,一个字符串变量就是可以被修改的。 这种静态存储区域的保护机制是由编译器实现的,而非存储该值的内存的电器属性。换言之,实质上内存永远都可以被用户随意修改,只是编译器给用户的代码注入了一些自己的保护代码,通过软件手段将这段内存软保护起来。这种保护在汇编级别可以轻松突破,其保护也就无效了。)。

3.用法区别

define宏定义和const常变量区别:
1.define是宏定义,程序在预处理阶段将用define定义的内容进行了替换。因此程序运行时,常量表中并没有用define定义的常量,系统不为它分配内存。const定义的常量,在程序运行时在常量表中,系统为它分配内存。
2.define定义的常量,预处理时只是直接进行了替换。所以编译时不能进行数据类型检验。const定义的常量,在编译时进行严格的类型检验,可以避免出错。3.define定义表达式时要注意“边缘效应”,例如如下定义:
#define N 2+3 //我们预想的N值是5,我们这样使用N,int a = N/2; //我们预想的a的值是2.5,可实际上a的值是3.5原因在于在预处理阶段,编译器将 a = N/2处理成了 a = 2+3/2;这就是宏定义的字符串替换的“边缘效应”因此要如下定义:#define N (2+3)。const定义表达式没有上述问题。const定义的常量叫做常变量原因有二:const定义常量像变量一样检查类型;const可以在任何地方定义常量,编译器对它的处理过程与变量相似,只是分配内存的地方不同。

1
字符串常量, '\0'遇到这个会知道字符串结束了

P6

C语言数据类型的分类方式如下:

  • 基本类型
    • 标准整数类型,以及扩充的整数类型
    • 实数浮点类型,以及复数浮点类型
  • 枚举类型
  • void类型
  • 派生类型
    • 指针类型
    • 数组类型
    • 结构类型
  • 联合类型
  • 函数类型
1
2
sizeof(a);//return size of a
sizeof(int);//return 4单位是字节

objective-c 中的BOOL 实际上是一种对带符号的字符类型(signed char)的类型定义(typedef),它使用8位的存储空间。通过#define指令把YES定义为1,NO定义为0。

C99标准定义了一个新的关键字_Bool,提供了布尔类内型。以前,C程序员总是使用自己的方法定义布尔类型。0表示false,非0表示true。

可能使用char类型表示一个布尔类型,也可能使用int类型表示一个布尔类型。

1、类型不同百 : BOOL为int型 , bool为布尔型

2、长度不同 : bool只有一个字节 , BOOL长度视实际环境来定,一般可认为是4个字节

3、取值不同 :bool取值false和true,是0和1的区别; false可以代表0,但true有很多种,并非度只有1。

4、bool表示布尔型变量,也就是逻辑型变量的定义符,以英国数学家、布尔代数的奠基人乔治·布尔(George Boole)命名。

总结bool是true,false,而_Bool is 0,1 但是都是1字节。

1
sign unsigned//带符号不带符号,如果不用会引发栈溢出栈泄露

P7

printf是输出一个字符串,putchar输出一个char。

printf格式字符:

打印格式 对应数据类型 含义
%d int 接受整数值并将它表示为有符号的十进制整数
%hd short int 短整数
%hu unsigned short 无符号短整数
%o unsigned int 无符号8进制整数
%u unsigned int 无符号10进制整数
%x,%X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF
%f float 单精度浮点数
%lf double 双精度浮点数
%e,%E double 科学计数法表示的数,此处”e”的大小写代表在输出时用的”e”的大小写
%c char 字符型。可以把输入的数字按照ASCII码相应转换’对应的字符
%s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符)
%p void * 以16进制形式输出指针
%% % 输出一个百分号

%d 整形 int

%f 浮点型 float

%c 字符型 char

%hd 短整型 short

%ld 长整形 long

%lld 长长整形 long long

1
2
3
4
1bit=8 bytes
pow(a,b)//#include <math.h>,一般打印需要<stdio.h>, a的b次方
gcc -lm test.c && ./a.out// warning:overflow in implicit constant conversion
unsign int 用%u,sign int用%d

使用math.h中声明的库函数还有一点特殊之处,gcc命令行必须加-lm选项,因为数学函数位于libm.so库文件中(这些库文件通常位于/lib目录下),-lm选项告诉编译器,我们程序中用到的数学函数要到这个库文件里找。本书用到的大部分库函数(例如printf)位于libc.so库文件中,使用libc.so中的库函数在编译时不需要加-lc选项,当然加了也不算错,因为这个选项是gcc的默认选项。

C标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型 声明和宏定义。要在一个平台上支持C语言,不仅要实现C编译器,还要实现C标准库,这样的实现才算符合C标准。不符合C标准的实现也是存在的,例如很多单 片机的C语言开发工具中只有C编译器而没有完整的C标准库。

在Linux平台上最广泛使用的C函数库是glibc,其中包括C标准库的实现。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础。glibc提供一组头文件和一组库文件,最基本、最常用的C标准库函数和系统函数在libc.so库文件中,几乎所有C程序的运行都依赖于libc.so,有些做数学计算的C程序依赖于libm.so,以后我们还会看到多线程的C程序依赖于libpthread.so。以后我说libc时专指libc.so这个库文件,而说glibc时指的是glibc提供的所有库文件。

1. 原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:

[+1]原 = 0000 0001

[-1]原 = 1000 0001

第一位是符号位. 因为第一位是符号位, 所以8位二进制数的取值范围就是:

[1111 1111 , 0111 1111]

[-127 , 127]

原码是人脑最容易理解和计算的表示方式.

2. 反码

反码的表示方法是:

正数的反码是其本身

负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

[+1] = [00000001]原 = [00000001]反

[-1] = [10000001]原 = [11111110]反

可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算.

3. 补码

补码的表示方法是:

正数的补码就是其本身

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

[+1] = [00000001]原 = [00000001]反 = [00000001]补

[-1] = [10000001]原 = [11111110]反 = [11111111]补

对于负数, 补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值.

P8

字符用ASCII字符表写的,常用的ascii: 空格的ASCII码值为32;数字0到9的ASCII码值分别为48到57;大写字母“A”到“Z”的ASCII码值分别为65到90;小写字母“a”到“z”的ASCII码值分别为97到到122。

字符类型是特殊的整型,ascii有0-255个字符

1
unsigned char 170//这是正数,若不用unsigned,则是负数

字符串

1
char name[length];//0 to length-1
1
2
3
4
5
//经典问题
char a[2]={'a','b'};
printf("%s\n",a);
printf("Hi\n");//会出现错误,因为字符数组a没有\0,改法: char a[3]={'a','b','\0'};
char a[]="hi";//字符串常量

P9

1
2
3
4
5
6
7
5/3=1
5%3=2
5.0%3.0//出错
//强制类型转换
1+2.0;//->1.0+2.0
1+(int)2.0;//->3
1+(int)1.8;//2

P10

关系表达式返回0,1

c语言中没有true,false,只有1,0

c和java一样采用短路与和短路或

P1:C++和OO思想

经典面向对象java 一样封装继承多态

P2-P4:从小程序说起

1.类,异常,对象都是c++特有的

  1. 整形数组求和:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
       #include <stdio.h>
    int addArray(int array[], int n);//在main用前需要声明
    int main(){
    int data[]={0,1,2,3,4,5,6,7,8,9};
    int size=sizeof(data)/sizeof(data[0]);
    printf("data size: %ld\n",sizeof(data));//不要用%d,c99报错
    printf("answer :%d\n",addArray(data,size));
    return 0;
    }
    int addArray(int* array, int n){
    int sum=0;
    printf("data size: %ld\n",sizeof(array));
    for(int i=0;i<n;i++){
    sum+=array[i];
    }
    return sum;
    }
    /*data size: 40
    data size: 8
    answer :45
    */
1
//c++版本

94. 二叉树的中序遍历

难度中等477收藏分享切换为英文关注反馈

给定一个二叉树,返回它的中序 遍历。

示例:

1
2
3
4
5
6
7
8
输入: [1,null,2,3]
1
\
2
/
3

输出: [1,3,2]

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {//recursion
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList();
helper(root, res);
return res;
}
private void helper(TreeNode root, List<Integer> res){
if(root !=null){
if(root.left != null){
helper(root.left, res);
}
res.add(root.val);
if(root.right !=null){
helper(root.right,res);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {//iterative
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList();
Stack<TreeNode> s= new Stack();
TreeNode cur=root;
while(cur != null || !s.isEmpty()){
while(cur !=null){
s.push(cur);
cur=cur.left;
}
cur=s.pop();
res.add(cur.val);
cur=cur.right;
}
return res;
}
}

102. 二叉树的层序遍历

难度中等454收藏分享切换为英文关注反馈

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:
二叉树:[3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回其层次遍历结果:

1
2
3
4
5
[
[3],
[9,20],
[15,7]
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {//recursion
List<List<Integer>> levels= new ArrayList<List<Integer>>();
private void helper(TreeNode root, int level){
if(level==levels.size())
levels.add(new ArrayList<Integer>());
levels.get(level).add(root.val);
if(root.left != null)
helper(root.left, level+1);
if(root.right != null)
helper(root.right, level+1);
}
public List<List<Integer>> levelOrder(TreeNode root) {
if(root==null)
return levels;
helper(root, 0);
return levels;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {//iterative
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> levels= new ArrayList<List<Integer>>();
if(root==null) return levels;
Queue<TreeNode> q=new LinkedList<>();
q.add(root);
int level=0;
while(!q.isEmpty()){
levels.add(new ArrayList<Integer>());
int level_l= q.size();
for(int i=0; i<level_l;i++){
TreeNode n=q.remove();
levels.get(level).add(n.val);
if(n.left !=null) q.add(n.left);
if(n.right !=null) q.add(n.right);
}
level++;
}
return levels;
}
}

103. 二叉树的锯齿形层次遍历

难度中等183收藏分享切换为英文关注反馈

给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:
给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回锯齿形层次遍历如下:

1
2
3
4
5
[
[3],
[20,9],
[15,7]
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
protected void DFS(TreeNode node, int level, List<List<Integer>> results) {
if (level >= results.size()) {
LinkedList<Integer> newLevel = new LinkedList<Integer>();
newLevel.add(node.val);
results.add(newLevel);
} else {
if (level % 2 == 0)
results.get(level).add(node.val);
else
results.get(level).add(0, node.val);
}

if (node.left != null) DFS(node.left, level + 1, results);
if (node.right != null) DFS(node.right, level + 1, results);
}

public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<List<Integer>>();
}
List<List<Integer>> results = new ArrayList<List<Integer>>();
DFS(root, 0, results);
return results;
}
}

Intuition

Following the description of the problem, the most intuitive solution would be the BFS (Breadth-First Search) approach through which we traverse the tree level-by-level.

The default ordering of BFS within a single level is from left to right. As a result, we should adjust the BFS algorithm a bit to generate the desired zigzag ordering.

One of the keys here is to store the values that are of the same level with the deque (double-ended queue) data structure, where we could add new values on either end of a queue.

So if we want to have the ordering of FIFO (first-in-first-out), we simply append the new elements to the tail of the queue, i.e. the late comers stand last in the queue. While if we want to have the ordering of FILO (first-in-last-out), we insert the new elements to the head of the queue, i.e. the late comers jump the queue.

pic

Algorithm

There are several ways to implement the BFS algorithm.

  • One way would be that we run a two-level nested loop, with the outer loop iterating each level on the tree, and with the inner loop iterating each node within a single level.
  • We could also implement BFS with a single loop though. The trick is that we append the nodes to be visited into a queue and we separate nodes of different levels with a sort of delimiter (e.g. an empty node). The delimiter marks the end of a level, as well as the beginning of a new level.

Here we adopt the second approach above. One can start with the normal BFS algorithm, upon which we add a touch of zigzag*order with the help of deque. For each level, we start from an empty deque container to hold all the values of the same level. Depending on the ordering of each level, *i.e. either from-left-to-right or from-right-to-left, we decide at which end of the deque to add the new element:

pic

  • For the ordering of from-left-to-right (FIFO), we append the new element to the tail\ of the queue, so that the element that comes late would get out late as well. As we can see from the above graph, given an input sequence of [1, 2, 3, 4, 5], with FIFO ordering, we would have an output sequence of [1, 2, 3, 4, 5].
  • For the ordering of from-right-to-left (FILO), we insert the new element to the head\ of the queue, so that the element that comes late would get out first. With the same input sequence of [1, 2, 3, 4, 5], with FILO ordering, we would obtain an output sequence of [5, 4, 3, 2, 1].

Note: as an alternative approach, one can also implement the normal BFS algorithm first, which would generate the ordering of from-left-to-right for each of the levels. Then, at the end of the algorithm, we can simply reverse\ the ordering of certain levels, following the zigzag steps.

Complexity Analysis

  • Time Complexity: O(N)O(N), where NN is the number of nodes in the tree.

    • We visit each node once and only once.
    • In addition, the insertion operation on either end of the deque takes a constant time, rather than using the array/list data structure where the inserting at the head could take the O(K)O(K) time where KK is the length of the list.
  • Space Complexity: O(N)O(N) where NN is the number of nodes in the tree.

    • The main memory consumption of the algorithm is the node_queue that we use for the loop, apart from the array that we use to keep the final output.

    • As one can see, at any given moment, the node_queue would hold the nodes that are at most across two levels. Therefore, at most, the size of the queue would be no more than 2⋅L2⋅L, assuming LL is the maximum number of nodes that might reside on the same level. Since we have a binary tree, the level that contains the most nodes could occur to consist all the leave nodes in a full binary tree, which is roughly L=N2L=2N. As a result, we have the space complexity of 2⋅N2=N2⋅2N=N in the worst case.


Intuition

Though not intuitive, we could also obtain the BFS traversal ordering via the DFS (Depth-First Search) traversal in the tree.

The trick is that during the DFS traversal, we maintain the results in a global array that is indexed by the level, i.e. the element array[level] would contain all the nodes that are at the same level. The global array would then be referred and updated at each step of DFS.

pic

Similar with the above modified BFS algorithm, we employ the deque data structure to hold the nodes that are of the same level, and we alternate the insertion direction (i.e. either to the head or to the tail) to generate the desired output ordering.

Algorithm

Here we implement the DFS algorithm via recursion. We define a recursive function called DFS(node, level) which only takes care of the current node which is located at the specified level. Within the function, here are three steps that we would perform:

  • If this is the first time that we visit any node at the level, i.e. the deque for the level does not exist, then we simply create the deque with the current node value as the initial element.
  • If the deque for this level exists, then depending on the ordering, we insert the current node value either to the head or to the tail of the queue.
  • At the end, we recursively call the function for each of its child nodes.

It might go without saying that, one can also implement the DFS traversal via iteration\ rather than recursion, which could be one of the followup questions by an interviewer.

Complexity Analysis

  • Time Complexity: O(N)O(N), where NN is the number of nodes in the tree.
    • Same as the previous BFS approach, we visit each node once and only once.
  • Space Complexity: O(H)O(H), where HH is the height of the tree, i.e. the number of levels in the tree, which would be roughly log⁡2Nlog2N.
    • Unlike the BFS approach, in the DFS approach, we do not need to maintain the node_queue data structure for the traversal.
    • However, the function recursion would incur additional memory consumption on the function call stack. As we can see, the size of the call stack for any invocation of DFS(node, level) would be exactly the number of level that the current node resides on. Therefore, the space complexity of our DFS algorithm is O(log⁡2N)O(log2N) which is much better than the BFS approach.

107. 二叉树的层次遍历 II

难度简单235收藏分享切换为英文关注反馈

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

例如:
给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回其自底向上的层次遍历为:

1
2
3
4
5
[
[15,7],
[9,20],
[3]
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
List<List<Integer>> levels= new ArrayList<List<Integer>>();
private void helper(TreeNode node, int level){
if(levels.size()==level)
levels.add(new ArrayList<Integer>());
levels.get(level).add(node.val);
if(node.left!=null)
helper(node.left,level+1);
if(node.right!=null)
helper(node.right,level+1);
}
public List<List<Integer>> levelOrderBottom(TreeNode root) {
if(root==null) return levels;
helper(root,0);
Collections.reverse(levels);
return levels;
}
}

How to traverse the tree

There are two general strategies to traverse a tree:

  • Depth First Search (DFS)

    In this strategy, we adopt the depth as the priority, so that one would start from a root and reach all the way down to a certain leaf, and then back to root to reach another branch.

    The DFS strategy can further be distinguished as preorder, inorder, and postorder depending on the relative order among the root node, left node, and right node.

  • Breadth First Search (BFS)

    We scan through the tree level by level, following the order of height, from top to bottom. The nodes on a higher level would be visited before the ones on lower levels.

In the following figure the nodes are enumerated in the order you visit them, please follow 1-2-3-4-5 to compare different strategies.

postorder

Here the problem is to implement split-level BFS traversal : [[4, 5], [2, 3], [1]]. That means we could use one of the Node->Left->Right techniques: BFS or DFS Preorder.

We already discussed three different ways to implement iterative BFS traversal with the queue, and compared iterative BFS vs. iterative DFS. Let’s use this article to discuss the two most simple and fast techniques:

  • Recursive DFS.
  • Iterative BFS with two queues.

Note, that both approaches are root-to-bottom traversals, and we’re asked to provide bottom-up output. To achieve that, the final result should be reversed.


Approach 1: Recursion: DFS Preorder Traversal

Intuition

The first step is to ensure that the tree is not empty. The second step is to implement the recursive function helper(node, level), which takes the current node and its level as the arguments.

Algorithm for the Recursive Function

Here is its implementation:

  • Initialize the output list levels. The length of this list determines which level is currently updated. You should compare this level len(levels) with a node level level, to ensure that you add the node on the correct level. If you’re still on the previous level - add the new level by adding a new list into levels.
  • Append the node value to the last level in levels.
  • Process recursively child nodes if they are not None: helper(node.left / node.right, level + 1).

Implementation

Current

1 / 16

Complexity Analysis

  • Time complexity: O(N)O(N) since each node is processed exactly once.

  • Space complexity: O(N)O(N) to keep the output structure which contains NN node values.


Approach 2: Iteration: BFS Traversal

Algorithm

The recursion above could be rewritten in the iteration form.

Let’s keep each tree level in the queue structure, which typically orders elements in a FIFO (first-in-first-out) manner. In Java one could use ArrayDeque implementation of the Queue interface. In Python using Queue structure would be an overkill since it’s designed for a safe exchange between multiple threads and hence requires locking which leads to a performance downgrade. In Python the queue implementation with a fast atomic append() and popleft() is deque.

Algorithm

  • Initialize two queues: one for the current level, and one for the next. Add root into nextLevel queue.
  • While nextLevel queue is not empty:
    • Initialize the current level currLevel = nextLevel, and empty the next level nextLevel.
    • Iterate over the current level queue:
      • Append the node value to the last level in levels.
      • Add first left and then right child node into nextLevel queue.
  • Return reversed levels.

Implementation

Complexity Analysis

  • Time complexity: O(N)O(N) since each node is processed exactly once.
  • Space complexity: O(N)O(N) to keep the output structure which contains NN node values.

230. 二叉搜索树中第K小的元素

难度中等216收藏分享切换为英文关注反馈

给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。

说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

示例 1:

1
2
3
4
5
6
7
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 1

示例 2:

1
2
3
4
5
6
7
8
9
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 3

进阶:
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//recursion
class Solution {
private ArrayList<Integer> helper(TreeNode root, ArrayList<Integer> ans){
if(root==null) return ans;
helper(root.left, ans);
ans.add(root.val);
helper(root.right,ans);
return ans;

}
public int kthSmallest(TreeNode root, int k) {
ArrayList<Integer> num= helper(root, new ArrayList<Integer>());
return num.get(k-1);
}
}
//iterative
class Solution {
public int kthSmallest(TreeNode root, int k) {
LinkedList<TreeNode> stack = new LinkedList<TreeNode>();

while (true) {
while (root != null) {
stack.add(root);
root = root.left;
}
root = stack.removeLast();
if (--k == 0) return root.val;
root = root.right;
}
}
}

How to traverse the tree

There are two general strategies to traverse a tree:

  • Depth First Search (DFS)

    In this strategy, we adopt the depth as the priority, so that one would start from a root and reach all the way down to certain leaf, and then back to root to reach another branch.

    The DFS strategy can further be distinguished as preorder, inorder, and postorder depending on the relative order among the root node, left node and right node.

  • Breadth First Search (BFS)

    We scan through the tree level by level, following the order of height, from top to bottom. The nodes on higher level would be visited before the ones with lower levels.

On the following figure the nodes are numerated in the order you visit them, please follow 1-2-3-4-5 to compare different strategies.

postorder

To solve the problem, one could use the property of BST : inorder traversal of BST is an array sorted in the ascending order.


Approach 1: Recursion

It’s a very straightforward approach with O(N)O(N) time complexity. The idea is to build an inorder traversal of BST which is an array sorted in the ascending order. Now the answer is the k - 1th element of this array.

bla

Complexity Analysis

  • Time complexity : O(N)O(N) to build a traversal.

  • Space complexity : O(N)O(N) to keep an inorder traversal.


Approach 2: Iteration

The above recursion could be converted into iteration, with the help of stack. This way one could speed up the solution because there is no need to build the entire inorder traversal, and one could stop after the kth element.

bla

Complexity Analysis

  • Time complexity : O(H+k)O(H+k), where HH is a tree height. This complexity is defined by the stack, which contains at least H+kH+k elements, since before starting to pop out one has to go down to a leaf. This results in O(log⁡N+k)O(logN+k) for the balanced tree and O(N+k)O(N+k) for completely unbalanced tree with all the nodes in the left subtree.

  • Space complexity : O(H+k)O(H+k), the same as for time complexity, O(N+k)O(N+k) in the worst case, and O(log⁡N+k)O(logN+k) in the average case.


Follow up

What if the BST is modified (insert/delete operations) often and you need to find the kth smallest frequently? How would you optimize the kthSmallest routine?

Insert and delete in a BST were discussed last week, the time complexity of these operations is O(H)O(H), where HH is a height of binary tree, and H=log⁡NH=logN for the balanced tree.

Hence without any optimisation insert/delete + search of kth element has O(2H+k)O(2H+k) complexity. How to optimise that?

That’s a design question, basically we’re asked to implement a structure which contains a BST inside and optimises the following operations :

  • Insert
  • Delete
  • Find kth smallest

Seems like a database description, isn’t it? Let’s use here the same logic as for LRU cache design, and combine an indexing structure (we could keep BST here) with a double linked list.

Such a structure would provide:

  • O(H)O(H) time for the insert and delete.
  • O(k)O(k) for the search of kth smallest.

bla

The overall time complexity for insert/delete + search of kth smallest is O(H+k)O(H+k) instead of O(2H+k)O(2H+k).

Complexity Analysis

  • Time complexity for insert/delete + search of kth smallest: O(H+k)O(H+k), where HH is a tree height. O(log⁡N+k)O(logN+k) in the average case, O(N+k)O(N+k) in the worst case.
  • Space complexity : O(N)O(N) to keep the linked list.

501. 二叉搜索树中的众数

难度简单109收藏分享切换为英文关注反馈

给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。

假定 BST 有如下定义:

  • 结点左子树中所含结点的值小于等于当前结点的值
  • 结点右子树中所含结点的值大于等于当前结点的值
  • 左子树和右子树都是二叉搜索树

例如:
给定 BST [1,null,2,2],

1
2
3
4
5
1
\
2
/
2

返回[2].

提示:如果众数超过1个,不需考虑输出顺序

进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private List<Integer> items;
private int maxCount;
private int curCount;
private TreeNode pre;

public int[] findMode(TreeNode root) {
if (root == null)
return new int[0];
items = new ArrayList<>();
// 这里设置为1是因为 在递归中 BST中最左边的结点被跳过了,作为初状态 该结点值出现一次,记录的出现最多次数一开始也为1
maxCount = 1;
curCount = 1;
midTraversal(root);
// 最右端结点的处理,从递归中看,最后一个结点与前一个结点相等只更新了curCount的值;不相等则只考虑到倒数第二个结点。
if(curCount > maxCount)
return new int[]{pre.val};
if(curCount == maxCount)
items.add(pre.val);
int[] res = new int[items.size()];
for (int i = 0; i < res.length; i++)
res[i] = items.get(i);
return res;
}

private void midTraversal(TreeNode x) {
if (x == null) return;
midTraversal(x.left);
if(pre != null){
if(x.val != pre.val){ // 说明上一个值的结点数量已经统计完成 要看出现次数的情况
if(curCount > maxCount){ // 出现次数更多,清空之前的出现少的数,更新最大出现次数
maxCount = curCount;
items.clear();
items.add(pre.val);
}
else if(curCount == maxCount){ // 不止一个众数
items.add(pre.val);
}
curCount = 1; // 当前值与上一个结点值不同,重置计数
}
else curCount++; // 当前值与上一个结点值相同,当前值的出现次数增加。
}
pre = x;
midTraversal(x.right);
}


}

使用List即时存放出现最多的数

private List<Integer> items;
private int maxCount;
private int curCount;
private TreeNode pre;

public int[] findMode(TreeNode root) {
    if (root == null)
        return new int[0];
    items = new ArrayList<>();
    // 这里设置为1是因为 在递归中 BST中最左边的结点被跳过了,作为初状态 该结点值出现一次,记录的出现最多次数一开始也为1
    maxCount = 1;
    curCount = 1;
    midTraversal(root);
    // 最右端结点的处理,从递归中看,最后一个结点与前一个结点相等只更新了curCount的值;不相等则只考虑到倒数第二个结点。
    if(curCount > maxCount)
        return new int[]{pre.val};
    if(curCount == maxCount)
        items.add(pre.val);
    int[] res = new int[items.size()];
    for (int i = 0; i < res.length; i++)
        res[i] = items.get(i);
    return res;
}

private void midTraversal(TreeNode x) {
    if (x == null) return;
    midTraversal(x.left);
    if(pre != null){
        if(x.val != pre.val){ // 说明上一个值的结点数量已经统计完成 要看出现次数的情况
            if(curCount > maxCount){ // 出现次数更多,清空之前的出现少的数,更新最大出现次数
                maxCount = curCount;
                items.clear();
                items.add(pre.val);
            }
            else if(curCount == maxCount){ // 不止一个众数
                items.add(pre.val);
            }
            curCount = 1; // 当前值与上一个结点值不同,重置计数
        }
        else curCount++; // 当前值与上一个结点值相同,当前值的出现次数增加。
    }
    pre = x;
    midTraversal(x.right);
}

思路分析:

找众数,说明要统计出现次数,一般会直接想到搞个哈希表来计数(就像后面的方法二)。但是如果一个有序数组中统计出现次数,使用双指针就能很好解决,类似的这里给我们的树不是一般的树,而是BST,中序遍历相当于遍历有序数组。

利用BST这个特性,我们不需要使用哈希表来计数。如同双指针的做法,这里也需要记录上一个结点TreeNode pre;这样才能知道当前结点值与谁比较;另外还需要记录某个值的出现次数curCount,以及出现次数的最大值maxCount(否则你咋知道谁出现最多次)。并且这里遍历过程中的众数信息需要记录(List存放众数)及更新。

在中序遍历中:

如果pre == null,说明这是遍历的第一个结点,不需要处理(第一个结点的初条件在主函数中设定)。

如果当前结点值与上一个结点值相等,那么这个数字的出现次数+1。

否则,我们先去判断,上一个数字的出现次数curCount与之前的最大出现次数maxCount谁更大:

如果上一个数字出现次数最大,需要更新众数信息。首先更新最大出现次数maxCount = curCount;。然后将之前记录的众数清空,再将上一个数字放入items.clear(); items.add(pre.val);
如果一个数字出现次数等于最大出现次数,那么目前来看,它也是可能的众数,加入列表items.add(pre.val);
否则,上一个数字一定不是众数,不管它,继续保留List中的数字。
最后,重置计数curCount = 1;,表示当前数字出现一次了。
然后更新pre = x;

回到主函数:

特殊情况处理,root == null直接返回new int[0]。
然后上文提到的初始化,因为最左边结点在中序的递归中不处理,所以,我们要首先将maxCount = 1,因为树非空总会有数字出现一次,然后curCount = 1,代表最左边结点被我们在主函数计数了。
调用辅助函数后,还需要处理最后一个结点的值,因为在递归中不会有再下一个结点值与之不相等然后来判断它的值是否为众数。
所以如果curCount > maxCount,说明最后一个结点才是唯一众数,return new int[]{pre.val};
如果curCount == maxCount,说明最后一个结点也是众数,items.add(pre.val);
最后,将List转成数组返回。
这个题要注意边界的考虑!

时间复杂度为
O
(
n
)
O(n), 不考虑递归调用栈的话,空间复杂度可以认为是常数,中间状态List的元素并不会很多。

543. 二叉树的直径

难度简单372收藏分享切换为英文关注反馈

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 :
给定二叉树

1
2
3
4
5
    1
/ \
2 3
/ \
4 5

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
int ans=1;
public int depth(TreeNode node){
if(node==null) return 0;
int l=depth(node.left);
int r=depth(node.right);
ans=Math.max(l+r+1,ans);
return Math.max(l,r)+1;
}

public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return ans-1;
}
}

Approach #1: Depth-First Search [Accepted]

Intuition

Any path can be written as two arrows (in different directions) from some node, where an arrow is a path that starts at some node and only travels down to child nodes.

If we knew the maximum length arrows L, R for each child, then the best path touches L + R + 1 nodes.

Algorithm

Let’s calculate the depth of a node in the usual way: max(depth of node.left, depth of node.right) + 1. While we do, a path “through” this node uses 1 + (depth of node.left) + (depth of node.right) nodes. Let’s search each node and remember the highest number of nodes used in some path. The desired length is 1 minus this number.

Complexity Analysis

  • Time Complexity: O(N)O(N). We visit every node once.
  • Space Complexity: O(N)O(N), the size of our implicit call stack during our depth-first search.

663. Equal Tree Partition

Given a binary tree with n nodes, your task is to check if it’s possible to partition the tree to two trees which have the equal sum of values after removing exactly one edge on the original tree.

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Input:     
5
/ \
10 10
/ \
2 3

Output: True
Explanation:
5
/
10

Sum: 15

10
/ \
2 3

Sum: 15

Example 2:

1
2
3
4
5
6
7
8
9
Input:     
1
/ \
2 10
/ \
2 20

Output: False
Explanation: You can't split the tree into two trees with equal sum after removing exactly one edge on the tree.

Note:

  1. The range of tree node value is in the range of [-100000, 100000].
  2. 1 <= n <= 10000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
Stack <Integer> seen;
private int sum(TreeNode node){
if(node==null) return 0;
seen.push(sum(node.left)+sum(node.right)+node.val);
return seen.peek();
}
public boolean checkEqualTree(TreeNode root) {
seen=new Stack();
int total=sum(root);
seen.pop();
if(total%2==0){
for(int i: seen)
if(i==total/2)
return true;
}
return false;
}
}

Approach #1: Depth-First Search [Accepted]

Intuition and Algorithm

After removing some edge from parent to child, (where the child cannot be the original root) the subtree rooted at child must be half the sum of the entire tree.

Let’s record the sum of every subtree. We can do this recursively using depth-first search. After, we should check that half the sum of the entire tree occurs somewhere in our recording (and not from the total of the entire tree.)

Our careful treatment and analysis above prevented errors in the case of these trees:

1
2
3
4
5
6
7
  0
/ \
-1 1

0
\
0

Complexity Analysis

  • Time Complexity: O(N)O(N) where NN is the number of nodes in the input tree. We traverse every node.
  • Space Complexity: O(N)O(N), the size of seen and the implicit call stack in our DFS.

1339. 分裂二叉树的最大乘积

难度中等26收藏分享切换为英文关注反馈

给你一棵二叉树,它的根为 root 。请你删除 1 条边,使二叉树分裂成两棵子树,且它们子树和的乘积尽可能大。

由于答案可能会很大,请你将结果对 10^9 + 7 取模后再返回。

示例 1:

img

1
2
3
输入:root = [1,2,3,4,5,6]
输出:110
解释:删除红色的边,得到 2 棵子树,和分别为 11 和 10 。它们的乘积是 110 (11*10)

示例 2:

img

1
2
3
输入:root = [1,null,2,3,4,null,null,5,6]
输出:90
解释:移除红色的边,得到 2 棵子树,和分别是 15 和 6 。它们的乘积为 90 (15*6)

示例 3:

1
2
输入:root = [2,3,9,10,7,8,6,5,4,11,1]
输出:1025

示例 4:

1
2
输入:root = [1,1]
输出:1

提示:

  • 每棵树最多有 50000 个节点,且至少有 2 个节点。
  • 每个节点的值在 [1, 10000] 之间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
List<Integer> all=new ArrayList();
private int sum(TreeNode subroot){
if(subroot==null) return 0;
int l=sum(subroot.left);
int r=sum(subroot.right);
int total=l+r+subroot.val;
all.add(total);
return total;
}
public int maxProduct(TreeNode root) {
long total=sum(root);
long ans=0;
for(long i:all)
ans=Math.max(ans,i*(total-i));
return (int)(ans%1000000007);
}
}

Approach 1: One-Pass DFS

Intuition

To get started, we’re just going to pretend that integers can be infinitely large.

We’ll use the following tree example.

The tree example.

There are n - 1 edges in a tree with n nodes, and so for this question there are n - 1 different possible ways of splitting the tree into a pair of subtrees. Here are 4 out of the 10 possible ways.

4 possible ways of splitting the original tree.

Of these 4 possible ways, the best is the third one, which has a product of 651.

To make it easier to discuss the solution, we’ll name each of the subtrees in a pair.

  1. One of the new subtrees is rooted at the node below the removed edge. We’ll call it Subtree 1.
  2. The other is rooted at the root node of the original tree, and is missing the subtree below the removed edge. We’ll call it Subtree 2.

Diagram of Subtree 1 and Subtree 2.

Remember that we’re required to find the pair of subtrees that have the maximum product. This is done by calculating the sum of each subtree and then multiplying them together. The sum of a subtree is the sum of all the nodes in it.

Calculating the sum of Subtree 1 can be done using the following recursive tree algorithm. The root of Subtree 1 is passed into the function.

1
2
3
4
5
6
7
8
def tree_sum(subroot):
if subroot is None:
return 0
left_sum = tree_sum(subroot.left)
right_sum = tree_sum(subroot.right)
return subroot.val + left_sum + right_sum

print(tree_sum(sub_tree_1_root))

This algorithm calculates the sum of a subtree by calculating the sum of its left subtree, sum of its right subtree, and then adding these to the root value. The sum of the left and right subtrees is done in the same way by the recursion.

Diagram illustrating how sums are calculated.

If you’re confused by this recursive summing algorithm, it might help you to read this article on solving tree problems with recursive (top down) algorithms.

We still need a way to calculate the sum of Subtree 2. Recall that Subtree 2 is the tree we get by removing Subtree 1. The only way we could directly use the above summing algorithm to calculate the sum of Subtree 2 is to actually remove the edge above Subtree 1 first. Otherwise, Subtree 1 would be automatically traversed too.

A simpler way is to recognise that Sum(Subtree 2) = Sum(Entire Tree) - Sum(Sub Tree 1).

Diagram showing the relationship between subtree sums.

Another benefit of this approach is that we only need to calculate Sum(Entire Tree) once. Then, for each Sum(Subtree 1) we calculate, we can immediately calculate Sum(Subtree 2) as an O(1)O(1) operation.

Recall how the summing algorithm above worked. The recursive function is called once for every node in the tree (i.e. subtree rooted at that node), and returns the sum of that subtree.

Diagram of the recursive calls of part of the tree.

Therefore we can simply gather up all the possible Subtree 1 sums with a list as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
subtree_1_sums = [] # All Subtree 1 sums will go here.

def tree_sum(subroot):
if subroot is None:
return 0
left_sum = tree_sum(subroot.left)
right_sum = tree_sum(subroot.right)
subtree_sum = left_sum + right_sum + subroot.val
subtree_1_sums.append(subtree_sum) # Add this subtree sum to the list.
return subtree_sum

total_sum = tree_sum(root) # Call with the root of the entire tree.
print(subtree_1_sums) # This is all the subree sums.

Now that we have a list of the sums for all possible Subtree 1‘s, we can calculate what the corresponding Subtree 2 would be for each of them, and then calculate the product, keeping track of the best seen so far.

1
2
3
4
5
6
7
8
9
10
11
12
# Call the function.
subtree_1_sums = [] # Populate by function call.
total_sum = tree_sum(root)

best_product = 0
# Find the best product.
for subtree_1_sum in subtree_1_sums:
subtree_2_sum = total_sum - subtree_1_sum
product = subtree_1_sum * subtree_2_sum
best_product = max(best_product, product)

print(best_product)

The question also says we need to take the answer modulo 10 ^ 9 + 7. Expanded out, this number is 1,000,000,007. So when we return the product, we’ll do:

1
best_product % 1000000007

Only take the final product modulo 10 ^ 9 + 7. Otherwise, you might not be correctly comparing the products.


Up until now, we’ve assumed that integers can be of an infinite size. This is a safe assumption for Python, but not for Java. For Java (and other languages that use a 32-bit integer by default), we’ll need to think carefully about where integer overflows could occur.

The problem statement states that there can be up to 50000 nodes, each with a value of up to 10000. Therefore, the maximum possible subtree sum would be 50,000 * 10,000 = 500,000,000. This is well below the size of a 32-bit integer (2,147,483,647). Therefore, it is impossible for an integer overflow to occur during the summing phase with these constraints.

However, multiplying the subtrees could be a problem. For example, if we had subtrees of 100,000,000 and 400,000,000, then we’d get a total product of 400,000,000,000,000,000 which is definitely larger than a 32-bit integer, and therefore and overflow would occur!

The easiest solution is to instead use 64-bit integers. In Java, this is the long primitive type. The largest possible product would be 250,000,000 * 250,000,000 = 62,500,000,000,000,000‬, which is below the maximum a 64-bit integer can hold.

In Approach #3, we discuss other ways of solving the problem if you only had access to 32-bit integers.

Algorithm

Complexity Analysis

nn is the number of nodes in the tree.

  • Time Complexity : O(n)O(n).

    The recursive function visits each of the nn nodes in the tree exactly once, performing an O(1)O(1) recursive operation on each. This gives a total of O(n)O(n)

    There are n−1n−1 numbers in the list. Each of these is processed with an O(1)O(1) operation, giving a total of O(n)O(n) time for this phase too.

    In total, we have O(n)O(n).

  • Space Complexity O(n)O(n).

    There are two places that extra space is used.

    Firstly, the recursion is putting frames on the stack. The maximum number of frames at any one time is the maximum depth of the tree. For a balanced tree, this is around O(log⁡ n)O(logn), and in the worst case (a long skinny tree) it is O(n)O(n).

    Secondly, the list takes up space. It contains n−1n−1 numbers at the end, so it too is O(n)O(n).

    In both the average case and worst case, we have a total of O(n)O(n) space used by this approach.

Something you might have realised is that the subtree pair that leads to the largest product is the pair with the smallest difference between them. Interestingly, this fact doesn’t help us much with optimizing the algorithm. This is because subtree sums are notobtained in sorted order, and so any attempt to sort them (and thus find the nearest to middle directly) will cost at least O(n log⁡ n)O(nlogn) to do. With the overall algorithm, even with the linear search, only being O(n)O(n), this is strictly worse. The only situation this insight becomes useful is if you have to solve the problem using only 32-bit integers. The reason for this is discussed in Approach #3.


Approach 2: Two-Pass DFS

Intuition

Instead of putting the Subtree 1 sums into a separate list, we can do 2 separate tree summing traversals.

  1. Calculate the sum of the entire tree.
  2. Check the product we’d get for each subtree.

Calculating the total sum is done in the same way as before.

Finding the maximum product is similar, except requires a variable outside of the function to keep track of the maximum product seen so far.

1
2
3
4
5
6
7
8
9
10
11
12
13
def maximum_product(subroot, total):
best = 0
def recursive_helper(subroot):
nonlocal best
if subroot is None: return 0
left_sum = recursive_helper(subroot.left)
right_sum = recursive_helper(subroot.right)
total_sum = left_sum + right_sum + subroot.val
product = total_sum * (tree_total_sum - total_sum)
best = max(best, product)
return total_sum
recursive_helper(subroot)
return best

Algorithm

It is possible to combine the 2 recursive functions into a single one that is called twice, however the side effects of the functions (changing of class variables) hurt code readability and can be confusing. For this reason, the code below uses 2 separate functions.

Complexity Analysis

nn is the number of nodes in the tree.

  • Time Complexity : O(n)O(n).

    Each recursive function visits each of the nn nodes in the tree exactly once, performing an O(1)O(1) recursive operation on each. This gives a total of O(n)O(n).

  • Space Complexity O(n)O(n).

    The recursion is putting frames on the stack. The maximum number of frames at any one time is the maximum depth of the tree. For a balanced tree, this is around O(log⁡ n)O(logn), and in the worst case (a long skinny tree) it is O(n)O(n).

    Because we use worst case for complexity analysis, we say this algorithm uses O(n)O(n) space. However, it’s worth noting that as long as the tree is fairly balanced, the space usage will be a lot nearer to O(log⁡ n)O(logn).


Approach 3: Advanced Strategies for Dealing with 32-Bit Integers

Intuition

This is an advanced bonus section that discusses ways of solving the problem using only 32-bit integers. It’s not essential for an interview, although could be useful depending on your choice of programming language. Some of the ideas might also help with potential follow up questions. This section assumes prior experience with introductory modular arithmetic.

We’ll additionally assume that the 32-bit integer we’re working with is signed, so has a maximum value of 2,147,483,647.

What if your chosen programming language only supported 32-bit integers, and you had no access to a Big Integer library? Could we still solve this problem? What are the problems we’d need to address?

The solutions above relied on being able to multiply 2 numbers of up to 30 (signed) bits each without overflow. Because the number of bits in the product add, we would expect the product to require ~60 bits to represent. Using a 64-bit integer was therefore enough. Additionally, with a modulus of 1,000,000,007, the final product, after taken to the modulus, will always fit within a 32-bit integer.

However, we’re now assuming that we only have 32-bit integers. When working with 32-bit integers, we must always keep the total below 2,147,483,647, even during intermediate calculations. Therefore, we’ll need a way of doing the math within this restriction. One way to do the multiplication safely is to write an algorithm using the same underlying idea as modular exponentiation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int modularMultiplication(int a, int b, int m) {
int product = 0;
int currentSum = a;
while (b > 0) {
int bit = b % 2;
b >>= 1;
if (bit == 1) {
product += currentSum;
product %= m;
}
currentSum <<= 1;
currentSum %= m;
}
return product;
}

There is one possible pitfall with this though. We are supposed to return the calculation for the largest product, determined before the modulus is taken.

For example, consider the following 2 products:

  1. 34,000 * 30,000 = 1,020,000,000 becomes 1,020,000,000 % 1,000,000,007 = 19,999,993.
  2. 30,000 * 30,000 = 900,000,000 doesn’t change because 900,000,000 % 1,000,000,007 = 900,000,000.

So if we were to compare them before taking the modulus, product 1 would be larger, which is correct. But if we compared them after, then product 2 is larger, which is incorrect.

Therefore, we need a way to determine which product will be the biggest, without actually calculating them. Then once we know which is biggest, we can use our method for calculating a product modulo 1,000,000,007 without going over 2,147,483,647.

The trick is to realise that Sum(Subtree 1) + Sum(Subtree 2) is constant, but Sum(Subtree 1) * Sum(Subtree 2) increases as Sum(Subtree 1) - Sum(Subtree 2) gets nearer to 0, i.e. as the sum of the subtrees is more balanced. A good way of visualising this is to imagine you have X meters of fence and need to make a rectangular enclosure for some sheep. You want to maximise the area. It turns out that the optimal solution is to make a square. The nearer to a square the enclosure is, the nearer to the optimal area it will be. For example, where H + W = 11, the best (integer) solution is 5 x 6 = 30.

Different ways of building a fence.

A simple way to do this in code is to loop over the list of all the sums and find the (sum, total - sum) pair that has the minimal difference. This approach ensures we do not need to use floating point numbers.

Algorithm

We’ll use Approach #1 as a basis for the code, as it’s simpler and easier to understand. The same ideas can be used in Approach #2 though.

Complexity Analysis

nn is the number of nodes in the tree.

  • Time Complexity : O(n)O(n).

    Same as above approaches.

  • Space Complexity : O(n)O(n).

    Same as above approaches.

The modularMultiplication function has a time complexity of O(log⁡ b)O(logb) because the loop removes one bit from bb each iteration, until there are none left. This doesn’t bring up the total time complexity to O(n log⁡ b)O(nlogb) though, because bb has a fixed upper limit of 32, and is therefore treated as a constant.

72. 编辑距离

难度困难804收藏分享切换为英文关注反馈

给你两个单词 word1word2*,请你计算出将 *word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

示例 1:

1
2
3
4
5
6
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

1
2
3
4
5
6
7
8
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
动态规划

定义 dp[i][j]
21. dp[i][j] 代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数
22. 需要考虑 word1 或 word2 一个字母都没有,即全增加/删除的情况,所以预留 dp[0][j] 和 dp[i][0]

状态转移
31. 增,dp[i][j] = dp[i][j - 1] + 1
32. 删,dp[i][j] = dp[i - 1][j] + 1
33. 改,dp[i][j] = dp[i - 1][j - 1] + 1
34. 按顺序计算,当计算 dp[i][j] 时,dp[i - 1][j] , dp[i][j - 1] , dp[i - 1][j - 1] 均已经确定了
35. 配合增删改这三种操作,需要对应的 dp 把操作次数加一,取三种的最小
36. 如果刚好这两个字母相同 word1[i - 1] = word2[j - 1] ,那么可以直接参考 dp[i - 1][j - 1] ,操作不用加一



作者:ikaruga
链接:https://leetcode-cn.com/problems/edit-distance/solution/edit-distance-by-ikaruga/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//dp solution
class Solution {
public int minDistance(String word1, String word2) {
int row=word1.length();
int col=word2.length();
if((row==0) || (col==0))
return row+col;
int[][] dp=new int[row+1][col+1];
for(int i=0;i<row+1;i++)
dp[i][0]=i;
for(int j=0;j<col+1;j++)
dp[0][j]=j;
for(int i=1;i<row+1;i++){
for(int j=1;j<col+1;j++){
if(word1.charAt(i-1)!=word2.charAt(j-1))
dp[i-1][j-1]+=1;
dp[i][j]=Math.min(dp[i-1][j]+1,Math.min(dp[i][j-1]+1,dp[i-1][j-1]));
}
}
return dp[row][col];
}
}

132. 分割回文串 II

难度困难130收藏分享切换为英文关注反馈

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的最少分割次数。

示例:

1
2
3
输入: "aab"
输出: 1
解释: 进行一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int minCut(String s) {
char[] c = s.toCharArray();
int n = c.length;
int[] cut = new int[n];
boolean[][] dp = new boolean[n][n];

for(int i = 0; i < n; i++) {
int min = i;
for(int j = 0; j <= i; j++) {
if(c[j] == c[i] && (j + 1 > i - 1 || dp[j + 1][i - 1])) {
dp[j][i] = true;
min = j == 0 ? 0 : Math.min(min, cut[j - 1] + 1);
}
}
cut[i] = min;
}
return cut[n - 1];
}
}

5. 最长回文子串

难度中等2038收藏分享切换为英文关注反馈

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

1
2
3
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

1
2
输入: "cbbd"
输出: "bb"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
int strLen = s.length();
int maxStart = 0; //最长回文串的起点
int maxEnd = 0; //最长回文串的终点
int maxLen = 1; //最长回文串的长度

boolean[][] dp = new boolean[strLen][strLen];

for (int r = 1; r < strLen; r++) {
for (int l = 0; l < r; l++) {
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
dp[l][r] = true;
if (r - l + 1 > maxLen) {
maxLen = r - l + 1;
maxStart = l;
maxEnd = r;

}
}

}

}
return s.substring(maxStart, maxEnd + 1);

}
}

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

1
2
3
4
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

1
2
3
4
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int rob(int[] nums) {
int premax=0;
int curmax=0;
for(int x:nums){
int tmp=curmax;
curmax=Math.max(x+premax,curmax);
premax=tmp;
}
return curmax;
}
}

Algorithm

It could be overwhelming thinking of all possibilities on which houses to rob.

A natural way to approach this problem is to work on the simplest case first.

Let us denote that:

f(k) = Largest amount that you can rob from the first k houses.
Ai = Amount of money at the ith house.

Let us look at the case n = 1, clearly f(1) = A1.

Now, let us look at n = 2, which f(2) = max(A1, A2).

For n = 3, you have basically the following two options:

  1. Rob the third house, and add its amount to the first house’s amount.
  2. Do not rob the third house, and stick with the maximum amount of the first two houses.

Clearly, you would want to choose the larger of the two options at each step.

Therefore, we could summarize the formula as following:

f(k) = max(f(k – 2) + Ak, f(k – 1))

We choose the base case as f(–1) = f(0) = 0, which will greatly simplify our code as you can see.

The answer will be calculated as f(n). We could use an array to store and calculate the result, but since at each step you only need the previous two maximum values, two variables are suffice.

1
2
3
4
5
6
7
8
9
10
public int rob(int[] num) {
int prevMax = 0;
int currMax = 0;
for (int x : num) {
int temp = currMax;
currMax = Math.max(prevMax + x, currMax);
prevMax = temp;
}
return currMax;
}

Complexity analysis

  • Time complexity : O(n)O(n). Assume that nn is the number of houses, the time complexity is O(n)O(n).
  • Space complexity : O(1)O(1).

213. 打家劫舍 II

难度中等254收藏分享切换为英文关注反馈

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

1
2
3
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

1
2
3
4
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public int rob(int[] nums) {
int l=nums.length;
if(l<=0)return 0;
if(l==1)return nums[0];
if(l==2)return Math.max(nums[0],nums[1]);
return Math.max(rob0(nums),rob1(nums));

}
public int rob0(int[] nums) {//0~length-2
int premax=0;
int curmax=0;
int l=nums.length;
int[] arr1=new int[l-1];
System.arraycopy(nums,0,arr1,0,l-1);//copy array
for(int x:arr1){
int tmp=curmax;
curmax=Math.max(x+premax,curmax);
premax=tmp;
}
return curmax;
}
public int rob1(int[] nums) {//0~length-1
int premax=0;
int curmax=0;
int l=nums.length;
int[] arr2=new int[l-1];
System.arraycopy(nums,1,arr2,0,l-1);
for(int x:arr2){
int tmp=curmax;
curmax=Math.max(x+premax,curmax);
premax=tmp;
}
return curmax;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(rob(nums, 0, nums.length - 2), rob(nums, 1, nums.length - 1));
}
private int rob(int[] num, int lo, int hi) {
int include = 0, exclude = 0;
for (int j = lo; j <= hi; j++) {
int i = include, e = exclude;
include = e + num[j];
exclude = Math.max(e, i);
}
return Math.max(include, exclude);
}
}

Since this question is a follow-up to House Robber, we can assume we already have a way to solve the simpler question, i.e. given a 1 row of house, we know how to rob them. So we already have such a helper function. We modify it a bit to rob a given range of houses.

1
2
3
4
5
6
7
8
9
private int rob(int[] num, int lo, int hi) {
int include = 0, exclude = 0;
for (int j = lo; j <= hi; j++) {
int i = include, e = exclude;
include = e + num[j];
exclude = Math.max(e, i);
}
return Math.max(include, exclude);
}

Now the question is how to rob a circular row of houses. It is a bit complicated to solve like the simpler question. It is because in the simpler question whether to rob num[lo] is entirely our choice. But, it is now constrained by whether num[hi] is robbed.

However, since we already have a nice solution to the simpler problem. We do not want to throw it away. Then, it becomes how can we reduce this problem to the simpler one. Actually, extending from the logic that if house i is not robbed, then you are free to choose whether to rob house i + 1, you can break the circle by assuming a house is not robbed.

For example, 1 -> 2 -> 3 -> 1 becomes 2 -> 3 if 1 is not robbed.

Since every house is either robbed or not robbed and at least half of the houses are not robbed, the solution is simply the larger of two cases with consecutive houses, i.e. house i not robbed, break the circle, solve it, or house i + 1 not robbed. Hence, the following solution. I chose i = n and i + 1 = 0 for simpler coding. But, you can choose whichever two consecutive ones.

1
2
3
4
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(rob(nums, 0, nums.length - 2), rob(nums, 1, nums.length - 1));
}

221. 最大正方形

难度中等403收藏分享切换为英文关注反馈

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

示例:

1
2
3
4
5
6
7
8
输入: 

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

输出: 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int maximalSquare(char[][] matrix) {
int row=matrix.length, col=row>0? matrix[0].length:0;
int[][] dp=new int[row+1][col+1];
int max=0;
for(int i=1;i<row+1;i++){
for(int j=1;j<col+1;j++){
if(matrix[i-1][j-1]=='1'){
dp[i][j]=Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1;
max=Math.max(max,dp[i][j]);
}
}
}
return max*max;
}
}

Approach #1 Brute Force [Accepted]

The simplest approach consists of trying to find out every possible square of 1’s that can be formed from within the matrix. The question now is – how to go for it?

We use a variable to contain the size of the largest square found so far and another variable to store the size of the current, both initialized to 0. Starting from the left uppermost point in the matrix, we search for a 1. No operation needs to be done for a 0. Whenever a 1 is found, we try to find out the largest square that can be formed including that 1. For this, we move diagonally (right and downwards), i.e. we increment the row index and column index temporarily and then check whether all the elements of that row and column are 1 or not. If all the elements happen to be 1, we move diagonally further as previously. If even one element turns out to be 0, we stop this diagonal movement and update the size of the largest square. Now we, continue the traversal of the matrix from the element next to the initial 1 found, till all the elements of the matrix have been traversed.

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Solution {
public int maximalSquare(char[][] matrix) {
int rows = matrix.length, cols = rows > 0 ? matrix[0].length : 0;
int maxsqlen = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (matrix[i][j] == '1') {
int sqlen = 1;
boolean flag = true;
while (sqlen + i < rows && sqlen + j < cols && flag) {
for (int k = j; k <= sqlen + j; k++) {
if (matrix[i + sqlen][k] == '0') {
flag = false;
break;
}
}
for (int k = i; k <= sqlen + i; k++) {
if (matrix[k][j + sqlen] == '0') {
flag = false;
break;
}
}
if (flag)
sqlen++;
}
if (maxsqlen < sqlen) {
maxsqlen = sqlen;
}
}
}
}
return maxsqlen * maxsqlen;
}
}

Complexity Analysis

  • Time complexity : O((mn))O((m**n)2). In worst case, we need to traverse the complete matrix for every 1.
  • Space complexity : O(1)O(1). No extra space is used.

Approach #2 (Dynamic Programming) [Accepted]

Algorithm

We will explain this approach with the help of an example.

1
2
3
4
5
0 1 1 1 0
1 1 1 1 1
0 1 1 1 1
0 1 1 1 1
0 0 1 1 1

We initialize another matrix (dp) with the same dimensions as the original one initialized with all 0’s.

dp(i,j) represents the side length of the maximum square whose bottom right corner is the cell with index (i,j) in the original matrix.

Starting from index (0,0), for every 1 found in the original matrix, we update the value of the current element as

dp(i,j)=min⁡(dp(i−1,j),dp(i−1,j−1),dp(i,j−1))+1.dp(i,j)=min(dp(i−1,j),dp(i−1,j−1),dp(i,j−1))+1.

We also remember the size of the largest square found so far. In this way, we traverse the original matrix once and find out the required maximum size. This gives the side length of the square (say maxsqlenmaxsqle**n). The required result is the area maxsqlen2m*axsqlen*2.

To understand how this solution works, see the figure below.

Max Square

An entry 2 at (1,3)(1,3) implies that we have a square of side 2 up to that index in the original matrix. Similarly, a 2 at (1,2)(1,2) and (2,2)(2,2)implies that a square of side 2 exists up to that index in the original matrix. Now to make a square of side 3, only a single entry of 1 is pending at (2,3)(2,3). So, we enter a 3 corresponding to that position in the dp array.

Now consider the case for the index (3,4)(3,4). Here, the entries at index (3,3)(3,3) and (2,3)(2,3) imply that a square of side 3 is possible up to their indices. But, the entry 1 at index (2,4)(2,4) indicates that a square of side 1 only can be formed up to its index. Therefore, while making an entry at the index (3,4)(3,4), this element obstructs the formation of a square having a side larger than 2. Thus, the maximum sized square that can be formed up to this index is of size 2×22×2.

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
public int maximalSquare(char[][] matrix) {
int rows = matrix.length, cols = rows > 0 ? matrix[0].length : 0;
int[][] dp = new int[rows + 1][cols + 1];
int maxsqlen = 0;
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {
if (matrix[i-1][j-1] == '1'){
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
maxsqlen = Math.max(maxsqlen, dp[i][j]);
}
}
}
return maxsqlen * maxsqlen;
}
}

Complexity Analysis

  • Time complexity : O(mn)O(m**n). Single pass.
  • Space complexity : O(mn)O(m**n). Another matrix of same size is used for dp.

Approach #3 (Better Dynamic Programming) [Accepted]

Algorithm

In the previous approach for calculating dp of ithith row we are using only the previous element and the (i−1)(i−1)t**h row. Therefore, we don’t need 2D dp matrix as 1D dp array will be sufficient for this.

Initially the dp array contains all 0’s. As we scan the elements of the original matrix across a row, we keep on updating the dp array as per the equation dp[j]=min(dp[j−1],dp[j],prev)d**p[j]=min(d**p[j−1],d**p[j],pre**v), where prev refers to the old dp[j−1]d**p[j−1]. For every row, we repeat the same process and update in the same dp array.

 Max Square

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public int maximalSquare(char[][] matrix) {
int rows = matrix.length, cols = rows > 0 ? matrix[0].length : 0;
int[] dp = new int[cols + 1];
int maxsqlen = 0, prev = 0;
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {
int temp = dp[j];
if (matrix[i - 1][j - 1] == '1') {
dp[j] = Math.min(Math.min(dp[j - 1], prev), dp[j]) + 1;
maxsqlen = Math.max(maxsqlen, dp[j]);
} else {
dp[j] = 0;
}
prev = temp;
}
}
return maxsqlen * maxsqlen;
}
}

Complexity Analysis

  • Time complexity : O(mn)O(m**n). Single pass.
  • Space complexity : O(n)O(n). Another array which stores elements in a row is used for dp.

309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

1
2
3
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int maxProfit(int[] prices) {
int sold=Integer.MIN_VALUE, held=Integer.MIN_VALUE, reset=0;
for(int price: prices){
int presold=sold;
sold=held+price;
held=Math.max(held,reset-price);
reset=Math.max(reset, presold);
}
return Math.max(sold, reset);
}
}

Approach 1: Dynamic Programming with State Machine

Intuition

First of all, let us take a different perspective to look at the problem, unlike the other algorithmic problems.

Here, we will treat the problem as a game, and the trader as an agent in the game. The agent can take actions that lead to gain or lose of game points (i.e. profits). And the goal of the game for the agent is to gain the maximal points.

In addition, we will introduce a tool called state machine, which is a mathematical model of computation. Later one will see how the state machine coupled with the dynamic programming technique can help us solve the problem easily.

In the following sections, we will first define a state machine that is used to model the behaviors and states of the game agent.

Then, we will demonstrate how to apply the state machine to solve the problem.

Definition

Let us define a state machine\ to model our agent. The state machine consists of three states, which we define as follows:

  • state held: in this state, the agent holds a stock that it bought at some point before.
  • state sold: in this state, the agent has just sold a stock right before entering this state. And the agent holds no stock at hand.
  • state reset: first of all, one can consider this state as the starting point, where the agent holds no stock and did not sell a stock before. More importantly, it is also the transient state before the held and sold. Due to the cooldown\ rule, after the sold state, the agent can not immediately acquire any stock, but is forced into the reset state. One can consider this state as a “reset” button for the cycles of buy and sell transactions.

At any moment, the agent can only be in one\ state. The agent would transition to another state by performing some actions, namely:

  • action sell: the agent sells a stock at the current moment. After this action, the agent would transition to the sold state.
  • action buy: the agent acquires a stock at the current moment. After this action, the agent would transition to the heldstate.
  • action rest: this is the action that the agent does no transaction, neither buy or sell. For instance, while holding a stock at the held state, the agent might simply do nothing, and at the next moment the agent would remain in the held state.

Now, we can assemble the above states and actions into a state machine, which we show in the following graph where each node represents a state, and each edge represents a transition between two states. On top of each edge, we indicate the action that triggers the transition.

state machine

Notice that, in all states except the sold state, by doing nothing, we would remain in the same state, which is why there is a self-looped transition on these states.

Deduction

Now, one might wonder how exactly the state machine that we defined can help to solve the problem.

As we mentioned before, we model the problem as a game\, and the trader as an agent\ in the game. And this is where our state machine comes into the picture. The behaviors and the states of the game agent can be modeled by our state machine.

mario game

Given a list stock prices (i.e. price[0...n]), our agent would walk through each price point one by one. At each point, the agent would be in one of three states (i.e. held, sold and reset) that we defined before. And at each point, the agent would take one of the three actions (i.e. buy, sell and rest), which then would lead to the next state at the next price point.

Now if we chain up each state at each price point, it would form a graph\ where each path\ that starts from the initial price point and ends at the last price point represents a combination of transactions that the agent could perform through out the game.

graph of state transition

The above graph shows all possible paths that our game agent agent walks through the list, which corresponds to all possible combinations of transactions that the trader can perform with the given price sequence.

In order to solve the problem, the goal is to find such a path in the above graph that maximizes the profits.

In each node of graph, we also indicate the maximal profits that the agent has gained so far in each state of each step. And we highlight the path that generates the maximal profits. Don’t worry about them for the moment. We will explain in detail how to calculate in the next section.

Algorithm

In order to implement the above state machine, we could define three arrays (i.e. held[i], sold[i] and reset[i]) which correspond to the three states that we defined before.

Each element in each array represents the maximal profits that we could gain at the specific price point i with the specific state. For instance, the element sold[2] represents the maximal profits we gain if we sell the stock at the price point price[2].

According to the state machine we defined before, we can then deduce the formulas to calculate the values for the state arrays, as follows:

sold[i]=hold[i−1]+price[i]held[i]=max⁡(held[i−1],reset[i−1]−price[i])reset[i]=max⁡(reset[i−1],sold[i−1])sold[i]=hold[i−1]+price[i]held[i]=max(held[i−1],reset[i−1]−price[i])reset[i]=max(reset[i−1],sold[i−1])

Here is how we interpret each formulas:

  • sold[i]sold[i]: the previous state of sold can only be held. Therefore, the maximal profits of this state is the maximal profits of the previous state plus the revenue by selling the stock at the current price.
  • held[i]held[i]: the previous state of held could also be held, i.e. one does no transaction. Or its previous state could be reset, from which state, one can acquire a stock at the current price point.
  • reset[i]reset[i]: the previous state of reset could either be reset or sold. Both transitions do not involve any transaction with the stock.

Finally, the maximal profits that we can gain from this game would be max⁡(sold[n],reset[n])max(sold[n],reset[n]), i.e. at the last price point, either we sell the stock or we simply do no transaction, to have the maximal profits. It makes no sense to acquire the stock at the last price point, which only leads to the reduction of profits.

In particular, as a base case, the game should be kicked off from the state reset, since initially we don’t hold any stock and we don’t have any stock to sell neither. Therefore, we assign the initial values of sold[-1] and held[-1] to be Integer.MIN_VALUE, which are intended to render the paths that start from these two states impossible.

As one might notice in the above formulas, in order to calculate the value for each array, we reuse the intermediate values, and this is where the paradigm of dynamic programming comes into play.

More specifically, we only need the intermediate values at exactly one step before the current step. As a result, rather than keeping all the values in the three arrays, we could use a sliding window\ of size 1 to calculate the value for max⁡(sold[n],reset[n])max(sold[n],reset[n]).

In the following animation, we demonstrate the process on how the three arrays are calculated step by step.

Current

1 / 7

As a byproduct\ of this algorithm, not only would we obtain the maximal profits at the end, but also we could recover each action that we should perform along the path, although this is not required by the problem.

In the above graph, by starting from the final state, and walking backward following the path, we could obtain a sequence of actions that leads to the maximal profits at the end, i.e. [buy, sell, cooldown, buy, sell].

Complexity Analysis

  • Time Complexity: O(N)O(N) where NN is the length of the input price list.

    • We have one loop over the input list, and the operation within one iteration takes constant time.
  • Space Complexity: O(1)O(1), constant memory is used regardless the size of the input.


Approach 2: Yet-Another Dynamic Programming

Intuition

Most of the times, there are more than one approaches to decompose the problem, so that we could apply the technique of dynamic programming.

Here we would like to propose a different perspective on how to model the problem purely with mathematical formulas.

Again, this would be a journey loaded with mathematical notations, which might be complicated, but it showcases how the mathematics could help one with the dynamic programming (pun intended).

Definition

For a sequence of prices, denoted as price[0,1,…,n]price[0,1,…,n], let us first define our target function called MP(i)MP(i). The function MP(i)MP(i)gives the maximal profits that we can gain for the price subsequence starting from the index ii, i.e. price[i,i+1,…,n]price[i,i+1,…,n].

Given the definition of the MP(i)MP(i) function, one can see that when i=0i=0 the output of the function, i.e. MP(0)MP(0), is exactly the result that we need to solve the problem, which is the maximal profits that one can gain for the price subsequence of price[0,1,…,n]price[0,1,…,n].

Suppose that we know all the values for MP(i)MP(i) onwards until MP(n)MP(n), i.e. we know the maximal profits that we can gain for any subsequence of price[k…n]k∈[i,n]price[kn]k∈[i,n].

Now, let us add a new price point price[i−1]price[i−1] into the subsequence price[i…n]price[in], all we need to do is to deduce the value for the unknown MP(i−1)MP(i−1).

Up to this point, we have just modeled the problem with our target function MP(i)MP(i), along with a series of definitions. The problem now is boiled down to deducing the formula for MP(i−1)MP(i−1).

In the following section, we will demonstrate how to deduce the formula for MP(i−1)MP(i−1).

Deduction

With the newly-added price point price[i−1]price[i−1], we need to consider all possible transactions that we can do to the stock at this price point, which can be broken down into two cases:

  • Case 1): we buy this stock with price[i−1]price[i−1] and then sell it at some point in the following price sequence of price[i…n]price[in]. Note that, once we sell the stock at a certain point, we need to cool down for a day, then we can reengage with further transactions. Suppose that we sell the stock right after we bought it, at the next price point price[i]price[i], the maximal profits we would gain from this choice would be the profit of this transaction (i.e. price[i]−price[i−1]price[i]−price[i−1]) plus the maximal profits from the rest of the price sequence, as we show in the following:

example of profit calculation

In addition, we need to enumerate all possible points to sell this stock, and take the maximum among them. The maximal profits that we could gain from this case can be represented by the following:

C1=max⁡{k∈[i,n]}(price[k]−p[i−1]+MP(k+2))C1=max{k∈[i,n]}(price[k]−p[i−1]+MP(k+2))

  • Case 2): we simply do nothing with this stock. Then the maximal profits that we can gain from this case would be MP(i)MP(i), which are also the maximal profits that we can gain from the rest of the price sequence.

C2=MP(i)C2=MP(i)

By combining the above two cases, i.e. selecting the max value among them, we can obtain the value for MP(i−1)MP(i−1), as follows:

MP(i−1)=max⁡(C1,C2)MP(i−1)=max(C1,C2)

MP(i−1)=max⁡(max⁡{k∈[i,n]}(price[k]−price[i−1]+MP(k+2)),MP(i))MP(i−1)=max(max{k∈[i,n]}(price[k]−price[i−1]+MP(k+2)),MP(i))

By the way, the base case for our recursive function MP(i)MP(i) would be MP(n)MP(n) which is the maximal profits that we can gain from the sequence with a single price point price[n]price[n]. And the best thing we should do with a single price point is to do no transaction, hence we would neither lose money nor gain any profit, i.e. MP(n)=0MP(n)=0.

The above formulas do model the problem soundly. In addition, one should be able to translate them directly into code.

Algorithm

With the final formula we derived for our target function MP(i)MP(i), we can now go ahead and translate it into any programming language.

  • Since the formula deals with subsequences of price that start from the last price point, we then could do an iteration over the price list in the reversed order.
  • We define an array MP[i] to hold the values for our target function MP(i)MP(i). We initialize the array with zeros, which correspond to the base case where the minimal profits that we can gain is zero. Note that, here we did a trick to pad the array with two additional elements, which is intended to simplify the branching conditions, as one will see later.
  • To calculate the value for each element MP[i], we need to look into two cases as we discussed in the previous section, namely:
    • Case 1). we buy the stock at the price point price[i], then we sell it at a later point. As one might notice, the initial padding on the MP[i] array saves us from getting out of boundary in the array.
    • Case 2). we do no transaction with the stock at the price point price[i].
  • At the end of each iteration, we then pick the largest value from the above two cases as the final value for MP[i].
  • At the end of the loop, the MP[i] array will be populated. We then return the value of MP[0], which is the desired solution for the problem.

Complexity Analysis

  • Time Complexity: O(N2)O(N2) where NN is the length of the price list.
    • As one can see, we have nested loops over the price list. The number of iterations in the outer loop is NN. The number of iterations in the inner loop varies from 11 to NN. Therefore, the total number of iterations that we perform is ∑i=1Ni=N⋅(N+1)2∑i=1N**i=2N⋅(N+1).
    • As a result, the overall time complexity of the algorithm is O(N2)O(N2).
  • Space Complexity: O(N)O(N) where NN is the length of the price list.
    • We allocated an array to hold all the values for our target function MP(i)MP(i).

337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例 1:

1
2
3
4
5
6
7
8
9
10
输入: [3,2,3,null,3,null,1]

3
/ \
2 3
\ \
3 1

输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

示例 2:

1
2
3
4
5
6
7
8
9
10
输入: [3,4,5,1,3,null,1]

3
/ \
4 5
/ \ \
1 3 1

输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] result=helper(root);
return Math.max(result[0],result[1]);
}
public int[] helper(TreeNode root){
if(root==null)return new int[2];
int[] result=new int[2];//0 is not steal, 1 is steal
int[] left=helper(root.left);
int[] right=helper(root.right);
result[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
result[1]=left[0]+right[0]+root.val;
return result;
}
}

说明

本题目本身就是动态规划的树形版本,通过此题解,可以了解一下树形问题在动态规划问题解法
我们通过三个方法不断递进解决问题

解法一通过递归实现,虽然解决了问题,但是复杂度太高
解法二通过解决方法一中的重复子问题,实现了性能的百倍提升
解法三直接省去了重复子问题,性能又提升了一步
解法一、暴力递归 - 最优子结构

在解法一和解法二中,我们使用爷爷、两个孩子、4 个孙子来说明问题
首先来定义这个问题的状态
爷爷节点获取到最大的偷取的钱数呢

首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷
二叉树只有左右两个孩子,一个爷爷最多 2 个儿子,4 个孙子
根据以上条件,我们可以得出单个节点的钱该怎么算
4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构

由于是二叉树,这里可以选择计算所有子节点

4 个孙子投的钱加上爷爷的钱如下
int method1 = root.val + rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right)
两个儿子偷的钱如下
int method2 = rob(root.left) + rob(root.right);
挑选一个钱数多的方案则
int result = Math.max(method1, method2);
将上述方案写成代码如下

Java
public int rob(TreeNode root) {
if (root == null) return 0;

int money = root.val;
if (root.left != null) {
    money += (rob(root.left.left) + rob(root.left.right));
}

if (root.right != null) {
    money += (rob(root.right.left) + rob(root.right.right));
}

return Math.max(money, rob(root.left) + rob(root.right));

}
信心满满的提交,一次通过,然而 执行用时:837 ms,在所有 java 提交中击败了24.49%的用户 这个结果太没面子了,下个解法进行优化

解法二、记忆化 - 解决重复子问题

针对解法一种速度太慢的问题,经过分析其实现,我们发现爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点。

于是乎我们发现了一个动态规划的关键优化点

重复子问题

我们这一步针对重复子问题进行优化,我们在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。
由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value

解法一加上记忆化优化后代码如下:

Java
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> memo = new HashMap<>();
return robInternal(root, memo);
}

public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
if (root == null) return 0;
if (memo.containsKey(root)) return memo.get(root);
int money = root.val;

if (root.left != null) {
    money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
}
if (root.right != null) {
    money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
}
int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
memo.put(root, result);
return result;

}
提交代码,执行用时:4 ms, 在所有 java 提交中击败了 54.92% 的用户,速度提高了 200 倍。太开心了。别着急,还有一个终极方案呢,连记忆化消耗的时间都省了,能省则省么。

解法三、终极解法

上面两种解法用到了孙子节点,计算爷爷节点能偷的钱还要同时去计算孙子节点投的钱,虽然有了记忆化,但是还是有性能损耗。

我们换一种办法来定义此问题

每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷

当前节点选择偷时,那么两个孩子节点就不能选择偷了
当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)
我们使用一个大小为 2 的数组来表示 int[] res = new int[2] 0 代表不偷,1 代表偷
任何一个节点能偷到的最大钱的状态可以定义为

当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
表示为公式如下

root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) + Math.max(rob(root.right)[0], rob(root.right)[1])
root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
将公式做个变换就是代码啦

Java
public int rob(TreeNode root) {
int[] result = robInternal(root);
return Math.max(result[0], result[1]);
}

public int[] robInternal(TreeNode root) {
if (root == null) return new int[2];
int[] result = new int[2];

int[] left = robInternal(root.left);
int[] right = robInternal(root.right);

result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
result[1] = left[0] + right[0] + root.val;

return result;

}
再提交一次:
执行用时 1 ms, 在所有 java 提交中击败了 99.87% 的用户,这样的结果,我觉得可以了。

413. 等差数列划分

难度中等122收藏分享切换为英文关注反馈

如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,以下数列为等差数列:

1
2
3
1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下数列不是等差数列。

1
1, 1, 2, 5, 7

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。

如果满足以下条件,则称子数组(P, Q)为等差数组:

元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。

函数要返回数组 A 中所有为等差数组的子数组个数。

示例:

1
2
3
A = [1, 2, 3, 4]

返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int numberOfArithmeticSlices(int[] A) {
int result=0;
int l=A.length;
int[] dp=new int[l];
for(int i=2;i<l;i++){
if(A[i]-A[i-1]==A[i-1]-A[i-2]){
dp[i]=1+dp[i-1];
result+=dp[i];
}
}
return result;
}
}

Approach #1 Brute Force [Accepted]

The most naive solution is to consider every pair of elements(with atleast 1 element between them), so that the range of elements lying between these two elements acts as a slice. Then, we can iterate over every such slice(range) to check if all the consecutive elements within this range have the same difference. For every such range found, we can increment the countcount that is used to keep a track of the required result.

Complexity Analysis

  • Time complexity : O(n3)O(n3). We iterate over the range formed by every pair of elements. Here, nn refers to the number of elements in the given array AA.
  • Space complexity : O(1)O(1). Constant extra space is used.

Approach #2 Better Brute Force [Accepted]

Algorithm

In the last approach, we considered every possible range and then iterated over the range to check if the difference between every consercutive element in this range is the same. We can optimize this approach to some extent, by making a small observation.

We can see, that if we are currently considering the range bound by the elements, let’s say, A[s]As and A[e]Ae, we have checked the consecutive elements in this range to have the same difference. Now, when we move on to the next range between the indices ss and e+1e+1, we again perform a check on all the elements in the range s:es:e, along with one additional pair A[e+1]A[e+1]and A[e]A[e]. We can remove this redundant check in the range s:es:e and just check the last pair to have the same difference as the one used for the previous range(same ss, incremented ee).

Note that if the last range didn’t constitute an arithmetic slice, the same elements will be a part of the updated range as well. Thus, we can omit the rest of the ranges consisting of the same starting index. The rest of the process remains the same as in the last approach.

Complexity Analysis

  • Time complexity : O(n2)O(n2). Two for loops are used.
  • Space complexity : O(1)O(1). Constant extra space is used.

Approach #3 Using Recursion [Accepted]

Algorithm

By making use of the observation discussed in the last approach, we know, that if a range of elements between the indices (i,j)(i,j)constitute an Arithmetic Slice, and another element A[j+1]A[j+1] is included such that A[j+1]A[j+1] and A[j]A[j] have the same difference as that of the previous common difference, the ranges between (i,j+1)(i,j+1) will constitutes an arithmetic slice. Further, if the original range (i,j)(i,j) doesn’t form an arithmetic slice, adding new elements to this range won’t do us any good. Thus, no more arithmetic slices can be obtained by adding new elements to it.

By making use of this observation, we can develop a recursive solution for the given problem as well. Assume that a sumsum variable is used to store the total number of arithmetic slices in the given array AA. We make use of a recursive function slices(A,i)which returns the number of Arithmetic Slices in the range (k,i)(k,i), but which are not a part of any range (k,j)(k,j) such that j<ij<i. It also updates sumsum with the number of arithmetic slices(total) in the current range. Thus, kk refers to the minimum index such that the range (k,i)(k,i) constitutes a valid arithmetic slice.

Now, suppose we know the number of arithmetic slices in the range (0,i−1)(0,i−1) constituted by the elements [a0,a1,a2,…a(i−1)][a0,a1,a2,…a(i−1)], to be say xx. If this range itself is an arithmetic slice, all the consecutive elements have the same difference(equal to say, a(i−1)−a(i−2)a(i−1)−a(i−2)). Now, adding a new element aia**i to it to extend the range to (0,i)(0,i) will constitute an arithmetic slice only if this new element satisfies ai−a(i−1)=a(i−1)−a(i−2)a*i−*a(i−1)=a(i−1)−a(i−2). Thus, now, the addition of this new element, will lead to an addition of apa**p number of arithmetic slices to the ones obtained in the range (0,i−1)(0,i−1). The new arithmetic slices will be the ones constituting the ranges (0,i),(1,i),…(i−2,i)(0,i),(1,i),…(i−2,i), which are a total of x+1x+1 additional arithmetic slices. This is because, apart from the range (0,i)(0,i) the rest of the ranges (1,i),(2,i),…(i−2,i)(1,i),(2,i),…(i−2,i) can be mapped to (0,i−1),(1,i−1),…(i−3,i−1)(0,i−1),(1,i−1),…(i−3,i−1), with count equal to xx.

Thus, in every call to slices, if the ithith element has the same common difference with the last element as the previous common difference, we can find the number of new arithmetic slices added by the use of this element, apa**p and also update the sumsum to include this apa**p into it, apart from the count obtained by the smaller ranges. But, if the new element doesn’t have the same common difference, extra arithmetic slices can’t be contributed by it and hence, no addition is done to sumsum for the current element. But, of course sumsum will be updated as per the count obtained from the smaller ranges.

Complexity Analysis

  • Time complexity : O(n)O(n). The recursive function is called at most n−2n−2 times.
  • Space complexity : O(n)O(n). The depth of the recursion tree goes upto n−2n−2.

Approach #5 Dynamic Programming [Accepted]:

Algorithm

In the last approach, we start with the full range (0,n−1)(0,n−1), where nn is the number of elements in the given AA array. We can observe that the result for the range (0,i)(0,i) only depends on the elements in the range (0,i)(0,i) and not on any element beyond this range. Thus, we can make use of Dynamic Programming to solve the given problem.

We can make use of a 1-D dpd**p with number of elements equal to nn. dp[i]d**p[i] is used to store the number of arithmetic slices possible in the range (k,i)(k,i) and not in any range (k,j)(k,j) such that j<ij<i. Again, kk refers to the minimum index possible such that (k,j)(k,j)constitutes a valid Arithmetic Slice.

Instead of going in the reverse order as in the recursive approach, we can start filling the dpd**p in a forward manner. The intuition remains the same as in the last approach. For the ithith element being considered, we check if this element satsfies the common difference criteria with the previous element. If so, we know the number of new arithmetic slices added will be 1+dp[i−1]1+d**p[i−1] as discussed in the last approach. The sumsum is also updated by the same count to reflect the new arithmetic slices added.

The following animation illustrates the dpd**p filling process.

Current

1 / 9

Complexity Analysis

  • Time complexity : O(n)O(n). We traverse over the given AA array with nn elements once only.
  • Space complexity : O(n)O(n). 1-D dpd**p of size nn is used.

Approach #5 Constant Space Dynamic Programming [Accepted]:

Algorithm

In the last approach, we can observe that we only require the element dp[i−1]d**p[i−1] to determine the value to be entered at dp[i]d**p[i]. Thus, instead of making use of a 1-D array to store the required data, we can simply keep a track of just the last element.

Complexity Analysis

  • Time complexity : O(n)O(n). We traverse over the given AA array with nn elements once only.
  • Space complexity : O(1)O(1). Constant extra space is used.

Approach #6 Using Formula [Accepted]:

Algorithm

From the dpd**p solution, we can observe that for kk consecutive elements sastisfying the common difference criteria, we update the sumsum for each such element by 1,2,3,…,k1,2,3,…,k counts in that order. Thus, instead of updating the sumsum at the same time, we can just keep a track of the number of consecutive elements satisfying the common differnce criteria in a countcount variable and just update the sumsum directly as count∗(count+1)/2count∗(count+1)/2 whenver an element not satisfying this criteria is found. At the same time, we also need to reset the countcount value.

Complexity Analysis

  • Time complexity : O(n)O(n). We iterate over AA with nn elements exactly once.
  • Space complexity : O(1)O(1). Constant extra space is used.

1039. 多边形三角剖分的最低得分

难度中等41收藏分享切换为英文关注反馈

给定 N,想象一个凸 N 边多边形,其顶点按顺时针顺序依次标记为 A[0], A[i], ..., A[N-1]

假设您将多边形剖分为 N-2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 N-2 个三角形的值之和。

返回多边形进行三角剖分后可以得到的最低分。

示例 1:

1
2
3
输入:[1,2,3]
输出:6
解释:多边形已经三角化,唯一三角形的分数为 6。

示例 2:

img

1
2
3
输入:[3,7,4,5]
输出:144
解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。

示例 3:

1
2
3
输入:[1,3,1,4,1,5]
输出:13
解释:最低分数三角剖分的得分情况为 1*1*3 + 1*1*4 + 1*1*5 + 1*1*1 = 13。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int minScoreTriangulation(int[] A) {
int l=A.length;
int[][] dp=new int[l][l];
for(int i=l-3;i>=0;i--){
for(int j=i+2;j<l;j++){
for(int k=i+1;k<j;k++){
if(dp[i][j]==0)
dp[i][j]=dp[i][k]+dp[k][j]+A[i]*A[j]*A[k];
else
dp[i][j]=Math.min(dp[i][k]+dp[k][j]+A[i]*A[j]*A[k],dp[i][j]);
}
}
}
return dp[0][l-1];
}
}

解题思路

dp[i][j]表示从i到j序列的最低分。记底边为ij的三角形顶点为m,三角形imj将多边形分成三部分,总分即为三部分的分数和(如果m=i+1或m=j-1,则对应第一或第三部分分数为0)。
那么m在什么位置分数最低呢,将m从i+1到j-1遍历,分别计算dp[i][m]+A[i]A[j]A[m]+dp[m][j],取其中最小值即为dp[i][j]。
dp[i][j]=min(dp[i][m]+A[i]A[j]A[m]+dp[m][j]),for m in range [i+1,j-1]

dp table只用到右上半部分,初始化相邻两元素序列结果为0(两元素序列不能构成三角形);采用自底向上、自左向右的方向计算dp table。最终输出dp[0][n-1]。

1043. 分隔数组以得到最大和

难度中等48收藏分享切换为英文关注反馈

给出整数数组 A,将该数组分隔为长度最多为 K 的几个(连续)子数组。分隔完成后,每个子数组的中的值都会变为该子数组中的最大值。

返回给定数组完成分隔后的最大和。

示例:

1
2
3
输入:A = [1,15,7,9,2,5,10], K = 3
输出:84
解释:A 变为 [15,15,15,9,10,10,10]

提示:

  1. 1 <= K <= A.length <= 500
  2. 0 <= A[i] <= 10^6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int maxSumAfterPartitioning(int[] A, int K) {
int l=A.length;
if(l==0||A==null)return 0;
int[] dp=new int[l+1];
for(int i=1;i<=l;i++){
int j=i-1;
int max=dp[i];
while((i-j)<=K && j>=0){
max=Math.max(A[j],max);
dp[i]=Math.max(dp[i],dp[j]+max*(i-j));
j--;
}
}
return dp[l];
}
}
方法1:DP

e7c5ca8d4c19bed3e8652951c6f37c6.jpg

image.png

1218. 最长定差子序列

难度中等27收藏分享切换为英文关注反馈

给你一个整数数组 arr 和一个整数 difference,请你找出 arr 中所有相邻元素之间的差等于给定 difference 的等差子序列,并返回其中最长的等差子序列的长度。

示例 1:

1
2
3
输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。

示例 2:

1
2
3
输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。

示例 3:

1
2
3
输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。

提示:

  • 1 <= arr.length <= 10^5
  • -10^4 <= arr[i], difference <= 10^4
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int longestSubsequence(int[] arr, int difference) {
int result=1;
Map<Integer,Integer> map=new HashMap<>();
for(int i: arr){
int tmp=map.getOrDefault(i-difference, 0)+1;
map.put(i,tmp);
result=Math.max(result,tmp);
}
return result;
}
}

思路
这道题思路比较简单,跟经典问题最长递增(减)子序列有点相似,而这道题称为最长等差子序列, 也就是说是固定公差的递增(减),相对还更简单一点。

可以用dp[i]来记录以数字i为结尾的最长等差子序列的长度,那么它应该只有两种情况:

dp[i] = 1 // 表示在 i 之前没有出现等差子序列
dp[i] = dp[i - difference] + 1 // 表示在 i 之前出现了等差子序列,长度为 dp[i - difference], 而 i 也是满足这个等差序列的,所以等差序列的长度在此基础上加 1 就可以了
考虑元素值会出现负数,所以用数组存放是不行的,那么可以用一个 map来维护以 i 结尾的最长等差序列的长度,所以也就不难得出如下代码:

可以为下标加一个偏置,解决出现负值的情况,这是很OK,因为这道题arr[i]、difference的数据范围已经给的很明确了,而且比较小。

1269. 停在原地的方案数

难度困难23收藏分享切换为英文关注反馈

有一个长度为 arrLen 的数组,开始有一个指针在索引 0 处。

每一步操作中,你可以将指针向左或向右移动 1 步,或者停在原地(指针不能被移动到数组范围外)。

给你两个整数 stepsarrLen ,请你计算并返回:在恰好执行 steps 次操作以后,指针仍然指向索引 0 处的方案数。

由于答案可能会很大,请返回方案数 10^9 + 7 后的结果。

示例 1:

1
2
3
4
5
6
7
输入:steps = 3, arrLen = 2
输出:4
解释:3 步后,总共有 4 种不同的方法可以停在索引 0 处。
向右,向左,不动
不动,向右,向左
向右,不动,向左
不动,不动,不动

示例 2:

1
2
3
4
5
输入:steps = 2, arrLen = 4
输出:2
解释:2 步后,总共有 2 种不同的方法可以停在索引 0 处。
向右,向左
不动,不动

示例 3:

1
2
输入:steps = 4, arrLen = 2
输出:8

提示:

  • 1 <= steps <= 500

  • 1 <= arrLen <= 10^6

    s: steps

    l:经过s步,停留的坐标
    p[s][l]: 经过s步,停留在坐标l的总方案数
    arrLen: 数组长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
private static int MOD = 1_000_000_007;

public int numWays(int steps, int arrLen) {
int p[][] = new int[steps+1][steps+1];

p[0][0] = 1;
for (int s=1; s<=steps; s++) {
for (int l=0; l < Math.min(steps+1, arrLen); l++) {
if (s == l) {
p[s][l] = 1;
break;
}
if (s < l) {
break;
}
p[s][l] = p[s-1][l];
if (l-1 > -1) {
p[s][l] += p[s-1][l-1];
p[s][l] %= MOD;
}
if (l+1 < arrLen) {
p[s][l] += p[s-1][l+1];
p[s][l] %= MOD;
}
}
}
return p[steps][0];
}
}
动态规划解法三要素:
1、最优子结构
指针可以向左、向右、停在原地,所有最后一步可以前面的基础上往这三个方向前进,即子结构为:
p[s-1][l], p[s-1][l-1] , p[s-1][l+1]   PS: 原地、向右、向左
2、状态转移方程
p[s][l] = p[s-1][l] + p[s-1][l-1] + p[s-1][l+1]
3、边界条件
p[0][0] = 1;
p[s][l] = 0 if s < l  

问题求解:
p[s][0]
arrLen


注意点:
1、 中间结果数组,注意边界条件p[s][l] = 0 if s < l  ,所以只需要定义int[steps+1][steps+1] 而不需要是int[steps+1][arrLen],不然会超出内存限制;
2、 结果是返回模 10^9 + 7 后的结果,p[s][l] = p[s-1][l] + p[s-1][l-1] + p[s-1][l+1]  状态方程是两两相加就要求mod,而不是三个求和之后再求mod,之前结果总有用例不过
就是因为三个求和之后再求的mod。

1312. 让字符串成为回文串的最少插入次数

难度困难34收藏分享切换为英文关注反馈

给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。

请你返回让 s 成为回文串的 最少操作次数

「回文串」是正读和反读都相同的字符串。

示例 1:

1
2
3
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。

示例 2:

1
2
3
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。

示例 3:

1
2
3
输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。

示例 4:

1
2
输入:s = "g"
输出:0

示例 5:

1
2
输入:s = "no"
输出:1

提示:

  • 1 <= s.length <= 500
  • s 中所有字符都是小写字母。

我们用 dp[i][j] 表示对于字符串 s 的子串 s[i:j](这里的下标从 0 开始,并且 s[i:j] 包含 s 中的第 i 和第 j 个字符),最少添加的字符数量,使得 s[i:j] 变为回文串。

我们从外向内考虑 s[i:j]:

如果 s[i] == s[j],那么最外层已经形成了回文,我们只需要继续考虑 s[i+1:j-1];

如果 s[i] != s[j],那么我们要么在 s[i:j] 的末尾添加字符 s[i],要么在 s[i:j] 的开头添加字符 s[j],才能使得最外层形成回文。如果我们选择前者,那么需要继续考虑 s[i+1:j];如果我们选择后者,那么需要继续考虑 s[i:j-1]。

因此我们可以得到如下的状态转移方程:

dp[i][j] = min(dp[i + 1][j] + 1, dp[i][j - 1] + 1) if s[i] != s[j]
dp[i][j] = min(dp[i + 1][j] + 1, dp[i][j - 1] + 1, dp[i + 1][j - 1]) if s[i] == s[j]
边界条件为:

dp[i][j] = 0 if i >= j
注意该动态规划为区间动态规划,需要注意 dp[i][j] 的计算顺序。一种可行的方法是,我们递增地枚举子串 s[i:j] 的长度 span = j - i + 1,再枚举起始位置 i,通过 j = i + span - 1 得到 j 的值并计算 dp[i][j]。这样的计算顺序可以保证在计算 dp[i][j] 时,状态转移方程中的状态 dp[i + 1][j],dp[i][j - 1] 和 dp[i + 1][j - 1] 均已计算过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int minInsertions(String s) {
int n = s.length();
int[][] dp=new int[n][n];
for (int span = 2; span <= n; ++span) {
for (int i = 0; i <= n - span; ++i) {
int j = i + span - 1;
dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j - 1]);
}
}
}
return dp[0][n - 1];
}
}

509. 斐波那契数

难度简单118收藏分享切换为英文关注反馈

斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

给定 N,计算 F(N)

示例 1:

1
2
3
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1.

示例 2:

1
2
3
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2.

示例 3:

1
2
3
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3.

提示:

  • 0 ≤ N ≤ 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
private Integer[] cache = new Integer[31];

public int fib(int N) {
if (N <= 1) {
return N;
}
cache[0] = 0;
cache[1] = 1;
return memoize(N);
}

public int memoize(int N) {
if (cache[N] != null) {
return cache[N];
}
cache[N] = memoize(N-1) + memoize(N-2);
return memoize(N);
}
}

Approach 1: Recursion

Intuition

Use recursion to compute the Fibonacci number of a given integer.

fib(5) Recursion diagram

Figure 1. An example tree representing what fib(5) would look like

Algorithm

  • Check if the provided input value, N, is less than or equal to 1. If true, return N.
  • Otherwise, the function fib(int N) calls itself, with the result of the 2 previous numbers being added to each other, passed in as the argument. This is derived directly from the recurrence relation: Fn=Fn−1+Fn−2F**n=F*n−1+F*n−2
  • Do this until all numbers have been computed, then return the resulting answer.

Complexity Analysis

  • Time complexity : O(2N)O(2N). This is the slowest way to solve the Fibonacci Sequence because it takes exponential time. The amount of operations needed, for each level of recursion, grows exponentially as the depth approaches N.
  • Space complexity : O(N)O(N). We need space proportionate to N to account for the max size of the stack, in memory. This stack keeps track of the function calls to fib(N). This has the potential to be bad in cases that there isn’t enough physical memory to handle the increasingly growing stack, leading to a StackOverflowError. The Java docs have a good explanation of this, describing it as an error that occurs because an application recurses too deeply.

Approach 2: Bottom-Up Approach using Memoization

Intuition

Improve upon the recursive option by using iteration, still solving for all of the sub-problems and returning the answer for N, using already computed Fibonacci values. In using a bottom-up approach, we can iteratively compute and store the values, only returning once we reach the result.

Algorithm

  • If N is less than or equal to 1, return N
  • Otherwise, iterate through N, storing each computed answer in an array along the way.
  • Use this array as a reference to the 2 previous numbers to calculate the current Fibonacci number.
  • Once we’ve reached the last number, return it’s Fibonacci number.

Complexity Analysis

  • Time complexity : O(N)O(N). Each number, starting at 2 up to and including N, is visited, computed and then stored for O(1)O(1)access later on.
  • Space complexity : O(N)O(N). The size of the data structure is proportionate to N.

Approach 3: Top-Down Approach using Memoization

Intuition

Solve for all of the sub-problems, use memoization to store the pre-computed answers, then return the answer for N. We will leverage recursion, but in a smarter way by not repeating the work to calculate existing values.

Algorithm

  • Check if N <= 1. If it is, return N.
  • Call and return memoize(N)
  • If N exists in the map, return the cached value for N
  • Otherwise set the value of N, in our mapping, to the value of memoize(N-1) + memoize(N-2)

Complexity Analysis

  • Time complexity : O(N)O(N). Each number, starting at 2 up to and including N, is visited, computed and then stored for O(1)O(1)access later on.
  • Space complexity : O(N)O(N). The size of the stack in memory is proportionate to N.

Approach 4: Iterative Top-Down Approach

Intuition

Let’s get rid of the need to use all of that space and instead use the minimum amount of space required. We can achieve O(1)O(1)space complexity by only storing the value of the two previous numbers and updating them as we iterate to N.

Algorithm

  • Check if N <= 1, if it is then we should return N.
  • Check if N == 2, if it is then we should return 1 since N is 2 and fib(2-1) + fib(2-2) equals 1 + 0 = 1.
  • To use an iterative approach, we need at least 3 variables to store each state fib(N), fib(N-1) and fib(N-2).
  • Preset the initial values:
    • Initialize current with 0.
    • Initialize prev1 with 1, since this will represent fib(N-1) when computing the current value.
    • Initialize prev2 with 1, since this will represent fib(N-2) when computing the current value.
  • Iterate, incrementally by 1, all the way up to and including N. Starting at 3, since 0, 1 and 2 are pre-computed.
  • Set the current value to fib(N-1) + fib(N-2) because that is the value we are currently computing.
  • Set the prev2 value to fib(N-1).
  • Set the prev1 value to current_value.
  • When we reach N+1, we will exit the loop and return the previously set current value.

Complexity Analysis

  • Time complexity : O(N)O(N). Each value from 2 to N will be visited at least once. The time it takes to do this is directly proportionate to N where N is the Fibonacci Number we are looking to compute.
  • Space complexity : O(1)O(1). This requires 1 unit of Space for the integer N and 3 units of Space to store the computed values (curr, prev1 and prev2) for every loop iteration. The amount of Space doesn’t change so this is constant Space complexity.

Approach 5: Matrix Exponentiation

Intuition

Use Matrix Exponentiation to get the Fibonacci number from the element at (0, 0) in the resultant matrix.

In order to do this we can rely on the matrix equation for the Fibonacci sequence, to find the Nth Fibonacci number: (1  11  0)n=( F(n+1)     F(n) F(n)     F(n−1))(1110)n=(F(n+1)F(n)F(n)F(n−1))

Algorithm

  • Check if N is less than or equal to 1. If it is, return N.
  • Use a recursive function, matrixPower, to calculate the power of a given matrix A. The power will be N-1, where N is the Nth Fibonacci number.
  • The matrixPower function will be performed for N/2 of the Fibonacci numbers.
  • Within matrixPower, call the multiply function to multiply 2 matrices.
  • Once we finish doing the calculations, return A[0][0] to get the Nth Fibonacci number.

Complexity Analysis

  • Time complexity : O(log⁡N)O(logN). By halving the N value in every matrixPower‘s call to itself, we are halving the work needed to be done.
  • Space complexity : O(log⁡N)O(logN). The size of the stack in memory is proportionate to the function calls to matrixPower plus the memory used to account for the matrices which takes up constant space.

Approach 6: Math

Intuition Using the golden ratio, a.k.a Binet's forumula: φ=1+52≈1.6180339887….φ=21+5≈1.6180339887….

Here’s a link to find out more about how the Fibonacci sequence and the golden ratio work.

We can derive the most efficient solution to this problem using only constant time and constant space!

Algorithm

  • Use the golden ratio formula to calculate the Nth Fibonacci number.

Complexity Analysis

  • Time complexity : O(1)O(1). Constant time complexity since we are using no loops or recursion and the time is based on the result of performing the calculation using Binet's formula.
  • Space complexity : O(1)O(1). The space used is the space needed to create the variable to store the golden ratio formula.

面试题 17.16. 按摩师

难度简单98收藏分享切换为英文关注反馈

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

注意:本题相对原题稍作改动

示例 1:

1
2
3
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

示例 2:

1
2
3
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

示例 3:

1
2
3
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int massage(int[] nums) {
int premax=0;//记录上一个max
int curmax=0;//当前max
for(int i:nums){
int tmp=curmax;
curmax=Math.max(premax+i, curmax);//取当时max还是之前max加上目前值
premax=tmp;
}
return curmax;
}
}

《程序员面试金典(第 6 版)》独家授权img

img

本书是原谷歌资深面试官的经验之作,帮助了许多想要加入脸书、苹果、谷歌等 IT 名企的求职者拿到 Dream offer。本专题的 100+ 编程面试题是在原书基础上精心挑选出来的,帮助你轻松应战 IT 名企技术面试。

304. 二维区域和检索 - 矩阵不可变

难度中等79收藏分享切换为英文关注反馈

给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。

Range Sum Query 2D
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。

示例:

1
2
3
4
5
6
7
8
9
10
11
给定 matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

说明:

  1. 你可以假设矩阵不可变。

  2. 会多次调用 sumRegion 方法

  3. 你可以假设 row1 ≤ row2 且 col1 ≤ col2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NumMatrix {
private int[][] dp;
public NumMatrix(int[][] matrix) {
if(matrix.length==0 || matrix[0].length==0) return;
int r=matrix.length;
int c=matrix[0].length;
dp=new int[r+1][c+1];
for(int i=0;i<r;i++){
for(int j=0;j<c;j++){
dp[i+1][j+1]=dp[i][j+1]+dp[i+1][j]+matrix[i][j]-dp[i][j];
}
}
}

public int sumRegion(int row1, int col1, int row2, int col2) {
return dp[row2+1][col2+1]-dp[row1][col2+1]-dp[row2+1][col1]+dp[row1][col1];
}
}

/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/

1143. 最长公共子序列

难度中等101收藏分享切换为英文关注反馈

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

1
2
3
输入:text1 = "abcde", text2 = "ace" 
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

1
2
3
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

1
2
3
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000
  • 输入的字符串只含有小写英文字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] c1=text1.toCharArray();
char[] c2=text2.toCharArray();
int[][] dp=new int[c1.length+1][c2.length+1];
for(int i=1;i<=c1.length;i++){
for(int j=1;j<=c2.length;j++){
if(c1[i-1]==c2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
}
}
return dp[c1.length][c2.length];
}
}

139. 单词拆分

难度中等388收藏分享切换为英文关注反馈

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict*,判定 *s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

  • 拆分时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。

示例 1:

1
2
3
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

1
2
3
4
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。

示例 3:

1
2
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1.dp solution so slow
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set <String> w=new HashSet(wordDict);
int len=s.length();
boolean[] dp= new boolean[len+1];
dp[0]=true;
for(int i=1;i<=len;i++){
for(int j=0;j<i;j++){
if(dp[j] && w.contains(s.substring(j,i))){
dp[i]=true;
}
}
}
return dp[len];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//2.bfs真香
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet=new HashSet(wordDict);
Queue<Integer> queue = new LinkedList<>();
int[] visited = new int[s.length()];
queue.add(0);
while (!queue.isEmpty()) {
int start = queue.remove();
if (visited[start] == 0) {
for (int end = start + 1; end <= s.length(); end++) {
if (wordDictSet.contains(s.substring(start, end))) {
queue.add(end);
if (end == s.length()) {
return true;
}
}
}
visited[start] = 1;
}
}
return false;
}
}

300. 最长上升子序列

难度中等639收藏分享切换为英文关注反馈

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

1
2
3
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//dp真慢
class Solution {
public int lengthOfLIS(int[] nums) {
int len=nums.length;
if(len<=0)
return 0;
int[] dp=new int[len];
dp[0]=1;
int maxans=1;
for(int i=1;i<len;i++){
int maxval=0;
for(int j=0;j<i;j++){
if(nums[i]>nums[j])
maxval=Math.max(maxval,dp[j]);
dp[i]=maxval+1;
maxans=Math.max(maxans,dp[i]);
}
}
return maxans;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {//binary search加dp真香
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
int len = 0;
for (int num : nums) {
int i = Arrays.binarySearch(dp, 0, len, num);
if (i < 0) {
i = -(i + 1);
}
dp[i] = num;
if (i == len) {
len++;
}
}
return len;
}
}

322. 零钱兑换

难度中等547收藏分享切换为英文关注反馈

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

示例 1:

1
2
3
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

示例 2:

1
2
输入: coins = [2], amount = 3
输出: -1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
0。 贪心+dfs非常快
class Solution {

int ans = Integer.MAX_VALUE;

public int coinChange(int[] coins, int amount) {
Arrays.sort(coins);
coinChange(coins.length-1, coins, 0, amount);
return ans == Integer.MAX_VALUE ? -1 : ans;
}

private void coinChange(int index, int[] coins, int count, int needAmount) {
if (needAmount == 0) {
ans = Math.min(count, ans);
return;
}
if (index < 0) {
return;
}

int i = needAmount / coins[index];
for (int k=i; k>=0 && count+k<ans; k--) {
coinChange(index-1, coins, count+k, needAmount-k*coins[index]);
}

}
}
1.动态规划第二快
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
2.递归升级版很慢
public class Solution {

public int coinChange(int[] coins, int amount) {
if (amount < 1) return 0;
return coinChange(coins, amount, new int[amount]);
}

private int coinChange(int[] coins, int rem, int[] count) {
if (rem < 0) return -1;
if (rem == 0) return 0;
if (count[rem - 1] != 0) return count[rem - 1];
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = coinChange(coins, rem - coin, count);
if (res >= 0 && res < min)
min = 1 + res;
}
count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;
return count[rem - 1];
}
}

4自底向上dp自己写的

class Solution {
public int coinChange(int[] coins, int amount) {
int max=amount+1;
int[] dp=new int[max];
Arrays.fill(dp, max);
dp[0]=0;
for(int i=1;i<max;i++){
for(int coin: coins){
if(i>=coin){
dp[i]=Math.min(dp[i],dp[i-coin]+1);
}
}
}
return (dp[amount]>amount)? -1:dp[amount];
}
}

416. 分割等和子集

难度中等246收藏分享切换为英文关注反馈

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  1. 每个数组中的元素不会超过 100
  2. 数组的大小不会超过 200

示例 1:

1
2
3
4
5
输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

1
2
3
4
5
输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1. dfs+剪枝
class Solution {
public boolean canPartition(int[] nums) {
//涉及到剪枝的问题,先排个序
Arrays.sort(nums);
int sum = 0;
//算出SUM(S)
for (int n : nums){
sum += n;
}
//奇数肯定不行
if ((sum % 2) == 1){
return false;
}
sum /= 2;
//搜索
return canPartition(nums, nums.length-1, sum, sum);
}

//DFS idx为当前元素,had为待接受上限,pass为待丢弃上限
private boolean canPartition(int[] nums, int idx, int had, int pass){
//找到可行解
if (had == 0 || pass == 0){
return true;
}
//剪枝
else if (had < 0 || pass < 0){
return false;
}
//继续搜索
else{
return canPartition(nums, idx-1, had-nums[idx], pass) || canPartition(nums, idx-1, had, pass-nums[idx]);
}
}
}

作为“0-1 背包问题”,它的特点是:“每个数只能用一次”。思路是:物品一个一个选,容量也一点一点放大考虑(这一点是“动态规划”的思想,特别重要)。

如果在实际生活中,其实我们也是这样做的,一个一个尝试把候选物品放入“背包”,看什么时候能容纳的价值最大。

具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始,很多时候,我们需要考虑这个容量为 0 的数值。

状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
状态转移方程:很多时候,状态转移方程思考的角度是“分类讨论”,对于“0-1 背包问题”而言就是“当前考虑到的数字选与不选”。
1、不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;

2、选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。

状态转移方程是:

1
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

一般写出状态转移方程以后,就需要考虑边界条件(一般而言也是初始化条件)。

1、j - nums[i] 作为数组的下标,一定得保证大于等于 0 ,因此 nums[i] <= j;
2、注意到一种非常特殊的情况:j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时“背包的容积” j,这也是符合题意的。

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//2. dp解法 
class Solution {
public boolean canPartition(int[] nums) {
int len=nums.length;
int sum=0;
for(int num: nums)
sum+=num;
if(sum%2==1)
return false;
int target=sum/2;
boolean[][] dp=new boolean[len][target+1];//row: 0 to len-1 ,col:0 to target
if(nums[0]<=target)
dp[0][nums[0]]=true;
for(int i=1; i<len;i++){
for(int j=0;j<target+1;j++){
dp[i][j]=dp[i-1][j];//copy the previous answer
if(j==nums[i]){
dp[i][j]=true;
}
if(nums[i]<j){
dp[i][j]= dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
}
return dp[len-1][target];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//3.dp优化,还是dfs香
class Solution {
public boolean canPartition(int[] nums) {
int len=nums.length;
int sum=0;
for(int num: nums)
sum+=num;
if(sum%2==1)
return false;
int target=sum/2;
boolean[] dp=new boolean[target+1];//row: 0 to len-1 ,col:0 to target
if(nums[0]<=target)
dp[nums[0]]=true;
for(int i=1; i<len;i++){
for(int j=target;nums[i]<j;j--){
if(dp[target]){
dp[j]=true;
}
dp[j]= dp[j] || dp[j-nums[i]];
}
}
return dp[target];
}
}

62. 不同路径

难度中等499收藏分享切换为英文关注反馈

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右
    示例 2:

输入: m = 7, n = 3
输出: 28

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int uniquePaths(int m, int n) {
int[][] nums =new int[m][n];
for(int[] b: nums ){
Arrays.fill(b, 1);
}
for(int i=1; i< m; i++){
for(int j=1;j<n;j++){
nums[i][j]=nums[i-1][j]+nums[i][j-1];
}
}
return nums[m-1][n-1];
}
}

63. 不同路径 II

难度中等267收藏分享切换为英文关注反馈

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

说明:m 和 n 的值均不超过 100。

示例 1:

输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int row=obstacleGrid.length;
int col=obstacleGrid[0].length;
if(obstacleGrid[0][0]==1){
return 0;
}
obstacleGrid[0][0]=1;
for(int i=1;i<row;i++){
obstacleGrid[i][0]=(obstacleGrid[i][0]==0 && obstacleGrid[i-1][0]==1)? 1:0;
}
for(int j=1;j<col;j++){
obstacleGrid[0][j]=(obstacleGrid[0][j]==0 && obstacleGrid[0][j-1]==1)? 1:0;
}
for(int i=1;i<row;i++){
for(int j=1;j<col;j++){
if(obstacleGrid[i][j]==0){
obstacleGrid[i][j]=obstacleGrid[i-1][j]+obstacleGrid[i][j-1];
}
else{
obstacleGrid[i][j]=0;
}
}
}
return obstacleGrid[row-1][col-1];
}
}

64. 最小路径和

难度中等445收藏分享切换为英文关注反馈

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

1
2
3
4
5
6
7
8
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Solution {//2d array
public int minPathSum(int[][] grid) {
int row=grid.length;
int col=grid[0].length;
int [][] dp=new int [row][col];
for(int j=col-1;j>=0;j--){
for(int i=row-1; i>=0; i--){
if(i==row-1 && j !=col-1)
dp[i][j]=dp[i][j+1]+grid[i][j];
else if(j==col-1 && i !=row-1)
dp[i][j]=grid[i][j]+dp[i+1][j];
else if(i!=row-1 && j !=col-1)
dp[i][j]=grid[i][j]+Math.min(dp[i+1][j],dp[i][j+1]);
else
dp[i][j]=grid[i][j];
}
}
return dp[0][0];
}
}
Runtime: 3 ms, faster than 24.12% of Java online submissions for Minimum Path Sum.
Memory Usage: 42.6 MB, less than 72.97% of Java online submissions for Minimum Path Sum.

public class Solution {//1d array
public int minPathSum(int[][] grid) {
int[] dp = new int[grid[0].length];
for (int i = grid.length - 1; i >= 0; i--) {
for (int j = grid[0].length - 1; j >= 0; j--) {
if(i == grid.length - 1 && j != grid[0].length - 1)
dp[j] = grid[i][j] + dp[j + 1];
else if(j == grid[0].length - 1 && i != grid.length - 1)
dp[j] = grid[i][j] + dp[j];
else if(j != grid[0].length - 1 && i != grid.length - 1)
dp[j] = grid[i][j] + Math.min(dp[j], dp[j + 1]);
else
dp[j] = grid[i][j];
}
}
return dp[0];
}
}
Runtime: 2 ms, faster than 85.12% of Java online submissions for Minimum Path Sum.
Memory Usage: 42.1 MB, less than 85.13% of Java online submissions for Minimum Path Sum.

public class Solution {//2d array without extra space
public int minPathSum(int[][] grid) {
for (int i = grid.length - 1; i >= 0; i--) {
for (int j = grid[0].length - 1; j >= 0; j--) {
if(i == grid.length - 1 && j != grid[0].length - 1)
grid[i][j] = grid[i][j] + grid[i][j + 1];
else if(j == grid[0].length - 1 && i != grid.length - 1)
grid[i][j] = grid[i][j] + grid[i + 1][j];
else if(j != grid[0].length - 1 && i != grid.length - 1)
grid[i][j] = grid[i][j] + Math.min(grid[i + 1][j],grid[i][j + 1]);
}
}
return grid[0][0];
}
}
Runtime: 3 ms, faster than 24.12% of Java online submissions for Minimum Path Sum.
Memory Usage: 42.2 MB, less than 83.78% of Java online submissions for Minimum Path Sum.

120. 三角形最小路径和

难度中等365收藏分享切换为英文关注反馈

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

1
2
3
4
5
6
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int [] n= new int [triangle.size()+1];
for(int i=triangle.size()-1;i>=0;i--){
for(int j=0; j< triangle.get(i).size();j++){
n[j]=Math.min(n[j],n[j+1])+triangle.get(i).get(j);
}
}
return n[0];
}
}

931. 下降路径最小和

难度中等43收藏分享切换为英文关注反馈

给定一个方形整数数组 A,我们想要得到通过 A下降路径最小和。

下降路径可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列。

示例:

1
2
3
4
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:12
解释:
可能的下降路径有:
  • [1,4,7], [1,4,8], [1,5,7], [1,5,8], [1,5,9]
  • [2,4,7], [2,4,8], [2,5,7], [2,5,8], [2,5,9], [2,6,8], [2,6,9]
  • [3,5,7], [3,5,8], [3,5,9], [3,6,8], [3,6,9]

和最小的下降路径是 [1,4,7],所以答案是 12

提示:

  1. 1 <= A.length == A[0].length <= 100
  2. -100 <= A[i][j] <= 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int minFallingPathSum(int[][] A) {
int m=A.length;
for(int i=m-2;i>=0;i--){
for(int j=0;j<m;j++){
int best=A[i+1][j];
if(j>0)
best=Math.min(A[i+1][j-1],best);
if(j<m-1)
best=Math.min(A[i+1][j+1],best);
A[i][j]+=best;
}
}
int n=Integer.MAX_VALUE;
for(int i:A[0]){
n=Math.min(i,n);
}
return n;
}
}

1289. 下降路径最小和 II

难度困难13收藏分享切换为英文关注反馈

给你一个整数方阵 arr ,定义「非零偏移下降路径」为:从 arr 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。

请你返回非零偏移下降路径数字和的最小值。

示例 1:

1
2
3
4
5
6
7
8
输入:arr = [[1,2,3],[4,5,6],[7,8,9]]
输出:13
解释:
所有非零偏移下降路径包括:
[1,5,9], [1,5,7], [1,6,7], [1,6,8],
[2,4,8], [2,4,9], [2,6,7], [2,6,8],
[3,4,8], [3,4,9], [3,5,7], [3,5,9]
下降路径中数字和最小的是 [1,5,7] ,所以答案是 13 。

提示:

  • 1 <= arr.length == arr[i].length <= 200

  • -99 <= arr[i][j] <= 99

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Solution {
    public int minFallingPathSum(int[][] arr) {
    int m=arr.length;
    for(int i=m-2;i>=0;i--){
    for(int j=0;j<m;j++){
    int best=Integer.MAX_VALUE;
    for(int a=0;a<m;a++){
    if(a==j)
    continue;
    else{
    best=Math.min(arr[i+1][a],best);
    }
    }
    arr[i][j]+=best;
    }
    }
    int n=Integer.MAX_VALUE;
    for(int i:arr[0]){
    n=Math.min(i,n);
    }
    return n;
    }
    }

Python notes day 1

1.Run python: python name.py

2.Comment:

“””

multiple line

“””

  1. Sublime Text or PyCharm for python ide.

  2. import this#put this in terminal, the zen of python

    The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren’t special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one– and preferably only one –obvious way to do it.

Although that way may not be obvious at first unless you’re Dutch.

Now is better than never.

Although never is often better than right now.

If the implementation is hard to explain, it’s a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea – let’s do more of those!

Python notes day 2

  1. True, False, None

  2. Variable name: number, letter, _, and cannot use number as front.

  3. field instance, protected use , private use _

  4. type() : return <class ‘type’>

  5. chr(): int to unicode’s char

  6. ord(): unicode char to an int

  7. input(): for user input

  8. print(): for output

  9. %d是整数的占位符,%f是小数的占位符,%%表示百分号(因为百分号代表了占位符,所以带占位符的字符串中要表示百分号必须写成%%),字符串之后的%后面跟的变量值会替换掉占位符然后输出到终端中

  10. 运算符 描述
    [] [:] 下标,切片
    ** 指数
    ~ + - 按位取反, 正负号
    * / % // 乘,除,模,整除
    + - 加,减
    >> << 右移,左移
    & 按位与
    ^ ` `
    <= < > >= 小于等于,小于,大于,大于等于
    == != 等于,不等于
    is is not 身份运算符
    in not in 成员运算符
    not or and 逻辑运算符

    and, or , not

  11. 在使用print函数输出时,也可以对字符串内容进行格式化处理,上面print函数中的字符串%1.f是一个占位符,稍后会由一个float类型的变量值替换掉它。同理,如果字符串中有%d,后面可以用一个int类型的变量值替换掉它,而%s会被字符串的值替换掉。除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串,其中{f:.1f}{c:.1f}可以先看成是{f}{c},表示输出时会用变量f和变量c的值替换掉这两个占位符,后面的:.1f表示这是一个浮点数,小数点后保留1位有效数字。

  12. print(f'{f:.1f}华氏度 = {c:.1f}摄氏度')
    
    1
    2
    3

    13. ```python
    print('面积: %.2f' % area)
  13. python print格式化输出。

    1. 打印字符串
    1
    print ("His name is %s"%("Aviad"))

    效果:
    img

    2.打印整数

    1
    print ("He is %d years old"%(25))

    效果:
    img

    3.打印浮点数

    1
    print ("His height is %f m"%(1.83))

    效果:
    img

    4.打印浮点数(指定保留小数点位数)

    1
    print ("His height is %.2f m"%(1.83))

    效果:
    img

    5.指定占位符宽度

    1
    print ("Name:%10s Age:%8d Height:%8.2f"%("Aviad",25,1.83))

    效果:
    img

    6.指定占位符宽度(左对齐)

    1
    print ("Name:%-10s Age:%-8d Height:%-8.2f"%("Aviad",25,1.83))

    效果:
    img

    7.指定占位符(只能用0当占位符?)

    1
    print ("Name:%-10s Age:%08d Height:%08.2f"%("Aviad",25,1.83))

    效果:
    img

    8.科学计数法

    1
    format(0.0015,'.2e')

    效果:

    img

Python notes day 3

1.if, elif, else

Python notes day 4

  1. for in 循环: for x in range(a, b, c):# a是初始值,b是最终,到b-1, c是iterate的长度。
  2. while循环,配上break,continue

Python notes day 5

CRAPS game:

非常好玩的游戏, 在桌面新建.py结尾的文件或用idle直接打开即可

注意: python2可能不支持中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from random import randint

money = 1000
while money > 0:
print('你的总资产为:', money)
needs_go_on = False
while True:
debt = int(input('请下注: '))
if 0 < debt <= money:
break
first = randint(1, 6) + randint(1, 6)
print('玩家摇出了%d点' % first)
if first == 7 or first == 11:
print('玩家胜!')
money += debt
elif first == 2 or first == 3 or first == 12:
print('庄家胜!')
money -= debt
else:
needs_go_on = True
while needs_go_on:
needs_go_on = False
current = randint(1, 6) + randint(1, 6)
print('玩家摇出了%d点' % current)
if current == 7:
print('庄家胜')
money -= debt
elif current == first:
print('玩家胜')
money += debt
else:
needs_go_on = True
print('你破产了, 游戏结束!')