LeetCode Weekly Contest 29解题思路
赛题
本次周赛主要分为以下4道题:
- 563 Binary Tree Tilt (3分)
- 561 Array Partition I (6分)
- 562 Longest Line of Consecutive One in Matrix (8分)
- 564 Find the Closest Palindrome (10分)
563 Binary Tree Tilt
Problem:
Given a binary tree, return the tilt of the whole tree.
The tilt of a tree node is defined as the absolute difference between the sum of all left subtree node values and the sum of all right subtree node values. Null node has tilt 0.
The tilt of the whole tree is defined as the sum of all nodes’ tilt.
Example:
Input:
1
/ \
2 3
Output: 1
Explanation:
Tilt of node 2 : 0
Tilt of node 3 : 0
Tilt of node 1 : |2-3| = 1
Tilt of binary tree : 0 + 0 + 1 = 1
Note:
- The sum of node values in any subtree won’t exceed the range of 32-bit integer.
- All the tilt values won’t exceed the range of 32-bit integer.
很简单,递归一下完事。
public int findTilt(TreeNode root) {
dfs(root);
return sum;
}
int sum = 0;
private int dfs(TreeNode root) {
if (root == null)
return 0;
int left = dfs(root.left);
int right = dfs(root.right);
sum += Math.abs(left - right);
return root.val + left + right;
}
561 Array Partition I
Problem:
Given an array of 2n integers, your task is to group these integers into n pairs of integer, say (a1, b1), (a2, b2), …, (an, bn) which makes sum of min(ai, bi) for all i from 1 to n as large as possible.
Example:
Input: [1,4,3,2]
Output: 4
Explanation: n is 2, and the maximum sum of pairs is 4.
Note:
- n is a positive integer, which is in the range of [1, 10000].
- All the integers in the array will be in the range of [-10000, 10000]
代码很简单,简单说明下思路就出来了。按照题意,不管怎么二分,整个数组都会被划分成两部分和,这两部分和必然一大一小。如nums = [1,4,3,2]
,划分如下[1,2],[3,4]
,它们的和分别为3和7。现在我们考虑【小和】的情况,因为题目说了min(ai, bi)
,所以划分时,我们总是取较小的元素为一个集合,那么带来的结果必然是sum(xi), i = 1 to n, xi = min(ai,bi)
较小,现在要让这部分的和最小,那么自然在划分两部分时,让两边的和尽可能相等。这就说明了一点,每当选取两个元素时,应该让它们尽可能的【靠近】,这样小和能尽量接近大和。
所以,每次选择数组中最小的两个元素,如1,2
,即取min(1,2)
,接下来继续取剩余元素中的两个最小,取min(3,4)
,所以有了min(1,2) + min(3,4) = 1 + 3 = 4
。
public int arrayPairSum(int[] nums) {
Arrays.sort(nums);
int sum = 0;
for (int i = 0; i < nums.length; i += 2)
sum += nums[i];
return sum;
}
562 Longest Line of Consecutive One in Matrix
Problem:
Given a 01 matrix M, find the longest line of consecutive one in the matrix. The line could be horizontal, vertical, diagonal or anti-diagonal.
Example:
Input:
[[0,1,1,0],
[0,1,1,0],
[0,0,0,1]]
Output: 3
Hint:
The number of elements in the given matrix will not exceed 10,000.
其实就是对连续1的检测,计算最长长度,代码细节题,考你各种遍历。先送上自己的代码,逐步对它改进。
public int longestLine(int[][] M) {
int row = M.length;
if (row == 0)
return 0;
int col = M[0].length;
if (col == 0)
return 0;
int[][][] dp = new int[row][col][4];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
dp[i][j][0] = M[i][j];
dp[i][j][1] = M[i][j];
dp[i][j][2] = M[i][j];
dp[i][j][3] = M[i][j];
}
}
int max = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (j- 1 != -1 && M[i][j-1] == 1 && M[i][j] == M[i][j-1]){
dp[i][j][0] = dp[i][j-1][0] + 1;
max = Math.max(max, dp[i][j][0]);
}
if ((i != 0 && j != 0) && M[i-1][j-1] == 1 && M[i][j] == M[i-1][j-1]){
dp[i][j][1] = dp[i-1][j-1][1] + 1;
max = Math.max(max, dp[i][j][1]);
}
if (i != 0 && M[i-1][j] == 1 && M[i-1][j] == M[i][j]){
dp[i][j][2] = dp[i-1][j][2] + 1;
max = Math.max(max, dp[i][j][2]);
}
if ((i + 1 != row && j != 0) && M[i][j] == 1 && M[i][j] == M[i+1][j-1]){
dp[i+1][j-1][3] = dp[i][j][3] + 1;
max = Math.max(max, dp[i+1][j-1][3]);
}
max = Math.max(max, M[i][j]);
}
}
return max;
}
- 用了个三维dp,其中第三维的
[0,1,2,3]
分别表示四种遍历方向,horizontal,vertical,diagonal和anti-diagonal
。 - 注意越界问题,每种方向的遍历都可能导致不同的数组越界,需谨慎对待。
刚开始想着斜对角怎么遍历,但其实完全没有必要,我们只需要在i和j
循环递增时,过滤出方向的遍历即可。那么不同方向的遍历就可以写在一个循环内了。
连续元素的判断条件为:
起始条件 && 连续条件
两者捆绑在了一块,所以当存在当个元素符合起始条件时,会出现漏检的情况,所以我们有了最后一句判断max = Math.max(max, M[i][j]);
,针对的就是落单的元素。
上述代码还可以进一步优化,首先dp的初始化可以放在连续1循环
中,其次max = Math.max(max,dp[i][j][?])
可以合并。所以优化的代码如下:
public int longestLine(int[][] M) {
int row = M.length;
if (row == 0)
return 0;
int col = M[0].length;
if (col == 0)
return 0;
int[][][] dp = new int[row][col][4];
int max = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
for (int k = 0; k < 4; k++) dp[i][j][k] = M[i][j];
if (j- 1 != -1 && M[i][j-1] == 1 && M[i][j] == M[i][j-1]){
dp[i][j][0] = dp[i][j-1][0] + 1;
}
if ((i != 0 && j != 0) && M[i-1][j-1] == 1 && M[i][j] == M[i-1][j-1]){
dp[i][j][1] = dp[i-1][j-1][1] + 1;
}
if (i != 0 && M[i-1][j] == 1 && M[i-1][j] == M[i][j]){
dp[i][j][2] = dp[i-1][j][2] + 1;
}
if ((i != 0 && j + 1 != col) && M[i-1][j+1] == 1 && M[i][j] == M[i-1][j+1]){
dp[i][j][3] = dp[i-1][j+1][3] + 1;
}
max = Math.max(max,Math.max(dp[i][j][0],dp[i][j][1]));
max = Math.max(max,Math.max(dp[i][j][2],dp[i][j][3]));
}
}
return max;
}
这道题为什么需要一个dp[i][j][4]
?原因在于连续元素的判断条件在作怪,如果单纯的用一个变量去更新连续长度时,必须让该变量的初始值为1,连续元素的判断条件主角是M[i][j] == 1
,始终是后一位元素,而像这种情况,当M[i][j] == 0
时,也符合,那么自然而然的更新会出错。
看一道题
https://leetcode.com/contest/leetcode-weekly-contest-28/problems/student-attendance-record-i/
重新总结下,更优的写法。
public boolean checkRecord(String s){
int countA = 0;
int countL = 0;
int maxL = 0;
for (int i = 0; i < s.length(); i++){
if (s.charAt(i) == 'A') countA ++;
countL = s.charAt(i) == 'L' ? countL + 1 : 0;
maxL = Math.max(maxL, countL);
}
if (countA > 1 || maxL > 2) return false;
return true;
}
这道题给我的启示在于,我们现在可以利用【断】来重新计数,这样就不要row * col
的空间复杂度了。
public int longestLine(int[][] M) {
int row = M.length;
if (row == 0) return 0;
int col = M[0].length;
if (col == 0) return 0;
int max = 0;
for (int i = 0; i < row; i++){
for (int j = 0, hori = 0; j < col; j++){
hori = M[i][j] == 1 ? hori + 1 : 0;
max = Math.max(max, hori);
}
}
for (int j = 0; j < col; j++){
for (int i = 0, vert = 0; i < row; i++){
vert = M[i][j] == 1 ? vert + 1 : 0;
max = Math.max(max, vert);
}
}
for (int k = 0; k < row + col; k++){
for (int i = Math.min(k, row - 1), j = Math.max(0, k - row), diag = 0; i >= 0 && j < col; i--, j++) {
diag = M[i][j] == 1 ? diag + 1 : 0;
max = Math.max(max, diag);
}
for (int i = Math.max(row - 1 - k, 0), j = Math.max(0, k - row), anti = 0; i < row && j < col; j++, i++){
anti = M[i][j] == 1 ? anti + 1 : 0;
max = Math.max(max, anti);
}
}
return max;
}
这种解法严重依赖于遍历的顺序,在做对角线遍历时,需要注意M[row-1][0]
会被遍历两次,但此处不影响该题的答案。
564 Find the Closest Palindrome
Problem:
Given an integer n, find the closest integer (not including itself), which is a palindrome.
The ‘closest’ is defined as absolute difference minimized between two integers.
Example:
Input: “123”
Output: “121”
Note:
- The input n is a positive integer represented by string, whose length will not exceed 18.
- If there is a tie, return the smaller one as answer.
这道题比较综合,首先得想出一种合理的解决方案,其次代码细节也相对比较复杂,我们先给出思路。
回文就是镜像对称,题目除了要让我们求出回文外,还需要求得的回文与原始的num差距尽量小,因为差距可正可负,所以比原数大的回文和小的回文都是可以的,取其diff较小的即可。
咱们不说暴力求解了,没啥可以探讨的,直接考虑相对最优解。我们可以利用的条件就两条:回文镜像和最小的diff,但不管怎么样,我们最后解一定是个回文。
我们先从字符串的头和尾考虑,如果存在字符串诸如"807045053224792883"
,现在我们有未知解
{x0x2...x17},xi=x17−i
,该回文左右互为镜像,所以我们可以拿它来求diff,假设该回文大于原数。
所以有:
有了该公式,我们就可以开始生成我们的回文了,为了让diff最小,很明显越是高位的越不能【操作】,而回文我们知道它的特性,为了让该数变成回文,我们需要让首尾互为镜像,既然高位不能变动,那么就只能变动低位了,所以为了生成回文,我们对半砍,让左半部分生成右半部分,保证当前回文与原数的diff一定是最小的。但注意题目要求,他说的是比大比小的回文都可以,现在假设我们求比原数大的回文,结果镜像后是有可能出现比原数小的情况,所以我们要对其进行检查,并把它变成较大的回文才符合题意。所以我们有代码:
private long bigger (long u){
char[] origin = Long.toString(u).toCharArray();
int len = origin.length;
char[] palind = Arrays.copyOf(origin, len);
for (int i = 0; i < len / 2; i ++){
palind[len - 1 - i] = palind[i];
}
//检查生成的回文是否比原数大
for (int i = 0; i < len; i++){
if (origin[i] > palind[i]){
for (int j = (len-1) / 2; j >= 0; j--){
if (++palind[j] > '9'){
palind[j] = '0';
}else{
break;
}
}
for (int j = 0; j < len / 2; j++){
palind[len-j-1] = palind[j];
}
return Long.parseLong(new String(palind));
}else if (origin[i] < palind[i]){
return Long.parseLong(new String(palind));
}
}
return Long.parseLong(new String(palind));
}
上面还需注意一个细节,为什么我们是从左半部分的最右边开始改,能够确保返回diff最小的回文?一个原因,如果把右侧元素变大意味着左侧元素跟着变大,所以不管怎么改,一定是从最中间开始改起,才能够让diff最小。
接下来就是生成比原数小的回文了,思路是一模一样的,如果生成的回文比原数小,那么直接返回,而当生成的回文比原数大时,我们只需要从中间开始改动,当遇到’0’时,借位向上改。
private long smaller(long u) {
char[] origin = Long.toString(u).toCharArray();
int len = origin.length;
char[] palind = Arrays.copyOf(origin, len);
for (int i = 0; i < len / 2; i++) {
palind[len - 1 - i] = palind[i];
}
for (int i = 0; i < len; i++) {
if (origin[i] > palind[i]) {
return Long.parseLong(new String(palind));
} else if (origin[i] < palind[i]) {
for (int j = (len - 1) / 2; j >= 0; j--) {
if (--palind[j] < '0') {
palind[j] = '9';
} else {
break;
}
}
if(palind[0] == '0'){
if(palind.length == 1){
return 0;
}
palind = new char[len-1];
Arrays.fill(palind, '9');
return Long.parseLong(new String(palind));
}
for (int j = 0; j < len / 2; j++){
palind[len - j - 1] = palind[j];
}
return Long.parseLong(new String(palind));
}
}
return Long.parseLong(new String(palind));
}
此处需要注意一些例外,如给定"100001"
,如果不做特殊处理,按照上述思路会得到099990
,这种情况,需要把高位0去掉,且把最低位变成9。所以有了最终的代码:
public String nearestPalindromic(String n) {
long z = Long.parseLong(n);
long x = bigger(z + 1);
long y = smaller(z - 1);
if (x - z < z - y) return Long.toString(x);
else return Long.toString(y);
}