leetcode everyday

目录

day 1 2024-02-27

543 Diameter of Binary Tree

Given the root of a binary tree, return the length of the diameter of the tree.

The diameter of a binary tree is the length of the longest path between any two nodes in a tree. This path may or may not pass through the root.

The length of a path between two nodes is represented by the number of edges between them.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0227W27EaKxHpjd2.png

题解

读题, 题目要求寻找二叉树中任意两节点之间的最大距离. 这时这个二叉树更像是一个图的性质而不是树, 即寻找图中任意两节点之间的最大距离. 考虑任意一点到另一点的距离等于其到另一点的父节点的距离减一, 则使用一个二维数组保存每个节点的两个属性:

  1. 以该节点为根节点且经过该节点的最大直径
  2. 以该节点为根节点的子树中叶子节点到该节点的最大距离

属性1可以通过将该节点的两个子节点的属性2加和并加1来计算. 属性2取两个子节点属性2的最大值并加1来计算. 最后遍历数组求得数组中属性1的最大值即可. 有一点动态规划的思想.

题目中并没有提供二叉树的节点总数, 则可以使用动态创建的方法, 在遍历过程中每遇到新节点就在二维数组中增加一项. 这里使用递归来对树进行后序遍历, 对空节点, 设置其属性2的值为-1, 这样保证叶子节点的属性2的值为0.

解题时遇到一个小bug, 使用了一个全局切片(数组)来保存变量时, 第一个测试用例的数据会保留到测试第二个测试用例的过程中, 这大概是因为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
28
29
30
31
32
33
34
35
36
37
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */

var length [][]int

func diameterOfBinaryTree(root *TreeNode) int {
    length = nil
    _ = postorder(root)
    max := -1
    for _, value := range length{
        if value[0] > max{
            max = value[0]
        }
    }
    return max
}

func postorder(father *TreeNode) int {
    if father != nil{
        len1 := postorder(father.Left)
        len2 := postorder(father.Right)
        len := make([]int,2)
        // find the max diameter pass through current node from the tree rooted current node
        len[0] = len1 + len2 + 2
        len[1] = max(len1, len2) + 1
        length = append(length, len)
        return len[1]
    } else {
        return -1
    }
}

day2 2024-02-28

513. Find Bottom Left Tree Value

Given the root of a binary tree, return the leftmost value in the last row of the tree.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0228z98bIXZ3aQeQ.png

题解

找到二叉树最底层最左边的节点值, 用层序遍历遍历到最后一层找到最后一层最左边节点的值即可. 实现层序遍历可以使用一个队列, 将当前层节点的所有子节点入队后将当前层节点出队. 在go中可以使用切片实现一个队列. 使用一个标记变量记录当前层所有节点是否有子节点, 若无子节点则当前层为最低层, 返回当前层最左侧节点的值(此时队列中第一个节点的值).

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func findBottomLeftValue(root *TreeNode) int {
    queue := []*TreeNode{root}
    lowest := false
    key := -1
    var father *TreeNode
    for !lowest{
        queue = queue[key+1:]
        lowest = true
        for key, father = range queue{
            if(father.Left != nil || father.Right != nil){
                lowest = false
                if(father.Left != nil){
                    queue = append(queue, father.Left)
                }
                if(father.Right != nil){
                    queue = append(queue, father.Right)
                }
            }
        }

    }
    return queue[0].Val
}

总结

在题解中看到了一个使用深度优先搜索的方法, 记录当前搜索到的层级, 始终保存最大层级的第一个被搜索到的值, 因为使用的是后序遍历, 则每次遇到的当前层大于保存的最大层级时, 该节点就为新的最大层级的第一个节点, 即题目中要求的最左值(leftmost). 算法时间复杂度为O(n)——只遍历一次所有节点.

day3 2024-02-29

1609. Even Odd Tree

A binary tree is named Even-Odd if it meets the following conditions:

The root of the binary tree is at level index 0, its children are at level index 1, their children are at level index 2, etc. For every even-indexed level, all nodes at the level have odd integer values in strictly increasing order (from left to right). For every odd-indexed level, all nodes at the level have even integer values in strictly decreasing order (from left to right). Given the root of a binary tree, return true if the binary tree is Even-Odd, otherwise return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0229uX1ZoOdkFEtk.png

题解

对二叉树的奇数层, 其节点从左到右是严格递减的(意味着有两个节点的值相同是不允许的), 偶数层是严格递增的. 仍然可以使用昨天题目的层序遍历的方法, 增加一个level变量记录当前层数, 对该层内的节点值进行判断是否符合奇数层和偶数层对应的条件即可.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func isEvenOddTree(root *TreeNode) bool {
    level := 0
    index := -1
    var value *TreeNode
    queue := []*TreeNode{root}
    saved := 0
    for len(queue) != 0{
        // remove visited nodes
        queue = queue[index+1:]
        index = 0
        if(level%2 == 0){
            for index, value = range queue{
                if(index == 0){
                    saved = value.Val
                    if(saved %2 != 1){return false}
                } else{
                    if(value.Val <= saved || (value.Val%2) != 1){
                        return false
                    } else{
                        saved = value.Val
                    }
                }
                if(value.Left != nil){queue = append(queue, value.Left)}
                if(value.Right != nil){queue = append(queue, value.Right)}
            }
            level++
        } else{
            for index, value = range queue{
                if(index == 0){
                    saved = value.Val
                    if(saved %2 != 0){return false}
                } else{
                    if(value.Val >= saved || value.Val %2 != 0){
                        return false
                    } else{
                        saved = value.Val
                    }
                }
                if(value.Left != nil){queue = append(queue, value.Left)}
                if(value.Right != nil){queue = append(queue, value.Right)}
            }
            level++
        }
    }
    return true
}

总结

go语言中的for range循环, 如果使用类似for key, value := range list 的形式, 那么key, value这两个变量都会在当前作用域下新建, 意味着即使在前面定义了key, key的值在循环结束后也不会被修改. 若想修改之前定义的key值, 需要将value也提前定义好并使用=而不是:=.

go语言中的for range循环时如果使用:=会新建两个变量, 然后将slice中的值复制给value变量, 将对应的index值赋值给key变量, 这意味着value变量不会指向数组中对应位置的地址, 而是一个不变的单独地址.

day4 2024-03-01

2864. Maximum Odd Binary number

You are given a binary string s that contains at least one ‘1’.

You have to rearrange the bits in such a way that the resulting binary number is the maximum odd binary number that can be created from this combination.

Return a string representing the maximum odd binary number that can be created from the given combination.

Note that the resulting string can have leading zeros.

Example 1:

Input: s = “010” Output: “001” Explanation: Because there is just one ‘1’, it must be in the last position. So the answer is “001”.

Example 2:

Input: s = “0101” Output: “1001” Explanation: One of the ‘1’s must be in the last position. The maximum number that can be made with the remaining digits is “100”. So the answer is “1001”.

题解

题目中说明了给出的字符串中至少有一个1, 因此可以复制一个字符串, 然后遍历原字符串, 遇到第一个1放在最后一位, 0永远插入到倒数第二位, 不是第一个1放在字符串最前面. 由此除保证字符串是奇数的最后一个1以外, 其余的1都在字符串最前面, 其余的0都插入在最前面的一串1和最后的1之间. 保证了字符串是最大的奇数字符串.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func maximumOddBinaryNumber(s string) string {
    s_copy := ""
    flag := 0
    for _, value := range s{
        if value == '1'{
            if flag != 0{
                s_copy = "1" + s_copy
            } else{
                s_copy = s_copy + "1"
                flag = 1
            }
        } else{
            if(len(s_copy) >=2){
                s_copy = string(s_copy[:len(s_copy)-1]) + "0" + string(s_copy[len(s_copy)-1])
            } else {
                s_copy = "0" + s_copy
            }
        }
    }
    return s_copy
}

总结

在处理字符串的时候像中间某个位置插入字符也要使用双引号, 如插入字符0要用+"0"而不是+'0', 此外在截取切片的时候go的切片是左闭右开的. 如[0:3]截取的是0,1,2三个数

day5 2024-03-02

977. Squares of a Sorted Array

Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order.

Example 1:

Input: nums = [-4,-1,0,3,10] Output: [0,1,9,16,100] Explanation: After squaring, the array becomes [16,1,0,9,100]. After sorting, it becomes [0,1,9,16,100].

Example 2:

Input: nums = [-7,-3,2,3,11] Output: [4,9,9,49,121]

题解

考虑原数组已经按照非递减排序的情况下, 找到数组中正负交界处元素, 即数组中第一个正数, 以该位置作为起始位置, 使用双指针法, 分别向前和向后遍历数组, 遍历时不断比较两个指针指向的数字的绝对值大小, 将绝对值小的数字平方后追加到结果数组的尾部, 遍历完成即可完成平方值排序. 这样只需要遍历一遍数组即可完成排序.

代码

 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
func sortedSquares(nums []int) []int {
    index := 0
    value := nums[0]
    var result []int
    for index, value = range nums{
        if(value >= 0){
            break
        }
    }
    backward := index - 1
    forward := index
    if index != 0{
        for _,_ = range nums{
            if backward<0{
                result = append(result, nums[forward]*nums[forward])
                forward++
            } else if forward>= len(nums){
                result = append(result, nums[backward] * nums[backward])
                backward--
            } else{
            if(abs(nums[forward]) < abs(nums[backward])){
                result = append(result, nums[forward]*nums[forward])
                forward++
            } else{
                result = append(result, nums[backward] * nums[backward])
                backward--
            }
            }
        }
    }else{
        for _,_ = range nums{
            result = append(result, nums[forward]*nums[forward])
                forward++
        }
    }
    return result
}

func abs(val int) int {
    if(val < 0){
        return -val
    }else{
        return val
    }
}

总结

注意一个go的语法问题, 如果在for range循环中两个变量都使用匿名变量, 则应该使用赋值运算符而不是创建并赋值运算符, 即for _,_ = range slice 而不是for _,_ := range slice. 这很可能是因为匿名变量默认为已经创建好的变量, 不需要再创建匿名变量本身了.

day6 2024-03-03

19. Remove Nth Node From End of List

Given the head of a linked list, remove the nth node from the end of the list and return its head.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0303SQETJiUaYzaQ.png

题解

给出了链表头, 要求移除从后到前的第n个节点. 如果想一遍遍历就完成这个任务. 就使用空间换时间, 使用一个数组保存所有的Next指针的值. 然后让倒数第n+1个元素的Next指针指向n个元素的Next指针即可, 注意处理链表只有一个元素的特殊情况.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    var ptr []*ListNode
    current := head
    for current.Next != nil{
        ptr = append(ptr, current)
        current = current.Next
    }
    ptr = append(ptr, current)
    if(len(ptr) == 1){
        return nil
    }else if len(ptr) == n{
        return ptr[1]
    }else{
        ptr[len(ptr)-n-1].Next = ptr[len(ptr)-n].Next
        return head
    }
}

总结

在题解中看到大部分使用的是快慢指针的解法, 快慢指针应该是本题想要的解法, 下面贴一个快慢指针的解法示例

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */

func removeNthFromEnd(head *ListNode, n int) *ListNode {

    dummyHead := &ListNode{-1, head}

    cur, prevOfRemoval := dummyHead, dummyHead

    for cur.Next != nil{

        // n step delay for prevOfRemoval
        if n <= 0 {
            prevOfRemoval = prevOfRemoval.Next
        }

        cur = cur.Next

        n -= 1
    }

    // Remove the N-th node from end of list
    nthNode := prevOfRemoval.Next
    prevOfRemoval.Next = nthNode.Next

    return dummyHead.Next

}

day7 2024-03-04

948. Bag of Tokens

You start with an initial power of power, an initial score of 0, and a bag of tokens given as an integer array tokens, where each tokens[i] donates the value of tokeni.

Your goal is to maximize the total score by strategically playing these tokens. In one move, you can play an unplayed token in one of the two ways (but not both for the same token):

Face-up: If your current power is at least tokens[i], you may play tokeni, losing tokens[i] power and gaining 1 score. Face-down: If your current score is at least 1, you may play tokeni, gaining tokens[i] power and losing 1 score. Return the maximum possible score you can achieve after playing any number of tokens.

Example 1:

Input: tokens = [100], power = 50

Output: 0

Explanation: Since your score is 0 initially, you cannot play the token face-down. You also cannot play it face-up since your power (50) is less than tokens[0] (100).

Example 2:

Input: tokens = [200,100], power = 150

Output: 1

Explanation: Play token1 (100) face-up, reducing your power to 50 and increasing your score to 1.

There is no need to play token0, since you cannot play it face-up to add to your score. The maximum score achievable is 1.

Example 3:

Input: tokens = [100,200,300,400], power = 200

Output: 2

Explanation: Play the tokens in this order to get a score of 2:

Play token0 (100) face-up, reducing power to 100 and increasing score to 1. Play token3 (400) face-down, increasing power to 500 and reducing score to 0. Play token1 (200) face-up, reducing power to 300 and increasing score to 1. Play token2 (300) face-up, reducing power to 0 and increasing score to 2. The maximum score achievable is 2.

题解

本题的目的是最大化score. 一个基本的策略就是通过小的power换取score, 通过score换取大的power, 利用换到的大power赚取中间水平的token的score. 关键在于, 如何找到最大能换取的score. 首先考虑每次进行一次Face-up和Face-down, score没有变化, 只有power增大了, 那么每次都将score置0, 并判断当前能获得的最大score即可.

通过前面的分析可以得出, 算法分为以下几步

  1. 将tokens数组排序
  2. 判断power是否大于tokens[0], 即最小的token, 若大于, 则计算当前能获得的最大score
  3. 将power的值增加目前tokens数组中最大值和最小值(即排好序后的最后一项和第一项)的差值, 同时将tokens数组中第一项和最后一项移除. 重复2, 3步直到power小于tokens[0]或tokens数组长度为0中止.
  4. 返回最大的score

代码

 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
func bagOfTokensScore(tokens []int, power int) int {
    sort.Ints(tokens)
    var powernormal []int
    remain := power
    score := 0
    powernormal = append(powernormal, score)
    for len(tokens) != 0 && power > tokens[0]{
        remain = power
            for _, value := range tokens{
                if power >= value{
                    score++
                    power -= value
                } else{
                    break
                }
            }
            powernormal = append(powernormal, score)
            score = 0


            remain += tokens[len(tokens)-1] - tokens[0]
            if len(tokens) <= 1{
                break
            }
            tokens = tokens[1:len(tokens)-1]
            power = remain


    }
    sort.Ints(powernormal)
    return powernormal[len(powernormal)-1]
}

总结

在实现过程中, 起初使用一个数组来保存每次的score值, 这样空间复杂度略大, 后来查看他人代码, 发现只需要一个max变量来保存当前最大的score值, 并在每次循环计算当前轮次的score值时与当前的最大值比较并根据二者大小更新max变量的值即可, 这样只需要O(1)的空间复杂度.

day8 2024-03-05

1750. Minimum Length of String After Deleting Similar Ends

Given a string s consisting only of characters ‘a’, ‘b’, and ‘c’. You are asked to apply the following algorithm on the string any number of times:

  1. Pick a non-empty prefix from the string s where all the characters in the prefix are equal.
  2. Pick a non-empty suffix from the string s where all the characters in this suffix are equal.
  3. The prefix and the suffix should not intersect at any index.
  4. The characters from the prefix and suffix must be the same.
  5. Delete both the prefix and the suffix. Return the minimum length of s after performing the above operation any number of times (possibly zero times).

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0305Qc2qNiDBOKRk.png

题解

本题的题目略显复杂, 读起来乍一看让人不明所以, 实际上题目的目标即为从字符串的前后同时删除相同的字符, 删除时只要是相同的就全部删除, 如最前面有一个a最后面有3个a则同时将这四个a删除. 删除的字符串下标不能相交. 思路比较简单, 判断字符串的前后字符是否相同, 然后删除前后的连续相同字符即可, 注意下标不要重叠. 同时注意边界情况, 字符串只有一个字符的情况单独处理一下(直接返回1).

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func minimumLength(s string) int {
    forward := 0
    backward := 0
    for s[0] == s[len(s)-1]{
    if len(s) == 1{
        return 1
    } else {
            forward = 0
            backward = len(s)-1
            for forward<backward && s[forward+1] == s[forward] {
                forward++
            }
            for backward>forward && s[backward-1] == s[backward] {
                backward--
            }
            if forward == backward{
                return 0
            }
            s = s[forward+1:backward]
        }
    }
    return len(s)
}

总结

本题值得注意的地方在于for循环中条件的设置, 可能会忽略与运算符两侧条件表达式的前后顺序, 但由于短路机制的存在, 与运算符两侧表达式的前后顺序有时非常重要, 例如在本题中, 如果将forward<backward条件设置在前面, 则当forward到达数组的边界时会直接退出循环, 但若将相等判断放在前面, 会因为当forward到达数组边界时forward+1下标越界而出错.

day9 2024-03-06

141. Linked List Cycle

Given head, the head of a linked list, determine if the linked list has a cycle in it.

There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail’s next pointer is connected to. Note that pos is not passed as a parameter.

Return true if there is a cycle in the linked list. Otherwise, return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0306efjeHXzvLKPM.png

题解

本题最简单的思路就是使用并查集, 因为go语言中的map类型在获取数据时会返回该键值在map中是否存在, 因此可以将map类型直接当作一个简单的并查集使用.

代码

 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 singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */


func hasCycle(head *ListNode) bool {
    if head==nil{
        return false
    }
    ptrs := make(map[*ListNode]int)
    var exist bool
    ptrs[head] = 1
    for head.Next != nil{
        _, exist = ptrs[head.Next]
        if !exist{
            ptrs[head.Next] = 1
            head = head.Next
        } else{
            return true
        }
    }
    return false
}

总结

题目中给出提示本题可以使用O(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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func hasCycle(head *ListNode) bool {
	if head == nil {
		return false
	}

	slow := head
	fast := head

	for slow != nil && fast != nil && fast.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next

		if slow == fast {
			return true
		}
	}

	return false
}

day10 2024-03-07

876. Middle of the Linked List

Given the head of a singly linked list, return the middle node of the linked list.

If there are two middle nodes, return the second middle node.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0307rm4x7SjNblpu.png

题解

本题寻找链表中间位置的元素, 是一个经典快慢指针的题目. 只需要让快指针前进的速度为2, 慢指针为1, 则快指针到达链表末尾时慢指针正好指向中间位置. 要注意链表元素个数为奇数和偶数时的处理方法的不同.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func middleNode(head *ListNode) *ListNode {
    fast := head
    slow := head
    for fast.Next != nil && fast.Next.Next != nil{
        fast = fast.Next.Next
        slow = slow.Next
    }
    // consider list number is even
    if fast.Next != nil{
        slow = slow.Next
    }
    return slow
}

总结

本题是一道经典的快慢指针的简单题目, 进一步深化了快慢指针的应用.

day11 2024-03-08

3005. Count Elements With Maximum Frequency

You are given an array nums consisting of positive integers.

Return the total frequencies of elements in nums such that those elements all have the maximum frequency.

The frequency of an element is the number of occurrences of that element in the array.

Example 1:

Input: nums = [1,2,2,3,1,4] Output: 4 Explanation: The elements 1 and 2 have a frequency of 2 which is the maximum frequency in the array. So the number of elements in the array with maximum frequency is 4.

Example 2:

Input: nums = [1,2,3,4,5] Output: 5 Explanation: All elements of the array have a frequency of 1 which is the maximum. So the number of elements in the array with maximum frequency is 5.

题解

因为题目给出了正整数的范围为1-100, 因此本题可以用简单的数组来解决, 数组下标表示对应的整数, 0不做任何表示. 然后遍历数组将频率最多的元素相加即可. 可以设置一个max标志位来表示当前的最大频率, 相等则增加和, 比max大则将和重置并设max为新的最大值.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func maxFrequencyElements(nums []int) int {
    frequency := make([]int, 101)
    for _,value := range nums{
        frequency[value]++
    }
    max := 0
    sum := 0
    for _,value := range frequency{
        if value > max{
            max = value
            sum = max
        } else if value == max{
            sum += value
        }else{
            continue
        }
    }
    return sum
}

总结

本题注意考查数据范围, 在数据范围有限的情况下直接使用数组要比哈希表快得多.

day12 2024-03-09

2540. Minimum Common Value

Given two integer arrays nums1 and nums2, sorted in non-decreasing order, return the minimum integer common to both arrays. If there is no common integer amongst nums1 and nums2, return -1.

Note that an integer is said to be common to nums1 and nums2 if both arrays have at least one occurrence of that integer.

Example 1:

Input: nums1 = [1,2,3], nums2 = [2,4] Output: 2 Explanation: The smallest element common to both arrays is 2, so we return 2.

Example 2:

Input: nums1 = [1,2,3,6], nums2 = [2,3,4,5] Output: 2 Explanation: There are two common elements in the array 2 and 3 out of which 2 is the smallest, so 2 is returned.

题解

本题使用两个指针分别指向两个数组, 然后依次移动两个指针比较大小即可, 位于数值更小的位置的数组指针向前移动直到相等或者到数组末尾为止.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func getCommon(nums1 []int, nums2 []int) int {
    index1, index2 := 0 , 0
    for index1<len(nums1) && index2 < len(nums2){
        if nums1[index1] < nums2[index2]{
            index1++
        }else if nums2[index2] < nums1[index1]{
            index2++
        } else{
            return nums1[index1]
        }
    }
    return -1
}

总结

这是一道典型的双指针问题, 思路清晰就可以很快解决.

day13 2024-03-10

349. Intersection of Two arrays

Given two integer arrays nums1 and nums2, return an array of their intersection. Each element in the result must be unique and you may return the result in any order.

Example 1:

Input: nums1 = [1,2,2,1], nums2 = [2,2] Output: [2]

Example 2:

Input: nums1 = [4,9,5], nums2 = [9,4,9,8,4] Output: [9,4] Explanation: [4,9] is also accepted.

题解

因为数组是无序数组, 寻找二者的相同元素比较困难, 故可先对两数组排序, 然后双指针遍历两个数组找到数组中的相同值. 将值作为key, key对应的value为true放入map中. 这里不需要多余判断map中是否已经存在这个key了, 因为再次给相同的key赋值不会增加新的条目, 而只是覆盖之前的key的值, 我们只需要key来判断map中是否有相同值.

代码

 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
func intersection(nums1 []int, nums2 []int) []int {
    intersection := make(map[int]bool)
    sort.Ints(nums1)
    sort.Ints(nums2)
    index1, index2 := 0,0
    for index1 < len(nums1) && index2 < len(nums2){
        if nums1[index1] < nums2[index2]{
            index1++
        }else if nums1[index1] > nums2[index2]{
            index2++
        }else{
            _, ok := intersection[nums1[index1]]
            if !ok{
                intersection[nums1[index1]] = true
            }
            index1++
            index2++
        }
    }
    var result []int
    for key, _ := range intersection{
        result = append(result, key)
    }
    return result
}

总结

在查看他人题解过程中, 发现排序其实是没有必要的, 可以直接将一个数组中的值全部作为key, 对应的value为true放入map中. 然后遍历另外一个数组, 同时判断当前遍历的元素在不在map中, 若存在则将其放入结果数组中, 同时将map中key对应的value置为false, 表示该key已经被访问过, 这样可以避免在结果数组中添加重复元素.

一个示例代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func intersection(nums1 []int, nums2 []int) []int {
    res := make([]int, 0)
    m := make(map[int]bool, len(nums1))

    for _, v := range nums1 {
        if _, exists := m[v]; exists {
            continue
        }
        m[v] = false
    }

    for _, v := range nums2 {
        used, exists := m[v]
        if exists && !used {
            res = append(res, v)
            m[v] = true
        }
    }

    return res
}

day14 2024-03-11

791. Custom Sort String

You are given two strings order and s. All the characters of order are unique and were sorted in some custom order previously.

Permute the characters of s so that they match the order that order was sorted. More specifically, if a character x occurs before a character y in order, then x should occur before y in the permuted string.

Return any permutation of s that satisfies this property.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0311fLXVhVrYfXgA.png

题解

本题初始想先扫描s字符串, 使用一个map记录字符串中各个字符的数量, 再遍历order依次将字符按数量附加到末尾即可. 但考虑到字符只有26个小写英文字母, 使用一个长度为26的数组来保存对应位置的英文字母的数量. 再遍历要比map速度快.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func customSortString(order string, s string) string {
    a := 'a'
    result := ""
    numbers := make([]int, 26)
    for _, character := range s{
        numbers[character-a]++
    }
    for _,c := range order{
        temp := numbers[c-a]
        for i:=0;i<temp;i++{
            numbers[c-a]--
            result += string(c)
        }
    }
    for key,c := range numbers{
        if c!=0{
            for i:=0;i<c;i++{
                result += string(rune(int(a)+key))
            }
        }
    }
    return result
}

总结

注意题中条件, 遍历的类型有限的情况下直接使用数组保存, 遍历起来速度要快得多.

day15 2024-03-12

1171. Remove Zero Sum Consecutive Nodes from Linked List

Given the head of a linked list, we repeatedly delete consecutive sequences of nodes that sum to 0 until there are no such sequences.

After doing so, return the head of the final linked list. You may return any such answer.

(Note that in the examples below, all sequences are serializations of ListNode objects.)

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0312F5f5ZKGJQi7I.png

题解

本题有一定难度, 寻找连续的和为0的连续序列要使用前缀和, 若某两个元素的前缀和相同, 则位于二者之间的序列和即为0. 删除这部分序列. 先遍历链表, 找到所有前缀和相同的元素, 然后将其中间的部分全部删除即可. 使用一个map来保存前缀和对应的Node的指针, 当找到一个前缀和相同的Node时, 从map保存的指针开始删除到该Node的所有节点, 同时删除以中间部分节点的前缀和为key的map中对应的项防止后面有和中间部分项相同的前缀和的节点存在.

代码

 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
v/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */


func removeZeroSumSublists(head *ListNode) *ListNode {
    current := head
    sum := 0
    summap := map[int](*ListNode){}
    for current != nil{
        sum += current.Val
        if sum == 0{
            for head != current.Next{
                sum += head.Val
                delete(summap, sum)
                head = head.Next
            }
        }
        ptr, exist := summap[sum]
        if !exist{
            summap[sum] = current
        }else{
            back := ptr
            ptr = ptr.Next
            for ptr != current{
                sum += ptr.Val
                delete(summap, sum)
                ptr = ptr.Next
            }
            sum += ptr.Val
            back.Next = current.Next
        }
        current = current.Next
    }
    return head

}

总结

删除中间部分节点的前缀和对应的key的项时, 考虑到中间部分的和一定为0, 因此用sum去累加中间部分节点的值并依次删除, 最后得到的sum就和删除节点开始前的sum相同. 本题一是要清楚通过前缀和来寻找连续的和为0的序列, 另一方面则是时刻记住这个序列的和为0的特性. 其实本题有一种代表元的抽象思想. 可以将一组和为0的序列看为一个, 其在加和过程中与不存在的节点具有等价性. 不影响和的变化.

day16 2024-03-13

2485. Find the Pivot Integer

Given a positive integer n, find the pivot integer x such that:

The sum of all elements between 1 and x inclusively equals the sum of all elements between x and n inclusively. Return the pivot integer x. If no such integer exists, return -1. It is guaranteed that there will be at most one pivot index for the given input.

Example 1:

Input: n = 8 Output: 6 Explanation: 6 is the pivot integer since: 1 + 2 + 3 + 4 + 5 + 6 = 6 + 7 + 8 = 21. Example 2:

Input: n = 1 Output: 1 Explanation: 1 is the pivot integer since: 1 = 1. Example 3:

Input: n = 4 Output: -1 Explanation: It can be proved that no such integer exist.

题解

本题使用数学方法进行计算 https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0313p3Pvfwnneeh8.png 根据公式直接求出x的值即可, 注意考虑到精度问题, 要对最终得到的运算结果与取整后的数字的差的绝对值进行判断, 小于一定限度即可认为是整数值.

代码

1
2
3
4
5
6
7
8
9
func pivotInteger(n int) int {
    n_float := float64(n)
    x := n_float*math.Sqrt(n_float+1)/math.Sqrt(2*n_float)
    if math.Abs(x-float64(int(x))) < 0.0000001{
        return int(x)
    }else {
        return -1
    }
}

总结

查看他人解法发现本题其实也可以使用前缀和进行求解, 将每个下标位置处的前缀和求出并保存, 倒序遍历数组, 将后面的数的和与当前位置的前缀和比较即可, 示例代码

day17 2024-03-14

930. Binary Subarrays With Sum

Given a binary array nums and an integer goal, return the number of non-empty subarrays with a sum goal.

A subarray is a contiguous part of the array.

Example 1:

Input: nums = [1,0,1,0,1], goal = 2 Output: 4 Explanation: The 4 subarrays are bolded and underlined below: [1,0,1,0,1] [1,0,1,0,1] [1,0,1,0,1] [1,0,1,0,1] Example 2:

Input: nums = [0,0,0,0,0], goal = 0 Output: 15

题解

本题可以用前缀和求解, 类似这种求数组中连续和的问题都可以用前缀和求解, 首先计算出所有位置上的前缀和, 使用滑动窗口遍历前缀和数组, 根据goal的值调整窗口的左右边界, 注意0需要特殊处理, 0是否存在不影响整个序列的和, 0所在的位置处的前缀和和前面的元素相同.

代码

 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
func numSubarraysWithSum(nums []int, goal int) int {

    sum := 0

    prefixsum := []int{0}

    for _,value := range nums{

        sum += value

        prefixsum = append(prefixsum, sum)

    }

    front := 1

    back := 0

    flag := 0

    result := 0

    for front < len(prefixsum){

        if prefixsum[front] - prefixsum[back]<goal{

            front++

        }else if prefixsum[front] - prefixsum[back]>goal{

            if front-1 == back{

                front++

            }

            back++

        }else{

            if (front-1 == back){

                result++

                front++

                back = flag

            }else if prefixsum[back] == prefixsum[back+1]{

                result++

                back++

            }else if (front<len(prefixsum)-1) && prefixsum[front] == prefixsum[front+1]{

                result++

                front++

                back = flag

            }else{

                result++

                back++

                flag=back

            }

        }

    }

    return result

}

总结

本题使用滑动窗口时间复杂度上比较高, 考虑在滑动窗口的过程中, 连续的0的部分被重复遍历, 大大增加了总体的运行时间. 其实本题只需要用当前的前缀和减去goal, 并寻找前面的前缀和中是否有符合差值的前缀和存在, 存在则从该前缀和位置到当前前缀和位置直接的序列满足和为goal的条件. 已经求出了前缀和就没必要再去一个个遍历并且滑动了, 如果使用滑动窗口则没必要计算前缀和. 我的解法实际上是对前缀和理解不深刻导致的. 巧用前缀和加哈希表(存储某个前缀和出现的次数)可以快速的解决这个问题. 示例代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func numSubarraysWithSum(nums []int, goal int) int {
    hash := map[int]int{}
    sum := 0
    count := 0
    hash[0] = 1
    for i:=0; i < len(nums); i++ {
        sum = sum + nums[i]
        count = count + hash[sum - goal]
        val, ok := hash[sum]
        if(ok) {
            hash[sum] = val + 1
        } else {
            hash[sum] = 1
        }
    }
    return count
}

还有一种极其巧妙的解法, 分别求得和小于等于goal的连续子序列的数量, 再减去和小于等于goal-1的连续子序列的数量即为最终结果. 本题中求子序列等于goal是比较困难的, 要考虑很多条件, 但是求小于等于goal的子序列却是比较简单的问题. 这种方法将一个困难问题转化为两个简单的子问题求解, 得到了更高效的方法. 充分利用整体性可使问题更简单. 代码如下

 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
func numSubarraysWithSum(nums []int, goal int) int {

	return counting(nums, goal) - counting(nums, goal-1)

}

func counting(nums []int, goal int) int {

	if goal < 0 {

		return 0

	}

	left, right, sum, ans := 0, 0, 0, 0

	for ; right < len(nums); right++ {

		sum += nums[right]

		for left <= right && sum > goal {

			sum -= nums[left]

			left++

		}

		ans += right - left + 1

	}

	return ans

}

day18 2024-03-15

238. Product of Array Except Self

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].

The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

You must write an algorithm that runs in O(n) time and without using the division operation.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0315l01oAnZceg6j.png

题解

题目中明确要求本题不能使用分治法, 且算法复杂度在O(n). 本题要求的为数组中每个元素对应的除该元素之外的其他元素的乘积. 根据给出的例子可以发现当存在0的时候是一种特殊情况. 当存在0时除0以外的其他元素对应的结果均为0. 一般情况下可以使用先求出全部元素的乘积, 再遍历数据使用总乘积除以当前元素即可求得对应位置的结果. 因为这样只需要固定遍历两次数据, 故时间复杂度为O(n), 又只需要一个变量来保存总乘积, 一个变量指示是否存在0元素, 故空间复杂度为O(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
func productExceptSelf(nums []int) []int {
    sum := 1
    flag := 0
    for _, value := range nums{
        if value == 0{
            flag++
        }else{
            sum *= value
        }
    }
    if flag > 1{
        return make([]int, len(nums))
    }
    if flag == 1{
        result := []int{}
        for _, value := range nums{
            if value == 0{
                result = append(result, sum)
            }else{
                result = append(result, 0)
            }
        }
        return result
    }
    result := []int{}
    if flag == 0{
        for _, value := range nums{
            result = append(result, sum/value)
        }

    }
    return result
}

总结

查看更快的解法, 发现都使用了前缀积和后缀积, 即从前向后遍历, 计算出当前位置元素的前缀积, 然后反向遍历, 在计算出后缀积的同时就得到了最终的结果. 一个示例代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func productExceptSelf(nums []int) []int {
	res := make([]int, len(nums))

	prefix := 1
	for i, n := range nums {
		res[i] = prefix
		prefix *= n
	}

	postfix := 1
	for i := len(nums) - 1; i >= 0; i-- {
		res[i] *= postfix
		postfix *= nums[i]
	}
	return res
}

其实无论是前缀和还是前缀积, 都是一个对以往的计算状态的保留, 保存了更多的信息, 避免了重复的运算, 这种思想是值得细细品味的.

day19 525. Contiguous Array

Given a binary array nums, return the maximum length of a contiguous subarray with an equal number of 0 and 1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0316T5HJJzrn5slt.png

题解

考虑本题若想找到到某一个位置处的0和1数量相同的最长子数组长度, 只需要根据到该下标处的0和1数量的差值, 找出符合这个差值的最小下标, 用当前下标减去这个差值下标, 得到的即为0和1数量相同的子数组的长度. 关键在于想要让0和1数量相同, 需要找出能消除当前0和1数量差值的位置. 0和1数量对于寻找相同数量子数组用处不大, 二者数量的差值对于构造一个数量相同的子数组至关重要. 通过加上或减去二者的数量差即可构造出二者数量相同的子数组. 考虑清楚这一点, 思路就很清晰了.

  1. 遍历数组, 保存到当前位置0和1的数量
  2. 计算二者的差值, 在一个哈希表中寻找以这个差值为key的项是否存在, 不存在则将差值为key, 当前下标作为该key对应的值插入哈希表. 若存在则用当前下标减去哈希表中以差值作为key的对应的下标值, 即得到到当前位置0和1数量相同的最长子数组的长度. 比较这个长度和保存的最长子数组长度, 更新最长子数组长度.

关键在与将差值作为key下标作为value插入哈希表后, 后续有相同的差值作为key时不在更新哈希表, 这样保存的就是符合这个差值的位置的最小值, 也就能构造出最长的0和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
func findMaxLength(nums []int) int {
    map0 := map[int]int{}
    map1 := map[int]int{}
    sum0 := 0
    sum1 := 0
    maxarray := 0
    for index,value := range nums{
        if value == 1{
            sum1++
        }else{
            sum0++
        }
        if sum1>sum0{
            i, ok := map1[sum1-sum0]
            if !ok{
                map1[sum1-sum0] = index
            }else{
                maxarray = max(maxarray, index-i)
            }
        }else if sum1<sum0{
            i, ok := map0[sum1-sum0]
            if !ok{
                map0[sum1-sum0] = index
            }else{
                maxarray = max(maxarray, index-i)
            }
        }else{
            maxarray = max(maxarray, index+1)
        }
    }
    return maxarray
}

总结

本题和前几天的题目在思想上有异曲同工之妙, 也是前缀和的一种应用, 如果把0和1的数量差作为前缀和, 那么本题解题思路可简单概括为找到前缀和相等的位置的最远距离.

day20 2024-03-17

57. Insert Interval

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.

Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).

Return intervals after the insertion.

Note that you don’t need to modify intervals in-place. You can make a new array and return it.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/03179SRnh9piq8gy.png

题解

遍历intervals, 将interval复制到结果数组中. 判断要插入的interval的start和end是否在当前interval中

  1. 在范围内则将新interval的start设置为当前interval的start. 使用相同的方式判断end的范围.
  2. 若start或end不在当前interval范围内且小于下一个interval的start, 则将start或者end设置为新interval的start或end. 此时新interval构造完成, 将后面的interval原样复制到result中即可.

代码

 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
func insert(intervals [][]int, newInterval []int) [][]int {
    result := [][]int{}
    start := newInterval[0]
    end := newInterval[1]
    insertInterval := []int{}
    flag := 0
    for _, value := range intervals{
        if flag == 2{
            result = append(result, value)
        }else if flag == 0{
            if start < value[0]{
                insertInterval = append(insertInterval, start)
                flag++
                if end < value[0]{
                    insertInterval = append(insertInterval, end)
                    flag++
                    result = append(result, insertInterval)
                    result = append(result, value)
                }else if end >= value[0] && end <= value[1]{
                    insertInterval = append(insertInterval, value[1])
                    flag++
                    result = append(result, insertInterval)
                }
            }else if start >= value[0] && start <= value[1]{
                insertInterval = append(insertInterval, value[0])
                flag++
                if end >= value[0] && end <= value[1]{
                    insertInterval = append(insertInterval, value[1])
                    flag++
                    result = append(result, insertInterval)
                }
            }else{
                result = append(result, value)
            }
        }else{
            if end < value[0]{
                insertInterval = append(insertInterval, end)
                flag++
                result = append(result, insertInterval)
                result = append(result, value)
            }else if end >= value[0] && end <= value[1]{
                insertInterval = append(insertInterval, value[1])
                flag++
                result = append(result, insertInterval)
            }
        }
    }
    if flag == 0{
        result = append(result, []int{start, end})
    }else if flag == 1{
        insertInterval = append(insertInterval, end)
        result = append(result, insertInterval)
    }
    return result
}

总结

这种题思路上并没有特别之处, 但是判断逻辑比较繁琐, 需要耐心思考边界情况, 查看他人题解, 发现用原始数组与需要插入的interval做比较要比使用interval和原始数组的start和end做比较思路简单清晰得多, 这里还是对判断条件的变与不变理解的不够透彻, 需要插入的interval的start和end是不变的, 不断遍历原始数组与不变的start和end做比较要比使用不变的start和end去和不断变化的interval的start和end做比较判断起来容易得多. 找到不变的条件对于思路清楚的解决问题至关重要. 给出示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func insert(intervals [][]int, newInterval []int) [][]int {

    res := make([][]int, 0)

    i := 0

    for ; i < len(intervals) && intervals[i][1] < newInterval[0]; i++ {
        res = append(res, intervals[i])
    }

    for ; i < len(intervals) && intervals[i][0] <= newInterval[1]; i++ {
        newInterval[0] = min(intervals[i][0], newInterval[0])
        newInterval[1] = max(intervals[i][1], newInterval[1])
    }

    res = append(res, newInterval)

    for i < len(intervals) {
        res = append(res, intervals[i])
        i++
    }

    return res
}

day21 2024-03-18

452. Minimum Number of Arrows to Burst Balloons

There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D integer array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.

Arrows can be shot up directly vertically (in the positive y-direction) from different points along the x-axis. A balloon with xstart and xend is burst by an arrow shot at x if xstart <= x <= xend. There is no limit to the number of arrows that can be shot. A shot arrow keeps traveling up infinitely, bursting any balloons in its path.

Given the array points, return the minimum number of arrows that must be shot to burst all balloons.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/03188j7wiR06A45M.png

题解

本题描述的很复杂, 其实是寻找相互交错的数组有几组, 如果将数组的范围看作集合, 范围重叠的数组可以看作一个大集合, 那么就是寻找这样的集合的数目. 本题可用贪心算法, 先将这些数组按照x_start的大小从小到大排序, 然后遍历并寻找x_start在当前重合范围内的数组,并且将重合范围设置为当前重合范围和当前遍历的数组的x_end中的较小值以缩小重合范围. 直到当前数组不满足条件, 即可认为之前的数组需要一个arrow. 继续遍历, 重复该操作, 即可找到所有需要的arrow.

代码

 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
func findMinArrowShots(points [][]int) int {
    sort.Slice(points, func (i, j int)bool{
        if points[i][0]<points[j][0]{
            return true
        }else{
            return false
        }
    })
    arrow := 0
    current := []int{points[0][0],points[0][1]}
    for _, value := range points{
        if value[0] <= current[1]{
            if value[1] < current[1]{
                current[1] = value[1]
            }
            current[0] = value[0]
        }else{
            arrow++
            current[0] = value[0]
            current[1] = value[1]
        }

    }
    arrow++
    return arrow
}

总结

本题的题目说明上有一些含糊, 根据题目说明, arrow只在竖直方向上移动, 也就是说, 必须要有重合的区间的数组才能用一个箭头, 假如有一个数组区间很大, 其和两个小区间重合但这两个小区间不重合, 按理来说应该使用两个arrow才能扎爆这三个气球, 但是看他人提交的代码中有如下一种代码, 似乎说明用一个arrow就可以. 究竟这种对不对有待进一步的思考. 代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func findMinArrowShots(points [][]int) int {
	sort.Slice(points, func(i,j int) bool {
        return points[i][1] < points[j][1]
    })
    res := 1
    arrow := points[0][1]
    for i := 0; i < len(points); i++ {
        if arrow >= points[i][0] {continue}
        res++
        arrow = points[i][1]
    }
    return res
}

day22 2024-03-19

621. Task Scheduler

You are given an array of CPU tasks, each represented by letters A to Z, and a cooling time, n. Each cycle or interval allows the completion of one task. Tasks can be completed in any order, but there’s a constraint: identical tasks must be separated by at least n intervals due to cooling time.

Return the minimum number of intervals required to complete all tasks.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/031901VKFQ0HNGWv.png

题解

拿到本题, 最直接的想法就是将每种任务的数量统计出来, 从大到小排序, 然后按照冷却时间轮流按序执行不同的任务, 不能执行任务的时间片留空. 某种任务全部执行完后, 该任务占据的时间片也留空. 知道全部任务都执行完需要的时间就是最少时间. 这是一种贪心的思想, 简单思考其正确性, 因为即使调换顺序, 在执行到某个时间时也不会比这种贪心算法执行的任务更多.

代码

 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
func leastInterval(tasks []byte, n int) int {
    tasknumber := map[byte]int{}
    for _, task := range tasks{
        _, exist := tasknumber[task]
        if !exist{
            tasknumber[task] = 1
        }else{
            tasknumber[task]++
        }
    }
    tasknumber_slice := []int{}
    for _, value := range tasknumber{
        tasknumber_slice = append(tasknumber_slice, value)
    }
    sort.Ints(tasknumber_slice)
    length := 0
    result := 0
    for {
        length = len(tasknumber_slice)
        for i:=1;i<=n+1;i++{
            if i<=length{
                if(tasknumber_slice[length-i] == 1){
                    if i==1{
                        tasknumber_slice = tasknumber_slice[:length-1]
                    }else{
                        tasknumber_slice = append(tasknumber_slice[:length-i],tasknumber_slice[length-i+1:]...)
                    }

                }else{
                    tasknumber_slice[length-i]--
                }
            }
            result++
            if len(tasknumber_slice)==0{
                goto Loop
            }
        }
        sort.Ints(tasknumber_slice)
    }
    Loop:
    return result
}

总结

看了0ms的解法, 十分惊艳, 与其将任务一个一个的安放到插槽中, 不如直接按照频率最大的任务算出必须存在的空插槽的个数, 再用剩余的任务去填这些空插槽, 最后只需要将任务总数和剩余的空插槽个数相加即可得到最终的时长. 到这里我想到, 其实频率最高的任务留出的空插槽数目是固定的, 只要将除频率最高之外的任务总数和空插槽数目相比, 若小于空插槽数目, 则最后时长就是频率最高任务完成需要的时长. 这里需要将和频率最高的任务频率相同的任务数目先减一计算算到任务总数中, 最后再加到最终的时间总数上. 若大于空插槽数目, 最终结果就是任务数目.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func leastInterval(tasks []byte, n int) int {
	freq := make([]int, 26)
	for _, task := range tasks {
		freq[task-'A']++
	}
	sort.Ints(freq)

	maxFreq := freq[25]
	idleSlots := (maxFreq - 1) * n

	for i := 24; i >= 0 && freq[i] > 0; i-- {
		idleSlots -= min(maxFreq-1, freq[i])
	}
	idleSlots = max(0, idleSlots)

	return len(tasks) + idleSlots
}

day23 2024-03-20

1669. Merge In Between Linked Lists

You are given two linked lists: list1 and list2 of sizes n and m respectively.

Remove list1’s nodes from the ath node to the bth node, and put list2 in their place.

The blue edges and nodes in the following figure indicate the result:

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0320kDnwlxpwmKhB.png

Build the result list and return its head.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/03200deJDZCQpgs3.png

题解

本题为将链表中某段替换为指定的链表, 只需要遍历链表, 保存需要替换部分首尾两个节点的指针即可, 需要注意边界情况的处理, 即替换开头某段或者结尾某段链表时要处理空指针.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeInBetween(list1 *ListNode, a int, b int, list2 *ListNode) *ListNode {
	count := 0
	var before *ListNode
	var after *ListNode
	current := list1
	head := list1
	for count <= b {
		if count == a-1 {
			before = current
		}
        count++
		current = current.Next
	}
	after = current
	if before == nil {
		head = list2
	} else {
		before.Next = list2
	}
	current = list2
	for current.Next != nil {
		current = current.Next
	}
	current.Next = after
    return head
}

总结

本题保存了首尾两个指针的地址, 是典型的用空间换时间的思路, 不过实际应用过程中可能还要根据语言注意被动态分配出去的空间回收的问题.

day24 2024-03-21

206. Reverse Linked

Given the head of a singly linked list, reverse the list, and return the reversed list.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0321QsvliuLwsiPu.png

题解

本题相当基础, 是一道简单题, 只需要将链表整个反转过来即可. 用一个变量保存当前访问的节点的前一个节点的指针, 将当前节点的next修改为指向前一个节点的指针, 遍历整个链表即可. 注意处理边界情况(头指针为0).

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reverseList(head *ListNode) *ListNode {
    var before *ListNode
    current := head
    for current!=nil && current.Next!=nil {
        temp := current.Next
        current.Next = before
        before = current
        current = temp
    }
    if head!=nil{
        current.Next = before
    }
    return current
}

总结

这两天都是链表相关的题目, 本题是一道基础题, 在查看他人解答的过程中发现本题也可以将链表数据全部复制出来, 再遍历链表逆序赋值即可. 示例代码如下

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reverseList(head *ListNode) *ListNode {
    arr := []int{}

	node := head

	for node != nil {
		arr = append(arr, node.Val)
		node = node.Next
	}

	node = head
	for i := len(arr); i > 0; i-- {
		node.Val = arr[i-1]
		node = node.Next
	}

	return head
}

day25 2024-03-22

234. Palindrome Linked

Given the head of a singly linked list, return true if it is a palindrome or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/03220NcGLgVTEmnC.png

题解

本题也是一道基础题, 使用快慢指针的方法遍历到链表的中间, 在遍历的同时将链表的前半部分的值保存到数组中, 再从中间继续向后遍历, 遍历的同时反向遍历数组, 比较遍历的节点和遍历到的数组中元素的值. 若不同则不是回文, 直到全部遍历完成为止.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func isPalindrome(head *ListNode) bool {
    back := head
    before:= head
    if head.Next != nil{
        before = head.Next
    }else{
        return true
    }
    values := []int{head.Val}
    for before!= nil && before.Next != nil{
        back = back.Next
        values = append(values, back.Val)
        before = before.Next.Next
    }
    if before == nil{
        for i:=len(values)-2;i>=0;i--{
            back = back.Next
            if back.Val != values[i]{
                return false
            }
        }
        return true
    }else{
        for i:= len(values)-1;i>=0;i--{
            back =back.Next
            if back.Val != values[i]{
                return false
            }
        }
        return true
    }
}

总结

查看用时较短的题解, 使用了快慢指针, 找到中间位置后将后半截链表反转, 然后从原始链表头部和反转的后半截链表头部开始依次遍历并比较即可. 这种方法时间复杂度为O(n), 空间复杂度为O(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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func isPalindrome(head *ListNode) bool {


    slow:=head
    fast:=head.Next

    for fast!=nil && fast.Next!=nil{
        slow=slow.Next
        fast=fast.Next.Next
    }

    second:=slow.Next
    slow.Next=nil

    second=reverse(second)

    for second!=nil && head!=nil{


        if second.Val!=head.Val{
            return false
        }
        second=second.Next
        head=head.Next
    }
    return true

}

func reverse(head *ListNode) *ListNode{

    var prev *ListNode
    var futr *ListNode

    for head!=nil{
        futr=head.Next
        head.Next=prev
        prev=head
        head=futr
    }

    return prev
}

day26 2024-03-23

143. Reorder

You are given the head of a singly linked-list. The list can be represented as:

L0 → L1 → … → Ln - 1 → Ln

Reorder the list to be on the following form:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

You may not modify the values in the list’s nodes. Only nodes themselves may be changed.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0323xGTn0liJdHDU.png

题解

拿到本题, 首先想到的是将链表中的全部数据保存到数组中, 然后通过同时从前后遍历数组并且给原来的链表赋值的方式, 即可快速解决本题, 如果不使用额外的空间, 可以使用快慢指针, 找到链表的中间节点, 从中间节点开始将链表的后半部分反向, 用两个指针分别从前半部分的开始和后半部分的末尾开始遍历, 并构造新链表即可. 也可以构造一个栈, 将链表的后半部分放入栈中, 然后从顶端一边出栈一边构造新链表即可. 题目中要求不能直接修改节点的值, 因此采用第三种思路.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reorderList(head *ListNode)  {
    slow := head
    fast := head
    if head.Next != nil{
        fast = head.Next
    }
    stack := []*ListNode{}
    for fast != nil && fast.Next != nil{
        fast = fast.Next.Next
        slow = slow.Next
    }
    odd := false
    if fast  == nil{
        odd = true
    }
    // construct pointer stack
    slow = slow.Next
    for slow != nil{
        stack = append(stack, slow)
        slow = slow.Next
    }
    slow = head
    temp := head.Next
    temp2 := head
    flag := 1
    for i:=len(stack)-1;i>=0;i--{
        if flag == 1{
            stack[i].Next = nil
            slow.Next = stack[i]
            slow = slow.Next
            flag = 0
        }else{
            temp2 = temp.Next
            temp.Next = nil
            slow.Next = temp
            slow = slow.Next
            flag = 1
            i++
            temp = temp2
        }
    }
    if odd{
        temp.Next = nil
        slow.Next = temp
    }
    return
}

总结

查看最快速度代码, 使用了一个数组先将链表的全部节点指针留一个间隔存放在数组中, 然后再逆序遍历数组将后半节点的指针逆序插入前面留出的空格中, 最后从头遍历数组, 连接整个链表即可. 这样写得出的代码比较简洁.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reorderList(head *ListNode) {
	list := []*ListNode{}

	for node := head; node != nil; node = node.Next {
		list = append(list, node)
		list = append(list, nil)
	}

	for i := 1; i < len(list) - i - 1; i += 2 {
		list[i] = list[len(list) - i - 1]
	}

	for i := 0; list[i] != nil; i++ {
		list[i].Next = list[i + 1]
	}
}

day27 2024-03-24

287. Find the Duplicate Number

Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive.

There is only one repeated number in nums, return this repeated number.

You must solve the problem without modifying the array nums and uses only constant extra space.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0324Ckibjiatyqu7.png

题解

根据鸽笼原理, 显然必有一个数字有一个重复数字. 最简单的思路就是用每个数和数组其余数字比较, 直到找到相同的数为止, 显然这个方法的时间复杂度比较高, 要O(n^2). 最终并没有想出题目中要求的时间复杂度为O(n), 空间复杂度为O(1)的解法, 看题解得知使用的是Floyd 循环检测算法, 这个算法一般用于检测链表中是否存在环, 即使用快慢指针, 同时遍历链表, 如果存在环, 快指针最终会追上慢指针. 如果链表没有环, 则遍历链表最终会到达空指针自然停止, 这里的快慢指针是给了未知长度的链表一个停止条件, 可以避免若链表有环无限循环遍历下去. 这里将数组堪称链表是非常精妙的思路, 将数组中的值看作下一个节点在数组中的下标, 这样如果有两个节点相同, 则最终快指针和慢指针指向的下标会相同, 再从头遍历一次数组, 找到值为这个下标的数据, 即为重复的数.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func findDuplicate(nums []int) int {
    // Step 1: Find the intersection point of the two pointers
    slow := nums[0]
    fast := nums[0]

    for {
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast {
            break
        }
    }

    // Step 2: Find the entrance to the cycle (duplicate number)
    slow = nums[0]
    for slow != fast {
        slow = nums[slow]
        fast = nums[fast]
    }

    return slow
}

day28 2024-03-25

442. Find All Duplicates in an Array

Given an integer array nums of length n where all the integers of nums are in the range [1, n] and each integer appears once or twice, return an array of all the integers that appears twice.

You must write an algorithm that runs in O(n) time and uses only constant extra space.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/03250qkp0rMB0Ms8.png

题解

本题若想在O(n)时间复杂度内求解, 必须在遍历数组的时候充分利用遍历过的数据提供的信息, 在后续充分利用已有信息, 在本题中, 遍历过程中的每个位置处的数本身就是信息. 若使用O(n)的空间复杂度, 可以创建一个和目标数组长度相同的空数组, 将遍历到的元素的数作为下标, 设定对应空数组中下标所在位置的值作为标记. 这样后续遍历时看到标记则知道这个数是重复的. 若想空间复杂度为O(1), 因为本题并没有要求不修改数组, 可以修改数组中以这个数为下标处的数据, 这里可以将其取相反数来表明这个下标已经出现过. 这也是因为体重明确说明数组中的数的范围在1-数组长度之间, 因此可以采用这种方法来标记数据是否已经出现, 如果有数大小超过数组长度, 这种方案就不适用了, 则必须使用O(n)的空间.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func findDuplicates(nums []int) []int {
    result := []int{}
    for _, values := range nums{
        if nums[int(math.Abs(float64(values)))-1] < 0{
            result = append(result, int(math.Abs(float64(values))))
        }
        nums[int(math.Abs(float64(values)))-1] = -nums[int(math.Abs(float64(values)))-1]
    }
    return result
}

day28 2024-03-26

41. First Missing Positive

Given an unsorted integer array nums. Return the smallest positive integer that is not present in nums.

You must implement an algorithm that runs in O(n) time and uses O(1) auxiliary space.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0326ratU6vVn9qLn.png

题解

本题要求O(n)的时间复杂度和O(1)的空间复杂度, 解题思路上与昨天的题目相似, 考虑到数组长度为n, 即使是从1开始的连续正整数, 最大填充到n. 故用数组中的数作为下标修改对应位置的值来标记某个数是否存在是可行的. 但要注意, 当前数组中可能出负数和超过n的数字, 如果想使用将数作为下标将对应位置数设为负数的方式来提供信息, 则要先对负数和超过n的数字进行处理, 为了区分被标记的数和本身为负的数, 将负数和超过n的数修改为0. 再遍历数组按照昨天的思路将数作为下标, 将相应位置数设为负数标记已经访问. 这里可以将数全部设为-1. 最后再遍历一遍数组, 找到第一个数字大于等于0的数的下标, 即为丢失的最小正整数. 这样需要固定遍历三遍数组, 且空间复杂度为O(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
func firstMissingPositive(nums []int) int {
	for index, value := range nums {
		if value < 0 || value > len(nums) {
			nums[index] = 0
		}
	}
	flag := 0
	for _, value := range nums {
		if abs(value) > 1 || value == -1{
			if nums[abs(value)-1] == 1 {
				flag = 1
				nums[abs(value)-1] = -1
			} else if nums[abs(value)-1] == 0 {
				nums[abs(value)-1] = -1
			} else if nums[abs(value)-1] > 1 {
				nums[abs(value)-1] = -nums[abs(value)-1]
			}
		}else if value == 1{
            flag = 1
            if nums[0] == 0{
                nums[0] = -1
            }else if nums[0] > 1{
                nums[0] = -nums[0]
            }
        }
	}
	for index, value := range nums {
		if flag == 1 && index == 0 {
			continue
		} else if index == 0 && flag == 0 {
			return 1
		}
		if value >= 0 {
			return index + 1
		}
	}
	return len(nums)+1
}

func abs(num int) int {
	if num < 0 {
		return -num
	} else {
		return num
	}
}

总结

查看最快速代码, 其将负数直接修改为整数类型最大值, 对于超过数组长度的数直接忽略, 不作处理, 其余的当作下标取对应位置的相反数. 这样处理起来思路比较清晰.

day29 2024-03-27

713. Subarray Product Less Than K

Given an array of integers nums and an integer k, return the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than k.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0327Kf6RehBJGKTs.png

题解

本题要求返回的是连续的相邻子数组, 第一个想到的就是滑动窗口, 设置窗口的前后指针, 当窗口内的积<k时, 扩大窗口, 并增加计数, 当窗口内的积≥k时, 缩小窗口, 直到重新满足<k的条件为止. 这里增加计数的地方需要仔细考量, 并不是扩大窗口且积<k时, 就将计数加一, 因为该题是求积, 所以当两个数的积<k时, 其实已经包含了每个数都小于k在其中, 同理, 当三个数积<k时, 包含了任意两个的组合积都<k, 而本题只要求连续相邻的子序列. 故每次窗口扩大且符合条件时增加的计数应该为当前窗口的长度. (可以自己考虑一下从2个数扩大到3个数时默认新增了几种符合要求的子序列, 应该是包含新增加的数本身, 加上相邻一个数, 加上相邻两个数三种情况, 前面两个数的情况在之前已经计数过了)

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func numSubarrayProductLessThanK(nums []int, k int) int {
    front := 0
    back := 0
    sum := 1
    result := 0
     for front < len(nums){
        sum *= nums[front]
        if sum < k{
            front++
            result += front - back
        }else{
            if front == back{
                front++
                back++
                sum = 1
                continue
            }
            sum /= nums[back]
            sum /= nums[front]
            back++
        }
     }
     return result
}

总结

在查看速度更快的题解代码时, 发现可以通过内层循环一直缩小窗口到<k时, 才继续向下滑动窗口, 这样外层只需要不断向下遍历来扩大窗口即可. 这样写循环思路更清晰, 代码更简洁 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func numSubarrayProductLessThanK(nums []int, k int) int {
    if k <= 1 {
        return 0
    }
    res := 0

    p := 1
    j := 0

    for i := 0; i < len(nums); i++ {
        p *= nums[i]
        for p >= k {
            p /= nums[j]
            j++
        }
        res += i -j + 1
    }

    return res
}

想到这, 忽然明白, 其实核心在于只需要考虑以某个位置为结尾的向前连续符合要求的数组长度作为该位置处应该增加的计数数目即可. 这样就把整个问题拆分成了独立不关联的小问题. 将每个位置应该增加的计数数目累积, 就是最终的结果.

day30 2024-03-28

2958. Length of Longest Subarray With at Most K Frequency

You are given an integer array nums and an integer k.

The frequency of an element x is the number of times it occurs in an array.

An array is called good if the frequency of each element in this array is less than or equal to k.

Return the length of the longest good subarray of nums.

A subarray is a contiguous non-empty sequence of elements within an array. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0328cwzoUxeYLcAx.png

题解

拿到本题, 直接思路就是使用滑动窗口, 使用一个哈希表来存储每个数字对应的个数, 在滑动的过程中, 扩大窗口时增加窗口新增数字的计数并不断更新最大长度, 直到当前数字的计数达到上限k开始缩小窗口, 缩小至当前数字计数小于k(缩小过程中的数都在哈希表中对应减掉频率)即可继续扩大窗口. 如此直到遍历完整个数组. 其实从解题思路上与昨天的题内核是十分相似的.

代码

 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
func maxSubarrayLength(nums []int, k int) int {
    count := map[int]int{}
    end := 0
    maxlength := 0
    for front:=0;front < len(nums);front++{
        _,exist := count[nums[front]]
        if !exist{
            count[nums[front]] = 1
        }else{
            count[nums[front]]++
        }

        if count[nums[front]] <= k{
            maxlength =  max(maxlength,front-end+1)
        }else{
            for nums[end] != nums[front]{
                count[nums[end]]--
                end++
            }
            // 将达到上限的数本身减掉
            count[nums[end]]--
            end++
        }
    }
    return maxlength
}

总结

查看前面10%更快的代码, 发现判断缩小窗口的结束可以用当前窗口前端达到上限的数在哈希表中对应的计数值来判断, 缩小窗口直到达到上限的数的计数值小于k即可结束, 这样整体代码更清晰, 更简洁, 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func maxSubarrayLength(nums []int, k int) int {
    m := make(map[int]int)
    res := 0
    for l, r := 0, 0; r < len(nums); r++ {
        m[nums[r]]++
        for m[nums[r]] > k {
            m[nums[l]]--
            l++
        }

        if r-l+1 > res {
            res = r-l+1
        }
    }

    return res
}

day31 2024-03-29

2962. Count Subarrays Where Max Element Appears at Least K Times

You are given an integer array nums and a positive integer k.

Return the number of subarrays where the maximum element of nums appears at least k times in that subarray.

A subarray is a contiguous sequence of elements within an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0329pKR0itEdYZ5b.png

题解

本题感觉就是昨天那道题的姊妹+升级版, 现在要求找到数组中包含的最大值, 其在这个子数组中的出现次数至少为k次. 这里明确题目中需要解决的两个问题. 1. 找到数组中的最大值 2. 对这个值在子序列中出现的次数进行计数. 无疑, 仍然可以使用滑动窗口来解决, 思路和昨天的题类似, 这里在扩大窗口时, 一直扩大到最大值数目为k, 继续向后遍历时要每次增加从头开始到包含k个最大值的窗口的最左端的元素个数. 核心思路在于让滑动窗口中只保留k个最大值, 这样所有包含前面数据的子数组和包含后面不为最大值的子数组的所有子数组都符合条件.

代码

 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
func countSubarrays(nums []int, k int) int64 {
    max := slices.Max(nums)
    var result int64
    left := 0
    nextleft := 0
    beforeadded := 0
    frequency := 0
    for _, value := range nums{
        if value == max{
            frequency++
            if frequency >= k{
                for nums[nextleft] != max{
                    nextleft++
                }
                beforeadded += nextleft - left + 1
                result += int64(beforeadded)
                left = nextleft+1
                nextleft = left
            }
        }else if frequency >= k{
            result += int64(beforeadded)
        }

    }

    return result
}

总结

查看他人的题解发现, 可以在保持窗口内最大值个数为k的思路下优化解题过程, 重复的加前面的元素是不必要的, 先将所有最大值的下标放入数组, 然后确定包含k个最大值的窗口的两端的下标, 将该窗口左侧前面的元素个数和窗口右侧后面的元素个数相乘即为该窗口对应的符合条件的解的个数, 切换到下一个包含k个最大值的窗口, 继续此操作, 直到窗口中最大值数目不足k为止.

 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
func countSubarrays(nums []int, k int) int64 {
	var m int
	for _, n := range nums {
		m = max(m, n)
	}
	var idxs []int
	for i := range nums {
		if nums[i] == m {
			idxs = append(idxs, i)
		}
	}
	start := 0
	count := int64(0)
	for i := range idxs {
		if i+k > len(idxs) {
			break
		}
		last := len(nums)
		if i+k < len(idxs) {
			last = idxs[i+k]
		}
		count += int64(idxs[i]-start+1) * int64(last-idxs[i+k-1])
	}
	return count
}

day32 2024-03-30

992. Subarrays with K Different Integers

Given an integer array nums and an integer k, return the number of good subarrays of nums.

A good array is an array where the number of different integers in that array is exactly k.

For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3. A subarray is a contiguous part of an array. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0330GvIAMJljJeoJ.png

题解

本题仍然使用滑动窗口, 这几天连续做滑动窗口的题, 总结了滑动窗口的"三要素": 1. 什么时候扩大窗口 2. 双么时候缩小窗口 3. 缩小和扩大窗口时执行哪些操作 对于本题, 题目中要求计数正好包含k个不同数的子数组的个数, 求精确包含k个这种问题往往比较困难, 可以转化为求解包含≤k个和≤k-1个不同数的子数组个数的差. 这种思路求解起来非常方便. 对于求解包含≤k个不同数的子数组的个数, 当数组中包含不同数个数不足k时, 扩大窗口同时将计数增加当前窗口的长度, 若为k+1, 则缩小窗口至正好完全去除了一个数(若某个数只出现了1次, 去掉后窗口内不同数个数就是k, 若不止一次, 则去掉了不同数个数也没有变化, 故要继续向下遍历). 最后求≤k和≤k-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
func subarraysWithKDistinct(nums []int, k int) int {

	return countk(nums, k) - countk(nums, k-1)
}

func countk(nums []int, k int) int {
	count := map[int]int{}
	different := 0
	left := 0
	result := 0
	for index, value := range nums {
		_, exist := count[value]
		if !exist {
			different++
			count[value] = 1
		} else {
			count[value]++
		}
		if different <= k {
			result += index - left + 1
		}
		if different == k+1 {
			for count[nums[left]] > 1 {
				count[nums[left]]--
				left++
			}
			delete(count, nums[left])
			left++
			different--
			result += index - left + 1
		}
	}
    return result
}

总结

解题时没有注意题目限制, 后来查看最快解法发现忽略了题目中的数的范围, 题目中的数组中的数的大小不超过数组的长度, 数的范围已知, 因此可以使用数组代替哈希表来计数这样可以大大加快解题速度.

 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
func subarraysWithKDistinct(nums []int, k int) (ans int) {
	f := func(k int) []int {
		n := len(nums)
		pos := make([]int, n)
		cnt := make([]int, n+1)
		s, j := 0, 0
		for i, x := range nums {
			cnt[x]++
			if cnt[x] == 1 {
				s++
			}
			for ; s > k; j++ {
				cnt[nums[j]]--
				if cnt[nums[j]] == 0 {
					s--
				}
			}
			pos[i] = j
		}
		return pos
	}
	left, right := f(k), f(k-1)
	for i := range left {
		ans += right[i] - left[i]
	}
	return
}

day33 2024-03-31

2444. Count Subarrays With Fixed Bounds

You are given an integer array nums and two integers minK and maxK.

A fixed-bound subarray of nums is a subarray that satisfies the following conditions:

The minimum value in the subarray is equal to minK. The maximum value in the subarray is equal to maxK. Return the number of fixed-bound subarrays.

A subarray is a contiguous part of an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0331tqDltORpk8jM.png

题解

本题先求出数组中包含上下界的所有可行子区域, 可行子区域指所有连续的符合上下界要求的最长区域. 将这些区域的头尾下标保存到一个二维数组中. 对于每个可行区域, 使用该区域包含的全部子数组的个数减去不满足上下界要求的子数组个数, 不满足上下界要求的子数组个数求解使用滑动窗口是比较容易的, 从头开始滑动, 窗口内只能包含上界或者下届或者都不包含. 若直接求解该区域中包含的满足上下界的子数组个数则比较困难. 用子数组总个数和不满足上下界要求的子数组个数做差即可得到想求的满足上下界的子数组个数. 对于每个子区域都执行这样的操作, 并将求得的满足条件的子数组个数加和即可得到最终的结果, 注意处理上下界都是同一个数的边界情况.

代码

 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
func countSubarrays(nums []int, minK int, maxK int) int64 {
    begin := 0
    borders := [][]int{}
    mincurrent := -1
    maxcurrent := -1
    var result int64
    // 找到所有可行区间
    for index, value := range nums{
        border := []int{}
        if value == minK {
            mincurrent = 1
        }
        if value == maxK {
            maxcurrent = 1
        }
        if value > maxK || value < minK || index == len(nums)-1{
            if maxcurrent == -1 || mincurrent == -1{
                mincurrent = -1
                maxcurrent = -1
                begin = index + 1
            }else{
                if value <= maxK && value >= minK && index == len(nums) - 1 {
                    index += 1
                }
                border = append(border, begin)
                border = append(border, index)
                borders = append(borders, border)
                border = border[:0]
                mincurrent = -1
                maxcurrent = -1
                begin = index + 1
            }
        }
    }

    // 求每个可行区间中解的数目并加和 求解可行区间已经保证区间内必有上下限存在
    for _, region := range borders{
        left := region[0]
        right := region[1]
        ismin := false
        ismax := false
        last := 0
        allarray := (right - left + 1) * (right - left) / 2
        left = 0
        outrange := 0
        for index, value := range nums[region[0]:right]{
            if value != minK && value != maxK{
                outrange += index - left + 1
            }else if value == minK{
                if value == maxK{
                    left = index + 1
                    continue
                }
                if ismax{
                    left = last + 1
                    ismax = false
                    ismin = true
                }else if !ismin{
                    ismin = true
                }
                last = index
                outrange += index - left + 1
            }else{
                if ismin{
                    left = last + 1
                    ismin = false
                    ismax = true
                }else if !ismax{
                    ismax = true
                }
                last = index
                outrange += index - left + 1
            }
        }
        result += int64(allarray - outrange)
    }
    return result
}

总结

本次解题代码的运行速度超过了100%的提交, 因此不再看他人的题解了, 同时庆祝一下自己拿到了3月份的奖章, 证明这个月每天都在坚持. 下个月希望能继续坚持下去.

day34 2024-04-01

58. Length of Last Word

Given a string s consisting of words and spaces, return the length of the last word in the string.

A word is a maximal substring consisting of non-space characters only. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04010SzI491D9VfR.png

题解

本题是一道简单题, 直接从头开始遍历, 遇到空格结束当前单词计数, 再遇到新字符时重新计数, 直到遍历完整个字符串即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func lengthOfLastWord(s string) int {
    length := 0
    flag := false
    for _, value := range s{
        if value == ' ' {
            flag = true
        }
        if value != ' ' {
            if flag{
                length = 1
                flag = false
            }else{
                length++
            }
        }
    }
    return length
}

总结

前面的题解大多用了go strings库中的函数来按空格分割字符串, 再返回最后一个分割出来的字符串长度. 一般实际应用中, 使用官方库是第一选择, 因为官方库大多对这些操作做了大量优化, 效率会高得多.

day35 2024-04-02

205. Isomorphic Strings

Given two strings s and t, determine if they are isomorphic.

Two strings s and t are isomorphic if the characters in s can be replaced to get t.

All occurrences of a character must be replaced with another character while preserving the order of characters. No two characters may map to the same character, but a character may map to itself. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0402cPE50Xu3nc34.png

题解

本题是一道简单题, 要求判定s和t是不是同构的, 即类似常说的ABB型这种结构. 本题使用一个哈希表记录字符与字符的映射即可, 如果出现了不同的映射则是不同构的.

代码

 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
func isIsomorphic(s string, t string) bool {
    table1 := map[rune]byte{}
    table2 := map[byte]rune{}
    for index, value := range s{
        mapping1, exist := table1[value]
        mapping2, exist2 := table2[t[index]]
        if !exist{
            table1[value] = t[index]
        }else{
            if !(mapping1 == t[index]){
                return false
            }
        }
        if !exist2{
            table2[t[index]] = value
        }else{
            if mapping2 == value{
                continue
            }else{
                return false
            }
        }
    }
    return true
}

总结

查看前面的题解, 发现了果然0ms用时的思路还是不同凡响, 用字符本身作为下标维护两个数组, 在遍历两个字符串的时候将两个字符串当前的字符作为下标, 将当前遍历的字符串的下标作为值放入两个数组中, 这样就记录了同时出现的两个字符最后的出现的下标, 如果这两个字符一直同时出现则二者下标一直是相同的, 若没有同时出现说明字符映射改变了, 没有继续之前的映射. 则两个字符串就是不同构的. 字符的本质是数, 那么字符可以表示更多的信息, 而不是字符本身, 将其作为数组下标就既使用了字符本身这个信息, 还使用了数组对应位置的元素这个信息, 能使用更多的信息就意味着更高的效率.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func isIsomorphic(s string, t string) bool {
    s2t := [128]byte{}
    t2s := [128]byte{}
    for i, end := 0, len(s); i < end; i++ {
        if s2t[s[i]] != t2s[t[i]] {
            return false
        }
        s2t[s[i]], t2s[t[i]] = byte(i+1), byte(i+1)
    }
    return true
}

day36 2024-04-03

Given an m x n grid of characters board and a string word, return true if word exists in the grid.

The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0403PbwhJmQsvAC8.png

题解

本题可使用递归求解, 设置一个标记数组和board大小相同, 来标记已经访问过的元素, 遍历board数组, 找到目标字符串的起始字符, 对其调用explore函数求解, 若为true则直接返回true. 最后全部遍历完则返回false.

explore函数中, 传入了当前字符的行下标和列下标, 先判断可行的探索方向, 再在可行的探索方向上逐个判断其是否为目标字符串中的下一个字符以及是否已经被访问过, 若可以访问且为目标字符, 则对这个可行方向的字符调用explore函数继续后面的探索, 知道探索到目标字符串结尾则直接返回true. 若调用explore函数返回为true说明后面的字符串符合目标字符串且有可行路径, 则返回true. 若没有可探索方向或者所有可探索方向都不满足条件, 返回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
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

func exist(board [][]byte, word string) bool {
    for index, value := range board{
        for index1, value2 := range value{
            if value2 == byte(word[0]){
                var flag [][]int
                for _,_ = range board{
                    a := make([]int, len(board[0]))
                    flag = append(flag, a)
                }
                if (explore(board, index, index1, 0, flag, word)){
                    return true
                }
            }
        }
    }
    return false

}

func explore(board [][]byte, row int, column int, target int, flag [][]int, word string)bool{
    if target == len(word)-1 && byte(word[target]) == board[row][column]{
        return true
    }
    up, down, left, right := !(row == 0),!(row == len(board)-1),!(column == 0), !(column == len(board[0])-1)
    ok := false
    if up && board[row-1][column] == byte(word[target+1])&&(flag[row-1][column] == 0){
        if target+1 == len(word) - 1{
            return true
        }
        flag[row][column] = 1
        ok = ok || explore(board, row-1, column, target+1, flag,word)
        flag[row][column] = 0
    }
    if down && board[row+1][column] == byte(word[target+1]) &&(flag[row+1][column] == 0){
        if target+1 == len(word) - 1{
            return true
        }
        flag[row][column] = 1
        ok =ok || explore(board, row+1, column, target+1, flag, word)
        flag[row][column] = 0
    }
    if left && board[row][column-1] == byte(word[target+1]) &&(flag[row][column-1] == 0){
        if target+1 == len(word) - 1{
            return true
        }
        flag[row][column] = 1
        ok =ok || explore(board, row, column-1, target+1, flag, word)
        flag[row][column] = 0
    }
    if right && board[row][column+1] == byte(word[target+1]) &&(flag[row][column+1] == 0){
        if target+1 == len(word) - 1{
            return true
        }
        flag[row][column] = 1
        ok = ok || explore(board, row, column+1, target+1, flag, word)
        flag[row][column] = 0
    }
    return ok
}

day37 2024-04-04

1614. Maximum Nesting Depth of the Parentheses

A string is a valid parentheses string (denoted VPS) if it meets one of the following:

It is an empty string “”, or a single character not equal to “(” or “)”, It can be written as AB (A concatenated with B), where A and B are VPS’s, or It can be written as (A), where A is a VPS. We can similarly define the nesting depth depth(S) of any VPS S as follows:

depth("") = 0

depth(C) = 0, where C is a string with a single character not equal to “(” or “)”.

depth(A + B) = max(depth(A), depth(B)), where A and B are VPS’s.

depth("(" + A + “)”) = 1 + depth(A), where A is a VPS.

For example, “”, “()()”, and “()(()())” are VPS’s (with nesting depths 0, 1, and 2), and “)(” and “(()” are not VPS’s.

Given a VPS represented as string s, return the nesting depth of s.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0404oW0NMjd6UQ2W.png

题解

本题是简单题, 因为题目说明了给定的括号一定是匹配的, 因此不用考虑括号匹配的问题. 只需设计一个值来保存当前还未匹配的左括号的个数, 这也是到当前这个字符的括号深度, 因此若出现了新的左括号, 就更新最大深度为当前的左括号个数和之前的最大深度中的较大值, 出现右括号则将未匹配的左括号个数减一, 最后返回最大深度即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func maxDepth(s string) int {
    depth := 0
    left := 0
    for _,value := range s{
        if value == '('{
            left++
            depth = max(depth, left)
        }
        if value == ')'{
            left--
        }
    }
    return depth
}

day38 2024-04-05

1544. Make The String Great

Given a string s of lower and upper case English letters.

A good string is a string which doesn’t have two adjacent characters s[i] and s[i + 1] where:

0 <= i <= s.length - 2 s[i] is a lower-case letter and s[i + 1] is the same letter but in upper-case or vice-versa. To make the string good, you can choose two adjacent characters that make the string bad and remove them. You can keep doing this until the string becomes good.

Return the string after making it good. The answer is guaranteed to be unique under the given constraints.

Notice that an empty string is also good.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/040507vRG8OFjjEa.png

题解

本题可以使用类似简单回溯的方法, 遍历字符串并判断当前字符和下一个字符是否互为大小写, 若是则删掉两个字符同时将循环下标回退到当前字符的前一个, 继续遍历字符串, 如此反复直到到达字符串结尾即可, 注意处理字符位于字符串结尾和开始的边界情况.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func makeGood(s string) string {
    index := 0
    length := len(s)
    for index=0;index<length-1;index++{
        if s[index]!=s[index+1]&&(s[index] == s[index+1]+32 || s[index] == s[index+1]-32){
            if index == length-2{
                s = s[0:index]
                length = len(s)
                break
            }else{
                s = s[0:index]+s[index+2:]
                length = len(s)
            }
            if index == 0{
                index--
                continue
            }else{
                index = index - 2
            }
        }
    }
    return s
}

总结

看到题解中有人使用栈, 通过额外的空间获得了更快的速度, 将前面的字符都入栈, 将栈顶和当前字符比较, 互为大小写则将栈顶出栈, 继续向下遍历并比较, 不互为大小写则将当前字符入栈. 这样不用回退下标, 可以充分利用go 中对for range循环优化带来的效率.

 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
func makeGood(s string) string {
    sb := []rune(s)
	stack := make([]byte, 0)

	if len(sb) > 1 {

		for i, _ := range s {

			n := len(stack)

			if n > 0 && ((s[i]-stack[n-1] == 32) || (stack[n-1]-s[i] == 32)) {

				stack = stack[:n-1]

			} else {
				stack = append(stack, s[i])
			}
		}
		return string(stack)

	} else {
		return s
	}

}

day39 2024-04-06

1249. Minimum Remove to Make Valid parentheses

Given a string s of ‘(’ , ‘)’ and lowercase English characters.

Your task is to remove the minimum number of parentheses ( ‘(’ or ‘)’, in any positions ) so that the resulting parentheses string is valid and return any valid string.

Formally, a parentheses string is valid if and only if:

It is the empty string, contains only lowercase characters, or It can be written as AB (A concatenated with B), where A and B are valid strings, or It can be written as (A), where A is a valid string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04066vTKpXK0w9Dc.png

题解

本题实际上还是一个括号匹配的问题, 使用一个栈就可以解决问题, 与前一天题不同的是, 本题中的左括号不一定能完全被匹配, 所以栈中可以保存左括号的下标, 遍历到字符串末尾后, 将栈中仍然存在的未匹配的左括号全部删去即可.

代码

 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
func minRemoveToMakeValid(s string) string {
    stack := []int{}
    number := 0
    index := 0
    for index < len(s){
        if s[index] == ')' {
            if number == 0{
            if index == len(s) - 1{
                s = s[0:index]
                return s
            }else{
                s = s[0:index] + s[index+1:]
            }
            }else{
                number--
                stack = stack[0:len(stack)-1]
                index++
            }
        }else if s[index] == '('{
            number++
            stack = append(stack, index)
            index++
        }else{
            index++
        }

    }
    if len(stack) != 0{
        for index,value := range stack{
            if value - index != len(s) - 1{
                s = s[0:value-index]+s[value+1-index:]
            }else{
                s = s[0:value-index]
                return s
            }

        }
    }
    return s
}

day40 2024-04-07

678. Valid Parenthesis String

Given a string s containing only three types of characters: ‘(’, ‘)’ and ‘*’, return true if s is valid.

The following rules define a valid string:

Any left parenthesis ‘(’ must have a corresponding right parenthesis ‘)’.

Any right parenthesis ‘)’ must have a corresponding left parenthesis ‘(’.

Left parenthesis ‘(’ must go before the corresponding right parenthesis ‘)’.

‘*’ could be treated as a single right parenthesis ‘)’ or a single left parenthesis ‘(’ or an empty string “”.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0407zQ0ujSVhFg50.png

题解

本题仍然是括号匹配问题, 只是这次加了一个万能字符’*’,相当于赖子,既可以当’(‘用也可以当’)‘用. 作为一个判定问题, 只需要判断字符串是否匹配即可. 因此使用一个变量记录当前未匹配的左括号个数和可以被匹配的星号个数, 当遇到未匹配的右括号时如果没有未匹配的左括号则使用星号匹配. 若遍历结束左括号没有剩余则匹配成功. 若星号个数比左括号少则直接匹配失败. 若左括号和星号仍有剩余, 且星号个数大于左括号时, 说明剩余的星号可能能完全匹配左括号,这时反向遍历,把’)‘作为之前的左括号看待,用同样的方法尝试匹配所有’(’. 最后只要’(‘能匹配成功即成功. p.s: 这里考虑的是正向遍历时若’)‘都匹配成功了, 那么只需要测试’(‘能否匹配就够了, 因为剩余的’)‘在正向遍历时已经确认可以通过多余的’*‘来完全匹配, 因此不用再考虑’)‘的匹配问题.

代码

 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
func checkValidString(s string) bool {
    leftstack := 0
    star := 0
    for _, value := range s {
        if value == '*'{
            star++
        }else if value == '('{
            leftstack++
        }else{
            if leftstack > 0{
                leftstack--
            }else if star > 0{
                star--
            }else{
                return false
            }
        }
    }
    if leftstack == 0{
        return true
    }else if star < leftstack{
        return false
    }else{
        leftstack = 0
        star = 0
        for index:= len(s)-1;index>=0;index--{
            if s[index] == ')'{
                leftstack++
            }else if s[index] == '*'{
                star++
            }else{
                if leftstack > 0{
                leftstack--
            }else if star > 0{
                star--
            }else{
                return false
            }
            }
        }
    }
    return true
}

day41 2024-04-08

1700. Number of Students Unable to Eat Lunch

The school cafeteria offers circular and square sandwiches at lunch break, referred to by numbers 0 and 1 respectively. All students stand in a queue. Each student either prefers square or circular sandwiches.

The number of sandwiches in the cafeteria is equal to the number of students. The sandwiches are placed in a stack. At each step:

If the student at the front of the queue prefers the sandwich on the top of the stack, they will take it and leave the queue.

Otherwise, they will leave it and go to the queue’s end.

This continues until none of the queue students want to take the top sandwich and are thus unable to eat.

You are given two integer arrays students and sandwiches where sandwiches[i] is the type of the ith sandwich in the stack (i = 0 is the top of the stack) and students[j] is the preference of the jth student in the initial queue (j = 0 is the front of the queue). Return the number of students that are unable to eat.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04085IjBuW1NxyMu.png

题解

本题是一道简单题, 题干看似很长, 分析下来会发现, 若当前栈顶的三明治当前学生不喜欢, 就会向下轮换, 直到轮换到喜欢的学生, 因此栈顶三明治是否会被拿走取决于队伍中是否还有喜欢这种三明治的学生, 只要有这个学生总会被轮换到队伍的最前面. 则遍历学生数组, 统计两种三明治的喜欢的学生的数量. 遍历三明治数组, 遍历过程中减掉被拿走的三明治的类型喜欢的学生的数量, 直到三明治数组的当前元素没有学生喜欢, 则剩余的三明治就无法被拿到, 也就是无法吃到三明治的学生个数.

代码

 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
func countStudents(students []int, sandwiches []int) int {
    love1 := 0
    love0 := 0
    length := len(sandwiches)
    for _,value := range students{
        if value == 1{
            love1++
        }else{
            love0++
        }
    }
    for index,value := range sandwiches{
        if value == 1{
            if love1 <= 0{
                return length - index
            }
            love1--
        }else{
            if love0 <= 0{
                return length - index
            }
            love0--
        }
    }
    return 0
}

day42 2024-04-09

2073. Time Needed to Buy Tickets

There are n people in a line queuing to buy tickets, where the 0th person is at the front of the line and the (n - 1)th person is at the back of the line.

You are given a 0-indexed integer array tickets of length n where the number of tickets that the ith person would like to buy is tickets[i].

Each person takes exactly 1 second to buy a ticket. A person can only buy 1 ticket at a time and has to go back to the end of the line (which happens instantaneously) in order to buy more tickets. If a person does not have any tickets left to buy, the person will leave the line.

Return the time taken for the person at position k (0-indexed) to finish buying tickets.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0409yxrIrQga6aAr.png

题解

本题是一道简单题,并不需要去考虑模拟队列一轮一轮买票的过程, 而考虑队伍中的每个人在目标k买完票之前买了多少次票并将每个人中买的次数加和即可. 这里要将k之前和k之后的分开考虑, k之前的如果比k的需求大则在k买完之前都要买k需求的票的数量, 比k小则将自己的需求数量买完即可. 在k之后不同在于需求数大于等于k的人只会买k-1张票, 在k买完自己需要的票的时候这些人还在k的后面故当k买完时这些人最多买了k-1张. 遍历一遍数组按照这个规则将每个人的买票数加和即可得到最终结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func timeRequiredToBuy(tickets []int, k int) int {
    target := tickets[k]
    time := 0
    for _,value := range tickets[0:k]{
        if value >= target{
            time += target
        }else{
            time += value
        }
    }
    for _,value := range tickets[k:]{
        if value >= target - 1{
            time += target - 1
        }else{
            time += value
        }
    }
    // add itself once
    time++
    return time
}

day43 2024-04-10

950. Reveal Cards In Increasing Order

You are given an integer array deck. There is a deck of cards where every card has a unique integer. The integer on the ith card is deck[i].

You can order the deck in any order you want. Initially, all the cards start face down (unrevealed) in one deck.

You will do the following steps repeatedly until all cards are revealed:

Take the top card of the deck, reveal it, and take it out of the deck. If there are still cards in the deck then put the next top card of the deck at the bottom of the deck. If there are still unrevealed cards, go back to step 1. Otherwise, stop. Return an ordering of the deck that would reveal the cards in increasing order.

Note that the first entry in the answer is considered to be the top of the deck.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0410kWPCSmxbhxXZ.png

题解

本题为中等难度, 题目中的不变的为牌的数量, 目标即为将牌按照合适的顺序插入这个牌堆, 首先排序, 然后模拟取牌顺序将牌插入. 用一个数组保存当前牌堆中的可用位置. 设置可用位置为一个队列, 对于需要跳过的位置(即翻一张牌后下一张牌需要放到底部, 相当于跳过了这张牌)将其从队列头放到队列尾, 取出队列下一个位置并将当前牌插入, 再跳过一个位置, 插入一张牌, 如此重复直到清空队列.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func deckRevealedIncreasing(deck []int) []int {
    position := []int{}
    sort.Ints(deck)
    now := 0
    result := make([]int,len(deck))
    for index,_ := range deck{
        position = append(position, index)
    }
    for len(position) > 1{
        result[position[0]] = deck[now]
        position = append(position,position[1])
        position = position[2:]
        now++
    }
    result[position[0]] = deck[now]
    return result
}

总结

这样虽然写起来方便, 但实际上在不断append的过程中position会占用更多的空间, 使用循环队列可以避免空间的浪费.

day44 2024-04-11

402. Remove K Digits

Given string num representing a non-negative integer num, and an integer k, return the smallest possible integer after removing k digits from num.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04114Me1ODc5Qd5U.png

题解

本题考虑数字中的某一位数字能产生的影响, 对应这个数字相邻的左右两个数字, 如果删掉了相邻的数字没有删掉当前这一位的数字, 在删除后的字符串中相当于用当前这一位数字代替了被删掉的相邻数字, 如果删掉了当前的数字没删掉相邻的数字, 同样相当于在这一位上用相邻的数字代替了被删掉的数字. 即在删除后的字符串中的这一位上用当前这一位的数字和相邻数字二选一, 前后的其余字符串不用考虑不影响这个局部变化. 仅考虑这个局部变化的过程, 最优解为在删除完毕的字符串中的当前位置应该放入较小的数字, 即相邻数字中较大的数应该被删去, 考虑左右相邻的对称性, 则可以得出, 删掉比左右相邻数字都大的数可以在删掉一个数后得到局部最优解, 删掉这个数后要回溯一位判断删除后之前的数是否符合条件. 重复此过程遍历数组, 删掉k个数后返回最终的字符串.

代码

 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
func removeKdigits(num string, k int) string {
    if len(num) == k{
        return "0"
    }
    index := 0
    delete := 0
    for index < len(num) && delete < k{
        if delete == 554{
            fmt.Println(num)
        }
        if index == 0{
            if len(num) == 1{
                break
            }
            if num[index] == '0'{
                num = num[1:]
                index--
            }else if num[index] > num[index+1]{
                delete++
                num = num[1:]
                index--
            }
            index++
        }else if index == len(num) - 1{
            if num[index] >= num[index-1]{
                num = num[0:len(num)-1]
                index--
                delete++
            }
        }else{
            if num[index] > num[index-1]&&num[index] >= num[index+1] || num[index] > num[index+1] && num[index] >= num[index-1]{
                num = num[0:index] + num[index+1:]
                index--
                delete++
            }else{
                index++
            }
        }
    }
    if delete < k{
        return "0"
    }
    for index = 0;num[index] == '0'&&index < len(num)-1;index++{}
    return num[index:]
}

总结

本题自己想出的解法效率实在不太行, 思路上也不够简洁优美, 看了前面的题解, 发现从原来的思考的基础上应该进一步思考, 对于一串数字字符串来说, 可以删除的数字数量是有限的, 则要首先让数字的高位部分尽可能小, 删除的时候不能改变未删除的数字的相对位置, 根据之前的局部最优解的思考, 如果当前位置的数字比后一位大, 则应将这一位删掉, 让后一位占据当前位置, 这样保证了当前的数字一定比后一位数字小, 这样就做到了尽可能让小的数字位于前面, 使得每一步操作结束后的子数组具有单调性. 这也意味着之前考虑前后两个相邻数字的大小是多余的, 考虑后一位即可, 只需要使用一个单调栈来保证栈末尾的数比要入栈的数字小即可. 注意处理前导0和遍历一次字符串后没有删够元素的情况即可. 代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func removeKdigits(num string, k int) string {
    result := []rune{}

    for _, c := range num {
        for len(result) > 0 && result[len(result) - 1] > c && k > 0 {
            result = result[:len(result) - 1]
            k--
        }
        if len(result) > 0 || c != '0' {
            result = append(result, c)
        }
    }

    for len(result) > 0 && k > 0 {
        result = result[:len(result) - 1]
        k--
    }

    if len(result) <= 0 {
        return "0"
    }
    return string(result)
}

day45 2024-04-12

42. Trapping Rain Water

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0412geuVcDJFbeRD.png

题解

本题是leetcode中一道经典难题, 思路上和昨天的题目有一些相似, 仍然思考局部的情况, 对于任一位置而言, 其在竖直方向上能接到的水的单位数与其自身的高度和其两侧的最高的"墙"的高度有关. 因此关键在于求出任一位置的两侧高度最高的"墙"的高度有多高. 可以使用两个数组来保存某一位置前面的最高的墙的高度和后面的最高墙的高度. 求最高墙的高度的过程可以通过动态规划解决, 用一个数记录当前位置左侧的最高墙高度是多少, 与自身比较, 如果该位置更高, 则更新最高高度, 否则将当前位置的左侧最高墙高度设置为之前的最高墙高度. 对于右侧同理.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func trap(height []int) int {
    highest := 0
    left_high := []int{}
    right_high := make([]int,len(height))
    for _,value := range height{
        if value > highest{
            highest = value
        }
        left_high = append(left_high, highest)
    }
    highest = 0
    for i:=len(height)-1;i>=0;i--{
        if height[i] > highest{
            highest = height[i]
        }
        right_high[i] = highest
    }
    result := 0
    for index,value := range height{
        result += min(left_high[index],right_high[index]) - value
    }
    return result
}

总结

看题解时发现一种更巧妙高效的思路, 之前提到对每一个位置能接的雨水仅与其左右的最高墙高度有关, 那么可以把同时需要知道两个方向的信息固定为只需要考虑一个方向的信息即可. 如果当前位置右侧有墙比当前位置左侧所有墙都高, 那么只需要考虑当前位置左侧最高的墙有多高就可以计算出能接的雨水, 直到出现了墙比右侧的最高的墙还高, 此时就移动尾部的指向右侧最高墙的指针, 向左移动, 此时已知左侧的最高墙比目前尾部指针右侧的所有墙都高, 因此同样只需考虑右侧最高的墙有多高即可, 直到出现墙比左侧的最高墙高, 则再次交换主导权, 移动头部方向的指针. 如此轮换移动指针, 将需要同时比较两个方向的问题简化成了只需要比较一个方向的问题, 保留了更多的信息, 只需要遍历一遍数组即可求得结果.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func trap(height []int) int {
    left, right := 0, len(height) - 1
    leftWall, rightWall := 0,0
    ans:=0
    for left < right {
        if height[left] < height[right] {
            leftWall = max(height[left], leftWall)
            ans += leftWall - height[left]
            left++
        } else {
            rightWall = max(height[right], rightWall)
            ans += rightWall - height[right]
            right--
        }
    }
    return ans
}

day46 2024-04-13

85. Maximal Rectangle

Given a rows x cols binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0413UmMkYaEEo3Qh.png

题解

本题可以在一层层遍历矩阵的过程中, 将每一行中每个位置的元素按照如下规则进行操作加入到当前的列状态数组中. 若值为1且之前的列状态值大于0, 则增加对应的列状态值. 若值为0则用0替换对应的列状态值, 这样相当于在列状态中保存了从以该行为底该列中向上连续为1的值的个数. 根据当前的列状态即可求得到当前行为止以当前行为底的全为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
func maximalRectangle(matrix [][]byte) int {
    now := make([]int, len(matrix[0]))
    maxarea := 0
    for _, row := range matrix{
        for index, number := range row{
            if number == '1'{
                now[index] += 1
            }else{
                now[index] = 0
            }
        }
        stack := []int{0}
        maxarea = max(maxarea,now[0])
        for index, value := range now[1:]{
            for len(stack)>0 && now[stack[len(stack)-1]] > value{
                length := len(stack)
                prev := now[stack[length-1]]
                stack = stack[:len(stack)-1]
                width := index + 1
                if len(stack) > 0{
                    width = index  - stack[length-2]
                }
                maxarea = max(maxarea, width*prev)
            }
            if len(stack) > 0 && value == now[stack[len(stack)-1]]{
                stack[len(stack)-1] = index+1
                continue
            }else{
                stack = append(stack, index+1)
            }
        }
        length := len(now)
        for len(stack) > 0 && now[stack[len(stack)-1]] != 0{
            width := length
            prev := now[stack[len(stack)-1]]
            stack = stack[:len(stack)-1]
            if len(stack) > 0{
                width = length - stack[len(stack)-1] - 1
            }
            maxarea = max(maxarea,width*prev)
        }
    }
    return maxarea
}

day47 2024-04-14

404. Sum of Left Leaves

Given the root of a binary tree, return the sum of all left leaves.

A leaf is a node with no children. A left leaf is a leaf that is the left child of another node.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0414YgfyxPJSsi8d.png

题解

本题是一道简单题, 使用深度优先搜索, 尾序遍历. 遍历的实现可以使用简单的递归函数, 注意对左右两个节点的处理方式不同, 对左节点如果其没有子节点则将值加到结果中, 对右节点则直接继续遍历即可.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func sumOfLeftLeaves(root *TreeNode) int {
    result := leftfirst(root)
    return result
}

func leftfirst(root *TreeNode)int{
    sum := 0
    if root.Left != nil && root.Left.Left == nil && root.Left.Right == nil{
        sum += root.Left.Val
    }else if root.Left != nil{
        sum += leftfirst(root.Left)
    }
    if root.Right != nil{
        sum += leftfirst(root.Right)
    }
    return sum
}

day48 2024-04-15

129. Sum Root to Leaf Numbers

You are given the root of a binary tree containing digits from 0 to 9 only.

Each root-to-leaf path in the tree represents a number.

For example, the root-to-leaf path 1 -> 2 -> 3 represents the number 123. Return the total sum of all root-to-leaf numbers. Test cases are generated so that the answer will fit in a 32-bit integer.

A leaf node is a node with no children. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0415uBAU0k9lKRbE.png

题解

仍然使用递归求解, 对树执行后序遍历, 在对子节点调用递归函数时将当前路径到该子节点的父节点组成的数字作为递归函数的参数, 子节点将该参数乘10后传给子节点的子节点, 并将子节点的两个子节点的返回值加和作为路径和返回给父节点. 若该子节点为叶子节点, 则加上叶子节点的值并返回. 最终在根节点将左右两子节点的返回值加和即可得到最终解.

代码

 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.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func sumNumbers(root *TreeNode) int {
    result := lastnode(root, 0)
    return result
}

func lastnode(root *TreeNode, value int) int{
    sum := 0
    if root.Left != nil{
        sum += lastnode(root.Left, value*10+root.Val)
    }
    if root.Right != nil{
        sum += lastnode(root.Right, value*10+root.Val)
    }
    if root.Left == nil && root.Right == nil{
        return value*10+root.Val
    }
    return sum
}

总结

运行时间2ms本以为算法效率不够高, 结果看了看运行时间0ms的代码, 代码逻辑和我一模一样, 可能被优化的点在于递归函数中可以先判断是否为叶子节点, 再去创建sum变量. 其实这种将属性传递给子节点的方法在编译原理中的语义分析阶段很常用, 也就是属性文法, 可以看作一种继承属性, 更加详细的内容可以查阅编译原理相关的书籍.

day49 2024-04-16

623. Add One Row to Tree

Given the root of a binary tree and two integers val and depth, add a row of nodes with value val at the given depth depth.

Note that the root node is at depth 1.

The adding rule is:

Given the integer depth, for each not null tree node cur at the depth depth - 1, create two tree nodes with value val as cur’s left subtree root and right subtree root.

cur’s original left subtree should be the left subtree of the new left subtree root.

cur’s original right subtree should be the right subtree of the new right subtree root.

If depth == 1 that means there is no depth depth - 1 at all, then create a tree node with value val as the new root of the whole original tree, and the original tree is the new root’s left subtree.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0416RdOc2pHJYPt7.png

题解

本题使用层序遍历, 遍历到对应深度减一的节点时按规则插入节点, 注意处理只有一个节点的特殊情况即可.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func addOneRow(root *TreeNode, val int, depth int) *TreeNode {
    newnode := &TreeNode{val, nil, nil}
    if depth == 1{
        newnode.Left = root
        return newnode
    }
    queue := []*TreeNode{root}
    depths := 1
    for len(queue) != 0{
        if depths == depth-1{
            for _,value := range queue{
                dunode := &TreeNode{val,nil,nil}
                dunode.Left = value.Left
                value.Left = dunode
                dunoderight := &TreeNode{val,nil,nil}
                dunoderight.Right = value.Right
                value.Right = dunoderight
            }
            return root
        }
        for _,value := range queue{
            if value.Left != nil{
                queue = append(queue, value.Left)
            }
            if value.Right != nil{
                queue = append(queue, value.Right)
            }
            queue = queue[1:]
        }
        depths++
    }
    return root
}

总结

本题也可使用dfs结合变量记录当前深度来解决.

day50 2024-04-17

988. Smallest String Starting From Leaf

You are given the root of a binary tree where each node has a value in the range [0, 25] representing the letters ‘a’ to ‘z’.

Return the lexicographically smallest string that starts at a leaf of this tree and ends at the root.

As a reminder, any shorter prefix of a string is lexicographically smaller.

For example, “ab” is lexicographically smaller than “aba”.

A leaf of a node is a node that has no children.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0417XhbdaAO3QUHm.png

题解

使用递归实现dfs, 在dfs的过程中将路径上经过的节点数组作为参数传递给子节点, 对子节点执行dfs. dfs中将父路径的数组和左右两个子树返回的数组分别拼接, 对拼接后的数组进行比较, 将较小的作为返回值返回. 最终在根节点即可返回全局最小字典序字符串.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func smallestFromLeaf(root *TreeNode) string {
	result := dfs(root,[]int{})
	var output []byte
	for _, value := range result {
		newbyte := []byte{byte('a' + value)}
		output = append(newbyte, output...)
	}
	return string(output)
}

func dfs(root *TreeNode, parent []int) ([]int) {
	localminstring := []int{root.Val}
    parentstring := append(parent, root.Val)
	if root.Left == nil && root.Right == nil {
		return localminstring
	} else if root.Left == nil && root.Right != nil {
		localminstring = append(localminstring, dfs(root.Right, parentstring)...)
		return localminstring
	} else if root.Left != nil && root.Right == nil {
		localminstring = append(localminstring, dfs(root.Left, parentstring)...)
		return localminstring
	} else {
        leftstring := dfs(root.Left, parentstring)
        rightstring := dfs(root.Right, parentstring)
        v1 := make([]int,1)
        v1 = append(v1, parentstring...)
        v2 := make([]int,0)
        v2 = append(v2, parentstring...)
        v1 = append(v1,leftstring...)
        v2 = append(v2,rightstring...)
		length1 := len(v1) - 1
		length2 := len(v2) - 1
		for length1 >= 0 && length2 >= 0 {
			if v1[length1] < v2[length2] {
				return append(localminstring, leftstring...)
			} else if v1[length1] > v2[length2] {
				return append(localminstring, rightstring...)
			}
			length1--
			length2--
		}
		if length1 > length2 {
            return append(localminstring, rightstring...)
		} else {
			return append(localminstring, leftstring...)
		}
	}

}

总结

本题看似简单, 实则有一些陷阱, 如果只考虑子树的字典序大小, 只比较子树的字典序大小并返回, 在比较过程中不考虑经过的父路径的话, 对于下面这种情况就会产生错误.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0417z71jf0KpYeae.png

感兴趣的可以自行尝试只考虑子树的字典序, 对于这个例子会产生问题.

另外一方面, 看0ms的解答代码, 其不在dfs过程中比较, 而是直接通过dfs将这棵树能产生的全部字符串保存起来, 最后再对所有的字符串进行排序, 使用了go内置的sort排序, 返回排好序后的第一个字符串即最小的字符串. 这种方法减少了递归过程中的处理逻辑, 运行起来开销小得多, 所以速度比较快.

day51 2024-04-18

463. Island Perimeter

You are given row x col grid representing a map where grid[i][j] = 1 represents land and grid[i][j] = 0 represents water.

Grid cells are connected horizontally/vertically (not diagonally). The grid is completely surrounded by water, and there is exactly one island (i.e., one or more connected land cells).

The island doesn’t have “lakes”, meaning the water inside isn’t connected to the water around the island. One cell is a square with side length 1. The grid is rectangular, width and height don’t exceed 100. Determine the perimeter of the island.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0418uiOqZ70eh1vg.png

题解

本题直接遍历二维数组, 当遇到值为1即陆地时对四个方向进行判断并对与水面相邻的陆地的边界进行累加即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func islandPerimeter(grid [][]int) int {
    result := 0
    for rowindex, row := range grid{
        for columnindex, value := range row{
            if value == 1{
                if rowindex == 0 || grid[rowindex-1][columnindex] == 0{
                    result++
                }
                if rowindex == len(grid)-1 || grid[rowindex+1][columnindex] == 0{
                    result++
                }
                if columnindex == 0 || grid[rowindex][columnindex-1] == 0{
                    result++
                }
                if columnindex == len(grid[0]) - 1 || grid[rowindex][columnindex+1] == 0{
                    result++
                }

                        }
    }
    }
    return result
}

总结

在查看题解的过程中, 发现尽管算法思路相同, 但实现起来有一种比较巧妙的方法, 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func islandPerimeter(grid [][]int) int {
	DIRS := [][]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}}
	sides := 0
	for row, ROWS := 0, len(grid); row < ROWS; row++ {
		for col, COLS := 0, len(grid[0]); col < COLS; col++ {
			if grid[row][col] == 1 {
				for _, dir := range DIRS {
					r, c := row+dir[0], col+dir[1]
					// Check to see if the neighbour is an edge tile or water.
					if r < 0 || r >= ROWS || c < 0 || c >= COLS || grid[r][c] == 0 {
						sides++
					}
				}
			}
		}
	}
	return sides
}

day52 2024-04-19

200. Number of Islands

Given an m x n 2D binary grid grid which represents a map of ‘1’s (land) and ‘0’s (water), return the number of islands.

An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0419861hzvutWOiR.png

题解

本题遍历矩阵, 遇到1时对该元素执行bfs, 并将搜索过程中遇到的所有1置为0. 搜索结束后将最终结果加一. 继续遍历, 重复此过程即可.

代码

 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
func numIslands(grid [][]byte) int {
    rowlength := len(grid)
    columnlength := len(grid[0])
    result := 0
    for rowindex, row := range grid{
        for columnindex, _ := range row{
            if grid[rowindex][columnindex] == '1'{
                queue := [][]int{{rowindex, columnindex}}
                grid[rowindex][columnindex] = '0'
                for len(queue) != 0{
                    for _, index := range queue{
                        queue = queue[1:]
                        if index[0] != 0 && grid[index[0]-1][index[1]] == '1'{
                            grid[index[0]-1][index[1]] = '0'
                            queue = append(queue,[]int{index[0]-1,index[1]})
                        }
                        if index[0] != rowlength-1 && grid[index[0]+1][index[1]] == '1'{
                            grid[index[0]+1][index[1]] = '0'
                            queue = append(queue,[]int{index[0]+1, index[1]})
                        }
                        if index[1] != 0 && grid[index[0]][index[1]-1] == '1'{
                            grid[index[0]][index[1]-1] = '0'
                            queue = append(queue, []int{index[0],index[1]-1})
                        }
                        if index[1] != columnlength-1 && grid[index[0]][index[1]+1] == '1'{
                            grid[index[0]][index[1]+1] = '0'
                            queue = append(queue, []int{index[0],index[1]+1})
                        }
                    }
                }
                result++
            }
        }
    }
    return result

}

总结

本题我的解法中使用队列来执行bfs, 使用数组来模拟队列的过程中出队入队都会消耗资源, 增加执行时间. 实际上本题无需额外空间, 因为数组是可以修改的, 只需要找到1并将与该1连接的岛屿的所有1置为0即可, 这个过程可以使用dfs来实现,在dfs的过程中目标仅为将所有相连岛屿的1置为0, 因此返回值并不重要. 这种方法省去了队列操作的开销, 下面这种总体比较简洁, 值得学习.

 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
func numIslands(grid [][]byte) int {
    if grid == nil {
        return 0
    }
    var res int
    for i:=0; i<len(grid); i++ {
        for j:=0; j < len(grid[0]);j++ {
            res += findIsl(i, j, grid)
        }
    }
    return res
}

func findIsl(i, j int, m [][]byte) int {
    if (i == -1 || j == -1 || i == len(m) || j == len(m[0])) {
        return 0
    }
    if m[i][j] == '1' {
        m[i][j] = '0'
        findIsl(i-1, j, m)
        findIsl(i+1, j, m)
        findIsl(i, j+1, m)
        findIsl(i, j-1, m)
        return 1
    }
    return 0
}

day53 2024-04-20

1992. Find All Groups of Farmland

You are given a 0-indexed m x n binary matrix land where a 0 represents a hectare of forested land and a 1 represents a hectare of farmland.

To keep the land organized, there are designated rectangular areas of hectares that consist entirely of farmland. These rectangular areas are called groups. No two groups are adjacent, meaning farmland in one group is not four-directionally adjacent to another farmland in a different group.

land can be represented by a coordinate system where the top left corner of land is (0, 0) and the bottom right corner of land is (m-1, n-1). Find the coordinates of the top left and bottom right corner of each group of farmland. A group of farmland with a top left corner at (r1, c1) and a bottom right corner at (r2, c2) is represented by the 4-length array [r1, c1, r2, c2].

Return a 2D array containing the 4-length arrays described above for each group of farmland in land. If there are no groups of farmland, return an empty array. You may return the answer in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0420fKa59Spo1al3.png

题解

本题限定了农田只会是一个矩形, 因此遍历数组,遇到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
func findFarmland(land [][]int) [][]int {
    rowlen := len(land)
    collen := len(land[0])
    result := [][]int{}
    nextrow := 0
    nextcol := 0
    for rowindex,row := range land{
        for colindex,_ := range row{
            if land[rowindex][colindex] == 1{
                farmland := []int{rowindex, colindex}
                nextrow = rowindex
                nextcol = colindex
                for nextrow < rowlen{
                    if land[nextrow][colindex] == 0{
                        break
                    }
                    nextcol = colindex
                    for nextcol < collen && land[nextrow][nextcol] == 1{
                        land[nextrow][nextcol] = 0
                        nextcol++
                    }
                    nextrow++
                }
                farmland = append(farmland, nextrow-1)
                farmland = append(farmland, nextcol-1)
                result = append(result, farmland)
            }
        }
    }
    return result
}

day54 2024-04-21

1971. Find if Path Exists in Graph

There is a bi-directional graph with n vertices, where each vertex is labeled from 0 to n - 1 (inclusive). The edges in the graph are represented as a 2D integer array edges, where each edges[i] = [ui, vi] denotes a bi-directional edge between vertex ui and vertex vi. Every vertex pair is connected by at most one edge, and no vertex has an edge to itself.

You want to determine if there is a valid path that exists from vertex source to vertex destination.

Given edges and the integers n, source, and destination, return true if there is a valid path from source to destination, or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04213IWGw1wdQ1vc.png

题解

本题使用并查集即可快速解决, 因为若两个节点连通, 两个节点必定位于同一个连通图中, 而每个连通图可以通过并查集使用一个节点的值来表示, 因此遍历所有边并构造并查集, 最终比较源点和目标点所在的并查集的代表元(即用来代表一个连通图的节点的值)是否相同即可确定是否有通路.

代码

 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
func validPath(n int, edges [][]int, source int, destination int) bool {
    querymap := map[int]int{}
    querymap[source] = source
    querymap[destination] = destination
    for _,edge := range edges{
        value,exist := querymap[edge[0]]
        value2,exist2 := querymap[edge[1]]
        if !exist && !exist2{
            querymap[edge[0]] = edge[0]
            querymap[edge[1]] = edge[0]
        }else if exist && !exist2{
            querymap[edge[1]] = value
        }else if !exist && exist2{
            querymap[edge[0]] = value2
        }else{
            for querymap[value] != value{
                value = querymap[value]
            }
            for querymap[value2] != value2{
                value2 = querymap[value2]
            }
            if value != value2{
                querymap[value2] = value
            }
        }
    }

    for querymap[source] != source{
        source = querymap[source]
    }
    for querymap[destination] != destination{
        destination = querymap[destination]
    }
    if source != destination{
        return false
    }else{
        return true
    }
}

总结

最快的解法同样使用了并查集, 不过将并查集的操作都单独写成了对应的函数. 同时使用了数组来保存集合中某个元素的父元素是什么, 数组下标表示某个节点, 对应的值表示其父节点的值. 这样查询速度更快, 不过浪费了一些空间.

 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
type DisjointSet struct {
    roots []int
    ranks []int
}

func (ds *DisjointSet) Find(x int) int {
    if ds.roots[x] == x {
        return x
    }
    ds.roots[x] = ds.Find(ds.roots[x])
    return ds.roots[x]
}

func (ds *DisjointSet) Union(x, y int) {
    rootX := ds.Find(x)
    rootY := ds.Find(y)
    if rootX == rootY {
        return
    }
    rankX := ds.ranks[rootX]
    rankY := ds.ranks[rootY]
    if rankX > rankY {
        ds.roots[rootY] = rootX
        return
    }
    if rankX < rankY {
        ds.roots[rootX] = rootY
        return
    }
    ds.roots[rootX] = rootY
    ds.ranks[rootY] += 1
}

func (ds *DisjointSet) IsConnected(x, y int) bool {
    return ds.Find(x) == ds.Find(y)
}

func newDisjointSet(n int) *DisjointSet {
    roots := make([]int, n)
    ranks := make([]int, n)
    for i := range n {
        roots[i] = i
        ranks[i] = 1
    }
    newDisjointSet := DisjointSet{roots, ranks}
    return &newDisjointSet
}

func validPath(n int, edges [][]int, source int, destination int) bool {
    ds := newDisjointSet(n)
    for _, edge := range edges {
        ds.Union(edge[0], edge[1])
    }
    return ds.IsConnected(source, destination)
}

day55 2024-04-22

752. Open the Lock

You have a lock in front of you with 4 circular wheels. Each wheel has 10 slots: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’. The wheels can rotate freely and wrap around: for example we can turn ‘9’ to be ‘0’, or ‘0’ to be ‘9’. Each move consists of turning one wheel one slot.

The lock initially starts at ‘0000’, a string representing the state of the 4 wheels.

You are given a list of deadends dead ends, meaning if the lock displays any of these codes, the wheels of the lock will stop turning and you will be unable to open it.

Given a target representing the value of the wheels that will unlock the lock, return the minimum total number of turns required to open the lock, or -1 if it is impossible.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0422NdMH16LLOOVH.png

题解

本题题面非常有意思, 关键是怎么转化这道题. 首先思考如果只有两个转盘, 那么问题可以转化成在一个二维平面上从点(0,0)走到目标点的有障碍物最短路问题, 即在二维数组中, 从(0,0)走到目标点,每次只能向相邻方向移动一位, 同时有一些坐标是不能走得(可以理解为有障碍物). 对于这个问题, 无疑首先想到的就可以通过BFS求解. 将这个问题从二维扩展到四维, 因为每个转盘只有0-9 共10个数字, 所以每一维只需要10个数表示即可, 这样问题就变成了在一个10*10*10*10的四维数组中, 从(0,0,0,0)走到目标点的最短路问题, 其中数组中有些位置是不可达的. 同样使用和二维数组中类似的BFS求解即可.

代码

 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
func openLock(deadends []string, target string) int {
	deadends_number := map[int]int{}
    targetnumber,_ := strconv.Atoi(target)

	for _, value := range deadends {
		number,_ := strconv.Atoi(value)
        if number == 0{
            return -1
        }
		deadends_number[number] = 1
	}

	pathmap := [][][][]int{}
	for z := 0; z < 11; z++ {
		second := [][][]int{}
		for i := 0; i < 11; i++ {
			three := [][]int{}
			for j := 0; j < 11; j++ {
				four := []int{}
				for k := 0; k < 11; k++ {
					_, exist := deadends_number[z*1000+i*100+j*10+k]
					if exist {
						four = append(four, 0)
					} else {
						four = append(four, 1)
					}
				}
				three = append(three, four)
			}
			second = append(second, three)
		}
		pathmap = append(pathmap, second)
	}

	queue := [][]int{{0, 0, 0, 0}}
    pathmap[0][0][0][0] = 0
	depth := -1
	for len(queue) != 0 {
		for _, way := range queue {
            if way[0]*1000+way[1]*100+way[2]*10+way[3] == targetnumber{
                return depth+1
            }
			for i := 0; i < 4; i++ { // 遍历四个维度
				original := way[i] // 保存原始值
				// 尝试向两个方向移动
				for _, diff := range []int{-1, 1} {
					newIdx := original + diff

					// 处理边界情况
					if newIdx < 0 {
						newIdx = 9
					} else if newIdx > 9 {
						newIdx = 0
					}

					way[i] = newIdx // 更新当前维度

					// 检查新位置是否可行
					if pathmap[way[0]][way[1]][way[2]][way[3]] == 1 {
						queue = append(queue, []int{way[0], way[1], way[2], way[3]})
						pathmap[way[0]][way[1]][way[2]][way[3]] = 0
					}

					way[i] = original // 恢复原始值以便下一次循环使用
				}
			}
            queue = queue[1:]
		}
		depth++
	}

    return -1
}

总结

遇到题面看上去复杂的题目需要将题目的核心问题抽离出来. 若题目本身解决起来比较困难, 可以先尝试思考解决题目的一个子问题, 如降低维度, 减少数量, 再尝试推广到题目本身. 另外在数学中往往从一维到二维有很多不同点, 可能需要全新的工具和解决方式. 但从二维到更高维往往只是简单推广, 这也是为什么很多数学问题只证明二维的情况即可代表全部高维情况的原因.

day56 2024-04-23

310. Minimum Height Trees

A tree is an undirected graph in which any two vertices are connected by exactly one path. In other words, any connected graph without simple cycles is a tree.

Given a tree of n nodes labelled from 0 to n - 1, and an array of n - 1 edges where edges[i] = [ai, bi] indicates that there is an undirected edge between the two nodes ai and bi in the tree, you can choose any node of the tree as the root. When you select a node x as the root, the result tree has height h. Among all possible rooted trees, those with minimum height (i.e. min(h)) are called minimum height trees (MHTs).

Return a list of all MHTs’ root labels. You can return the answer in any order.

The height of a rooted tree is the number of edges on the longest downward path between the root and a leaf.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0423RbJZu7RPqeXz.png

题解

本题乍一看让人摸不着头脑, 细细想想最远的距离肯定是从一个叶子节点到另一个叶子节点, 那么最小高度的子树就是位于这个最远路径上中间位置的一个或者两个节点(取决于路径的长度是奇数还是偶数). 则本题的关键在于求出该无向图中的最长路径, 求出无向图中最长路径可以选择任一叶子节点, 对其进行dfs, 再将得到的当前最长路径的终点作为起点, 进行dfs即可得到全图的最长路径.

代码

 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
func findMinHeightTrees(n int, edges [][]int) []int {
    if n == 1{
        return []int{0}
    }
    numbers := map[int]int{}
    graph := map[int][]int{}
    exist := false
    for _,value := range edges{
        for index,vertex := range value{
            _, exist = numbers[vertex]
            if !exist{
                numbers[vertex] = 1
                graph[vertex] = []int{value[1-index]}
            }else{
                numbers[vertex]++
                graph[vertex] = append(graph[vertex], value[1-index])
            }
        }
    }
    start := 0
    for node, count := range numbers{
        if count == 1{
            start = node
            break
        }
    }

    path := []int{start}
    depth := 1
    path = append(path, dfs(graph[start][0], start, depth+1, graph)...)
    length := len(path)
    start = path[length-1]
    depth = 1
    newtest := []int{start}
    newtest = append(newtest, dfs(graph[start][0], start, depth+1,graph)...)
    if len(newtest) > length{
        path = newtest
        length = len(newtest)
    }
    if length % 2 == 1{
        return []int{path[length/2]}
    }else{
        return []int{path[length/2-1],path[length/2]}
    }

}

func dfs(node int, parent int,  depth int, graph map[int][]int)[]int{
    max := depth
    return_path := []int{node}
    for _,value := range graph[node]{
        if value != parent{
            temppath := dfs(value, node, depth+1, graph)
            if len(temppath) + depth > max{
                max = len(temppath) + depth
                return_path = append(return_path[0:1], temppath...)
            }
        }
    }
    return return_path
}

总结

显然, 这种解法是相当慢的, 执行两次dfs也有大量的重复计算. 这里解决本题可以使用对无向图的拓扑排序. 排序操作为设定一个队列, 找到当前图中所有度为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
func findMinHeightTrees(n int, edges [][]int) []int {
    if n == 1 {
        return []int{ 0 }
    }


    //graph := make(map[int][]int)
    neibors := make([][]int, n)  //neibors[i]  -- all nodes node i can connect to
    degree := make([]int, n)    //degree[i]  -- connections from node i

    for _, e := range edges {
        na, nb := e[0], e[1]
        neibors[na] = append(neibors[na], nb)
        neibors[nb] = append(neibors[nb], na)
        degree[na]++
        degree[nb]++
    }
   queue := []int{}
    for i, d := range degree {
        if d == 1 {
            queue = append(queue, i)
        }
    }

    // topological sort
    for n > 2 {
        size := len(queue)
        n -= size
        for i:=0;i<size;i++{
            cur:=queue[i]
            for _, next:=range neibors[cur]{
                degree[next]--
                if degree[next]==1{
                    queue=append(queue, next)
                }
            }
        }
        queue=queue[size:]
    }

    return queue
}

day57 2024-04-24

1137. N-th Tribonacci Number

The Tribonacci sequence Tn is defined as follows:

T0 = 0, T1 = 1, T2 = 1, and Tn+3 = Tn + Tn+1 + Tn+2 for n >= 0.

Given n, return the value of Tn.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0424W6tl0Lo6n0pg.png

题解

一个简单的动态规划即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func tribonacci(n int) int {
    if n == 0{
        return 0
    }else if n == 1{
        return 1
    }else if n == 2{
        return 1
    }else{
        arrays := []int32{0,1,1}
        var result int32
        for i:=3;i<=n;i++{
            result = arrays[i-1]+arrays[i-2]+arrays[i-3]
            arrays = append(arrays, result)
        }
        return int(result)
    }

}

今天题目有些过于简单了,遂再补一道题

2385. Amount of Time for Binary Tree to Be Infected

You are given the root of a binary tree with unique values, and an integer start. At minute 0, an infection starts from the node with value start.

Each minute, a node becomes infected if:

The node is currently uninfected. The node is adjacent to an infected node. Return the number of minutes needed for the entire tree to be infected.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0424X5IWeZmiTuqj.png

题解

这道题第一眼看上去就像在一个图中给定一个节点,寻找这个图中与这个节点距离最远的节点的距离.然而题面条件是二叉树,因此最直观的方法就是将二叉树转换为无向图, 再使用BFS找到最远的距离, 这样需要将所有节点遍历两遍.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func amountOfTime(root *TreeNode, start int) int {
    undirected_map := map[int][]int{}
    undirected_map[root.Val] = []int{}
    queue := []*TreeNode{root}


    for len(queue) != 0{
        for _, node := range queue{
            if node.Left != nil{
                undirected_map[node.Val] = append(undirected_map[node.Val],node.Left.Val)
                undirected_map[node.Left.Val] = []int{node.Val}
                queue = append(queue, node.Left)
            }
            if node.Right != nil{
                undirected_map[node.Val] = append(undirected_map[node.Val],node.Right.Val)
                undirected_map[node.Right.Val] = []int{node.Val}
                queue = append(queue, node.Right)
            }
            queue = queue[1:]
        }
    }

    visited := make([]int,100001)
    neibor := []int{start}
    visited[start] = 1
    time := 0
    for len(neibor) != 0{
        for _,node := range neibor{
            for _,value := range undirected_map[node]{
                if visited[value] != 1{
                    neibor = append(neibor, value)
                    visited[value] = 1
                }
            }
            neibor = neibor[1:]
        }
        time++
    }
    return time-1

}

总结

显然这种方法略慢, 要是可以在一次遍历的时候保存一定的信息, 减少重复的节点访问就好了, 思考这棵二叉树, 如果我们知道了从根节点到开始节点的距离, 并且保存了从根节点到开始节点的路径, 那么最远距离分为两种情况, 要么是以开始节点为根节点的子树足够深, 要么是开始节点的祖先节点的另外一棵子树足够深, 二者哪个更大就取哪个的最远距离. 尽管思路如此, 但当时我的想法是这样在寻找从根节点到开始节点时也要使用DFS, 这样在最差情况也要访问整棵树, 但实际上平均情况下会少访问大概以开始节点为根节点的子树的节点. 这样当节点数很多的时候也有一定的复杂度优势. 但是事实证明, 我想的还是不够全面, 实际上我们在递归调用的时候可以通过返回更多信息(一个布尔值)来标记该节点的某个子树上含有开始节点, 同时返回当前节点距离开始节点的距离. 这样通过一个非常巧妙的信息流动, 在开始节点处判断了以开始节点为根节点的子树的最大深度, 从开始节点处递归返回时向祖先节点传递该子树包含开始节点和距离开始节点的距离信息. 这样充分利用了在递归遍历过程中的全部信息. 只需要一次遍历即可解决. 充分的利用和整合信息是提高算法效率的关键, 示例代码如下

 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
func amountOfTime(root *TreeNode, start int) int {
    if root == nil {
        return 0
    }

    r := 0
    //
    // returns true if start is a child of p.
    // and the distance from this node to start.
    // false if the start is not child of p.
    // and the longest distance to a leave in this subtree.
    //
    var dfs func(p * TreeNode) (bool, int)
    dfs = func(p * TreeNode) (bool, int) {
        ll, rr, lh, rh := 0, 0, false, false
        if p.Left != nil {
            lh, ll = dfs(p.Left)
            ll++
        }

        if p.Right != nil {
            rh, rr = dfs(p.Right)
            rr++
        }

        if p.Val == start {
            if r < ll {
                r = ll
            }

            if r < rr {
                r = rr
            }

            return true, 0  // 0 to get the dist to parent
        }

        if lh {
            if ll + rr > r {
                r = ll + rr
            }
            return true, ll
        }

        if rh {
            if ll + rr > r {
                r = ll + rr
            }
            return true, rr
        }

        if ll < rr {
            ll = rr
        }

        return false, ll
    }

    dfs(root)
    return r
}

day58 2024-04-25

2370. Longest Ideal Subsequence

You are given a string s consisting of lowercase letters and an integer k. We call a string t ideal if the following conditions are satisfied:

t is a subsequence of the string s. The absolute difference in the alphabet order of every two adjacent letters in t is less than or equal to k. Return the length of the longest ideal string.

A subsequence is a string that can be derived from another string by deleting some or no characters without changing the order of the remaining characters.

Note that the alphabet order is not cyclic. For example, the absolute difference in the alphabet order of ‘a’ and ‘z’ is 25, not 1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0425VwyUwgCD4hKc.png

题解

字符串子序列问题是一类很经典的递推计数问题. 一般可以用动态规划来解决. 问题的关键在于如何找到问题的子问题. 对于本题, 思考通过贪心等方式直接找到最长的子序列显然是不可行的, 因为一个字符在当前串中作为结尾可以使该串长度增大, 但后续可能有更长的串与没有这个字符的串可以连接, 但加上这个字符使得这个更长的串不能连接了. 显然考虑的不够周全. 那么现在还是要紧紧围绕我们在解题时多次提到过的思想: 能更有效的利用更多的信息, 算法的效率就越高. 对于一个字符来说, 哪些信息是有用的, 与之相差k个距离以内的字符是有用的, 因为这些字符可以与当前字符连接. 用贪心难以解决的原因在于, 只考虑了和当前字符相邻的k以内这一群邻居中的一个, 自然不能高效求解. 那么我们只要一直保存着所有以字符结尾的子序列的长度, 在遇到新字符时只需要对k个距离内的字符子序列进行比较, 找出最长的并将其加1作为当前字符的最长子序列长度. 这样充分利用了以前遍历过的可行字符组合的信息. 并将所有邻居都考虑进来, 最终就能得到可行解.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func longestIdealString(s string, k int) int {
    lengths := make([]int32, 26)
    var left int32
    var right int32
    var temp_max int32
    k32 := int32(k)
    for _,value := range s{
        temp_max = 0
        left = value - 'a' - k32
        right = value - 'a' + k32
        if value - 'a' < k32{
            left = 0
        }
        if 'z' - value < k32{
            right = 25
        }
        for _, number := range lengths[left:right+1]{
            temp_max = max(number, temp_max)
        }
        lengths[value - 'a'] = temp_max + 1
    }
    return int(slices.Max(lengths))
}

day59 2024-04-26

1289. Minimum Falling Path Sum II

Given an n x n integer matrix grid, return the minimum sum of a falling path with non-zero shifts.

A falling path with non-zero shifts is a choice of exactly one element from each row of grid such that no two elements chosen in adjacent rows are in the same column.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0426SCtPSD8Jui7k.png

题解

本题为一道难题, 拿到题目首先观察, 可以想到的比较直接的点是如果从第一行直接向下一行行遍历并选择的话, 那么以3*3的矩阵为例, 第一行的第一二列同时选择第二行的第三列, 则对二者来说, 第三行的可选项是相同的. 即除一二列以外的列二者可以选择来累加的元素是相同的. 如第一个示例中的1-6-7,1-6-8,2-6-7,2-6-8. 显然这里对于1,2来说二三列的选择完全相同, 在这种情况下无疑选择1可以得到更小的和. 问题就变成了, 如何把这种已知的对以前的行中的元素的相对大小这一信息保留下来, 很简单, 让每一行的各个元素都保存在其上面行的最小元素和即可. 这样就保证了在能选择该元素的情况下, 以前的路径是从每行中挑出来的可选元素中的最小元素组成的路径. 也就保留了之前各行的元素之间已经判断过的相对大小. 这是一种动态规划的思想, 这里还要解决另外一个问题, 已知n-1行中保留了之前行的最小路径和, 对于第n行的各个元素, 如何高效的从n-1行中的可选元素中选出最小的(同一列的不可选). 显然每个都对n-1行整行做比较是$n^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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func minFallingPathSum(grid [][]int) int {
    rowlen := len(grid)
    if rowlen == 1{
        return grid[0][0]
    }
    sums := [][]int{}
    firstrow := grid[0]
    sums = append(sums, firstrow)
    // grid[i][j] <= 99
    small, second, newsmall, newsecond := math.MaxInt32,math.MaxInt32,math.MaxInt32,math.MaxInt32
    for _, value := range firstrow{
        if value < small{
            second = small
            small = value
        }else if value < second{
            second = value
        }
    }
    for index, row := range grid[1:]{
        rowsum := []int{}
        newsmall, newsecond = math.MaxInt32, math.MaxInt32
        for col, value := range row{
            if sums[index][col] != small{
                thissum := small + value
                rowsum = append(rowsum, thissum)
                if thissum < newsmall{
                    newsecond = newsmall
                    newsmall = thissum
                }else if thissum < newsecond{
                    newsecond = thissum
                }
            }else{
                thissum := second + value
                rowsum = append(rowsum, thissum)
                if thissum < newsmall{
                    newsecond = newsmall
                    newsmall = thissum
                }else if thissum < newsecond{
                    newsecond = thissum
                }
            }
        }
        sums = append(sums, rowsum)
        small = newsmall
        second = newsecond
    }
    return small
}

总结

Beats 100%

day60 2024-04-27

514. Freedom Trail

In the video game Fallout 4, the quest “Road to Freedom” requires players to reach a metal dial called the “Freedom Trail Ring” and use the dial to spell a specific keyword to open the door.

Given a string ring that represents the code engraved on the outer ring and another string key that represents the keyword that needs to be spelled, return the minimum number of steps to spell all the characters in the keyword.

Initially, the first character of the ring is aligned at the “12:00” direction. You should spell all the characters in key one by one by rotating ring clockwise or anticlockwise to make each character of the string key aligned at the “12:00” direction and then by pressing the center button.

At the stage of rotating the ring to spell the key character key[i]:

You can rotate the ring clockwise or anticlockwise by one place, which counts as one step. The final purpose of the rotation is to align one of ring’s characters at the “12:00” direction, where this character must equal key[i].

If the character key[i] has been aligned at the “12:00” direction, press the center button to spell, which also counts as one step. After the pressing, you could begin to spell the next character in the key (next stage). Otherwise, you have finished all the spelling.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0427sXGin9o9zDmr.png

题解

本题是一道难题, 花了很长时间, 但理解的还是不够透彻, 关键在于保存什么样的状态是可以避免不必要的运算的. 保存的状态必须是在运算过程中必要的且若之前计算过则避免重复运算. 考虑本题中的关键, key中下一个字符要转动到12点方向只于当前12点方向的字符有关, 也就是只与当前12点方向的ring字符串中对应位置上的字符有关, 因此我们想要知道的是在ring字符串的该位置上的字符转动到key的下一个字符的最短距离是多少, ring中可能有多个key的下一个字符, 转动到这些字符中的哪个能获得全局最短距离在当前场景下是未知的, 因此最好的方法就是延迟决定, 递归的对所有可行位置执行搜索, 这样从上到下一层层深入直到key中最后一个字符, 再将各个路径上得到的距离做比较, 取最小的那个作为下一个转动到12点方向的字符位置, 这里在递归过程中要保存子递归中已经计算过的后面的ring对后面的key的字符的最短距离. 这里要理解保存的状态是在子递归过程中运算出来的一系列子结果, 这些子结果后面可能有用, 也可能没用, 但是算过了就要保存下来, 有没有用在比较高的递归层级是未知因素.

这里还有一些比较抽象的思考, 该题的解法中使用动态规划思想最小的子问题是什么, 其实是key中从任一个字符到下一个字符移动的最短距离, 可以将寻找路径的过程想象成一棵树, 每一层代表key中以层数为下标对应字符在ring串中的所有可行位置, 这样一棵非常庞大的演化树, 递归的过程就是遍历这棵树的所有到叶子节点的路径并保存该过程中的状态最终找到最短路. 理解这个状态空间是什么以及为什么保存这个状态是很高效的十分重要, 值得细细品味

代码

 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
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func findRotateSteps(ring string, key string) int {
	var m, n = len(ring), len(key)
	var pos = make(map[uint8][]int)
	for i := 0; i < len(ring); i++ {
		pos[ring[i]] = append(pos[ring[i]], i)
	}
	var mem = make([][]int, m)
	for i := range mem {
		mem[i] = make([]int, n)
		for j := range mem[i] {
			mem[i][j] = -1
		}
	}
	var dfs func(i, j int) int // ring[i] is currently at 12 o'clock , j stands for current key[j]
	dfs = func(i, j int) (res int) {
		if j == n {
			return
		}
		var p = &mem[i][j]
		if *p != -1 {
			return *p
		}
		defer func() { *p = res }()
		res = math.MaxInt
		for _, po := range pos[key[j]] { // scan all the position key[j] in ring
			res = min(res, dfs(po, j+1)+min(abs(i-po), m-abs(i-po))) // we need to make po at 12 o'clock by rotating ring clockwise or counterclockwise (choose the smaller one), and then plus dfs(po,j+1)
		}
		return
	}
	return n + dfs(0, 0) // every char in key needs to be pressed too
}

day61 2024-04-28

834. Sum of Distances in Tree

There is an undirected connected tree with n nodes labeled from 0 to n - 1 and n - 1 edges.

You are given the integer n and the array edges where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree.

Return an array answer of length n where answer[i] is the sum of the distances between the ith node in the tree and all other nodes.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0428xlxGuVh0UgVw.png

题解

考虑这棵树,其实将任意一个节点作为树的根节点都不影响结果, 这也意味着这个树中各个元素地位相同, 更像是一张无向图. 对于树中任意一个节点而言, 若将其看作根节点, 只要知道了其所有连通节点到连通节点的的"子树"的距离和和这个"子树"中包含的节点数量, 就能算出根节点到其他所有节点的距离和. 而求其连通节点到连通节点的子树的距离和可以继续采用这种思路, 显然可以通过递归的方式求解, 思想是动态规划的思想. 最直接的解法就是将这个递归式写出来, 同时保存在求解过程中解出的某节点对应的一个连通节点的"子树"的距离和. 但这种方法会超时, 因此需要思考哪里还有重复计算的部分, 考虑一个极端例子, 一共有30000个节点, 其中29999个节点都和节点0相连. 这种情况下, 如果依次遍历各个节点并计算其相邻节点的"子树"距离和, 会发现尽管已经保存了节点0到其余节点的距离和, 但还是要将0到剩余29998个节点都遍历一遍(不用计算,只直接拿结果), 这样显然是$n^2$的复杂度, 显然这里可以简化, 对于节点0, 已经计算得到了它到其余所有节点的距离和, 只需要使用这个距离和减掉从0到当前节点的"子树"包含的距离和再加上0剩余的连通节点的"子树"包含的节点数目(原本的距离是到节点0, 从节点0到当前节点要将每个节点距离加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
func sumOfDistancesInTree(n int, edges [][]int) []int {
    connected := [][]int{}
    dis := make([]map[int][]int,n)
    results := []int{}
    for i:=0;i<n;i++{
        connected = append(connected, []int{})
        dis[i] = map[int][]int{}
    }
    for _,edge := range edges{
        connected[edge[0]] = append(connected[edge[0]],edge[1])
        connected[edge[1]] = append(connected[edge[1]],edge[0])
    }

    var recusive func(int, int)(int,int)
    recusive = func(parent int, now int)(int,int){
        value, exist := dis[parent][now]
        if exist{
            return value[0], value[1]
        }else{
            if len(connected[now]) == 1{
                return 0,1
            }
            distance := 0
            numbers := 0
            for _, neibor := range connected[now]{
                if neibor != parent{
                    subdistance, number := recusive(now, neibor)
                    distance += subdistance + number
                    numbers += number
                }
            }
            numbers++
            newslice := []int{distance, numbers}
            dis[parent][now] = newslice
            return distance, numbers
        }
    }

    for index,neibors := range connected{
        result := 0
        for _, neibor := range neibors{
            if neibor < index {
                resultverse, numb := recusive(neibor,index)
                result += results[neibor] - resultverse - numb + n - numb

            }else{
                oneway, numbers := recusive(index, neibor)
                result += oneway + numbers
            }
        }
        results = append(results, result)
    }
    return results
}

优化版本:

 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
func dfs0(node int, parent int, t [][]int, dp []int, c []int) {
	n := len(t[node])
	c[node] = 1
	for i := 0; i < n; i++ {
		child := t[node][i]
		if child != parent {
			dfs0(child, node, t, dp, c)
			dp[node] += dp[child] + c[child]
			c[node] += c[child]
		}
	}
}

func dfs1(node int, parent int, sum int, t [][]int, dp []int, c []int, res []int) {
	res[node] = sum
	n := len(t[node])
	s := 0
	for i := 0; i < n; i++ {
		child := t[node][i]
		if child != parent {
			res[node] += dp[child] + c[child]
			s += dp[child] + c[child]
		}
	}
	for i := 0; i < n; i++ {
		child := t[node][i]
		if child != parent {
			dfs1(child, node, sum+(s-dp[child]-c[child])+(len(t)-c[child]), t, dp, c, res)
		}
	}
}

func sumOfDistancesInTree(n int, e [][]int) []int {
	t := make([][]int, n)
	for i := 0; i < len(e); i++ {
		u, v := e[i][0], e[i][1]
		t[u] = append(t[u], v)
		t[v] = append(t[v], u)
	}
	dp := make([]int, n)
	c := make([]int, n)
	dfs0(0, -1, t, dp, c)
	res := make([]int, n)
	dfs1(0, -1, 0, t, dp, c, res)
	return res
}

总结

看讨论区时发现, 这类题目叫作Tree rerooting DP, 的确在本题中计算距离和的过程相当于不断更换树的根节点. 下面这个视频中对这一问题有比较系统的讲解

https://www.youtube.com/watch?v=7_huTWwl5jM

了解下系统性的思路是好的, 但我向来不支持"套用", 要理解其内在的本质并根据实际问题灵活运用才是真正掌握. 就好像动态规划的"状态转移公式", 核心思想在于将大问题分解成小的子问题以及保存解决子问题过程中的信息(中间结果)用于后续处理, 正如我不断强调的核心观点, 利用的有效信息越多, 算法效率就越高.

day62 2024-04-29

2997. Minimum Number of Operations to Make Array XOR Equal to K

You are given a 0-indexed integer array nums and a positive integer k.

You can apply the following operation on the array any number of times:

Choose any element of the array and flip a bit in its binary representation. Flipping a bit means changing a 0 to 1 or vice versa. Return the minimum number of operations required to make the bitwise XOR of all elements of the final array equal to k.

Note that you can flip leading zero bits in the binary representation of elements. For example, for the number (101)2 you can flip the fourth bit and obtain (1101)2.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04298eOH147rsBGv.png

题解

本题最终对数组中所有的数进行异或运算, 得到目标k. 考虑到对k中的每一位而言, 数组中的数只需要在该位上做异或运算最终与k的该位相等. 对数组中的数, 对除最后一个数以外的所有数异或, 得到的结果再与最后一个数异或, 只需调整最后一个数的各个位使得最终结果与k相同. 因为调整任意一个数的某个位都是等价的, 而先将前面的数异或相当于保存了前面所有数中各个位异或后的信息. 将结果和最后一个数按位与k比较并调整即可. 只需注意如果一个数的后面的位比较完成前面全是0, 对对应的也要做相应调整.

代码

 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
func minOperations(nums []int, k int) int {
    numlen := len(nums)
    before := nums[0]
    last := 0
    if numlen == 1{
        last = 0
    }else{
        for _, value := range nums[1:numlen-1]{
            before ^= value
        }
        last = nums[numlen-1]
    }
    result := 0

    first := 0
    second := 0
    target := 0
    for before != 0 || last != 0 || k != 0{
        first = before % 2
        second = last % 2
        target = k % 2
        if first ^ second != target{
            result++
        }
        before /= 2
        last /= 2
        k /= 2
    }

    return result

}

总结

其实将所有数异或后直接和k进行异或, 得到的结果中如果某位为1则说明和k的这一位不同, 为0则相同, 将结果不断右移并判断最后一位是否为1即可得到需要翻转的次数.

day63 2024-04-30

1915. Number of Wonderful Substrings

A wonderful string is a string where at most one letter appears an odd number of times.

For example, “ccjjc” and “abab” are wonderful, but “ab” is not. Given a string word that consists of the first ten lowercase English letters (‘a’ through ‘j’), return the number of wonderful non-empty substrings in word. If the same substring appears multiple times in word, then count each occurrence separately.

A substring is a contiguous sequence of characters in a string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/04307MhnWDiJ486O.png

题解

这种子字符串的问题大概要保存一些状态以供后续的判断, 关键在于保存什么状态是有用的, 这里可以保存到某个下标为止的前缀字符串中各个字符的数量, 但是注意本题只要求判断字符串中出现了奇数次的字符的数量, 因此关键在于字符串中的某个字符的数量是否为奇数. 考虑这样的情况, 如果以下标3为结尾的字符串中字符’a’出现了奇数次, 以下标5为结尾的字符串中字符’a’也出现了奇数次, 那么这两个下标之间的字符串中’a’必定出现了偶数次(两奇数做差, 必定为偶数). 同样两偶数做差也必定为偶数, 只有偶数和奇数做差时才会出现奇数. 因此只需要保存到某个下标结尾处字符串中各个字符出现次数的奇偶性再与其他下标做比较, 字符奇偶性不同的字符小于2则二者之间的字符串是可行的, 否则不可行.

保存各个字符的奇偶性可以使用位标记, 从a到z每个字母用一个二进制位表示, 用一个十维数组也可行, 但在只需要保存一个二值状态时用位更节省空间. 求出到各个下标的各个字符的奇偶串后, 依次遍历每个位置, 分别与后面的所有位置做异或, 再判断异或后的数字的二进制表示中1的个数是否大于1个, 大于则这两个下标之间的字符串奇数个字符大于1, 判断是否大于一个可以使用n&(n-1)来消掉最低的1, 再判断剩余的数字是否为0, 不为0说明大于1个.

但使用这种方法, 每次都要将当前下标与后面的所有下标都比较一遍, 显然是$n^2$的复杂度. 这里可以发现, 使用这种方法遍历没有考虑到这种字符串差出现的先后位置与结果无关, 即差值的具体数量与结果无关. 例如, 在下标为1处的位串为0101, 在3和5处的位串都为0111, 实际上这两个位置与下标1做异或后的结果相同, 即1-3和1-5之间都只有1个位置是奇数个, 至于这个位置是3个或是5个, 或者3-5之间这个位置没发生变化其他位置多了两个, 都不影响区间字符串的奇偶性质. 这里的思想和用位来表示字符的奇偶性有异曲同工之妙, 即无需考虑具体数量, 只需考虑其拥有的性质即可.

有了这种想法, 那么只需要保存所有的位串, 并统计所有位串的数量, 如0101的位串有五个, 说明这五个位串代表的下标之间的四个间隙的所有字符都是偶数个那么这四个显然是符合要求的结果. 再计算有一个奇数位的情况, 只需要将每个串中的各个位依次翻转并查询翻转后的位串的数量即可. 每个串最多翻转10次(a-j 10个字符), 10位二进制串最多有1024种组合, 意味着最多翻转1024*10=10240次. 降为常数复杂度.

在翻转的过程中, 两种字符串之间翻转后的组合会被计数两次(0101翻转得到0111,0111翻转得到0101), 将最终结果除以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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func wonderfulSubstrings(word string) int64 {

    masks := map[int]int{}

    bytes := []byte(word)

    mask := 0

    masks[0] = 1

    for _, char := range bytes{

        mask ^= (1<<(char-'a'))

        masks[mask] = masks[mask] + 1

    }

    result := 0

    flip := 0

    fmt.Println(masks[0])

    for value, number := range masks{

        result += number * (number - 1) / 2

        for i:=0;i<10;i++{

            flip += masks[value ^ (1<<i)] * number



        }

    }



    return int64(result+flip/2)



}

总结

本题是一道比较综合的题目, 找到关键状态是解决问题的核心.

day64 2024-05-01

2000. Reverse Prefix of word

Given a 0-indexed string word and a character ch, reverse the segment of word that starts at index 0 and ends at the index of the first occurrence of ch (inclusive). If the character ch does not exist in word, do nothing.

For example, if word = “abcdefd” and ch = “d”, then you should reverse the segment that starts at 0 and ends at 3 (inclusive). The resulting string will be “dcbaefd”. Return the resulting string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0501LmcJwwQ9JiHr.png

题解

本题是一道简单题, 显然找到第一个ch的位置翻转字符串即可, 若想在找到后快速翻转字符串, 可以在找到对应字符的下标后除以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
func reversePrefix(word string, ch byte) string {
    bytes := []byte(word)
    first := 0
    for index, char := range bytes{
        if char == ch{
            first = index
            break
        }
    }

    mid := first/2
    copybytes := []byte(word)
    for index,_ := range copybytes[0:mid]{
        bytes[index] = copybytes[first-index]
        bytes[first-index] = copybytes[index]
    }

    if first % 2 == 1{
        bytes[mid] = copybytes[mid+1]
        bytes[mid+1] = copybytes[mid]
    }

    return string(bytes)



}

day65 2024-05-02

2441. Largest Positive Integer That Exists With Its Negative

Given an integer array nums that does not contain any zeros, find the largest positive integer k such that -k also exists in the array.

Return the positive integer k. If there is no such integer, return -1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0502lkM7WCBRSLZY.png

题解

本题是一道简单题, 题目中给出数字的范围为-1000到1000, 因此可以使用一个1001长度的数组来保存每个数字的状态, 状态分为未出现, 出现了一个负数, 出现了一个正数和出现过了这个数的正负两个数四种情况. 因此使用四个数字来表示这四种情况即可, 0表示未出现, -1表示只出现过负数, 1表示只出现过正数, 2表示一正一负, 遍历数组并改变相应状态, 当状态变为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
func findMaxK(nums []int) int {
    result := -1
    numbers := make([]int,1001)

    for _,value := range nums{
        if value > 0{
            if numbers[value] == -1{
                numbers[value] = 2
                result = max(result, value)
            }else if numbers[value] == 0{
                numbers[value] = 1
            }
        }else{
            if numbers[-value] == 1{
                numbers[-value] = 2
                result = max(result, -value)
            }else if numbers[-value] == 0{
                numbers[-value] = -1
            }
        }

    }

    return result
}

day 66 2024-05-03

165. Compare Version Numbers

Given two version numbers, version1 and version2, compare them.

Version numbers consist of one or more revisions joined by a dot ‘.’. Each revision consists of digits and may contain leading zeros. Every revision contains at least one character. Revisions are 0-indexed from left to right, with the leftmost revision being revision 0, the next revision being revision 1, and so on. For example 2.5.33 and 0.1 are valid version numbers.

To compare version numbers, compare their revisions in left-to-right order. Revisions are compared using their integer value ignoring any leading zeros. This means that revisions 1 and 001 are considered equal. If a version number does not specify a revision at an index, then treat the revision as 0. For example, version 1.0 is less than version 1.1 because their revision 0s are the same, but their revision 1s are 0 and 1 respectively, and 0 < 1.

Return the following:

If version1 < version2, return -1. If version1 > version2, return 1. Otherwise, return 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0503BlwzJn15HjHM.png

题解

本题将字符串根据.号分割成字符数组, 再将两个字符数组转换成整数并比较大小.

代码

 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
func compareVersion(version1 string, version2 string) int {
     ver1slice := strings.Split(version1, ".")
     ver2slice := strings.Split(version2, ".")

     len1 := len(ver1slice)
     len2 := len(ver2slice)

     shortlen := 0
     short := []string{}
     long := []string{}
     flag := 0
     if len1 > len2{
        short = ver2slice
        long = ver1slice
        shortlen = len2
        flag = 1
     }else{
        short = ver1slice
        long = ver2slice
        shortlen = len1
        flag = -1
     }

     for index, str := range short{
        result1,_ := strconv.Atoi(str)
        result2,_ := strconv.Atoi(long[index])
        if result1 < result2{
            return flag
        }else if result1 > result2{
            return -flag
        }
     }

     for _, str := range long[shortlen:]{
        result, _ := strconv.Atoi(str)
        if result != 0{
            return flag
        }
     }
     return 0


}

day67 2024-05-04

881. Boats to Save People

You are given an array people where people[i] is the weight of the ith person, and an infinite number of boats where each boat can carry a maximum weight of limit. Each boat carries at most two people at the same time, provided the sum of the weight of those people is at most limit.

Return the minimum number of boats to carry every given person.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0504OprsameBdg56.png

题解

本题中每艘船只能乘坐两个人, 则两个人的重量和应小于船的限制limit, 因此重量较小的人可以和重量较大的人乘同一艘船, 但重量大于limit/2的人只能独自乘船, 不能与他人共乘. 则先统计各个重量的人数, 放在数组中, 数组下标表示对应的重量. 值为对应的人数. 从头遍历数组, 对于重量小于limit/2的人, 可以和与其重量和为limit及以下的人共同乘船, 从能与其共同乘船的最大重量开始向前遍历, 直到遍历过的重量的人数和与当前重量人数相同为止. 这些人每两两一组可以共乘同一艘船. 如果还有剩余, 说明后面已经全部遍历完, 考虑当前重量小于等于limit/2, 故两个人可以共同乘船, 将结果加上剩余人数的1/2, 奇数再加上一即可. 对于遍历到重量大于limit/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
28
29
30
31
32
33
34
35
36
37
38
39
func numRescueBoats(people []int, limit int) int {
    peoples := make([]int, limit+1)
    for _, value := range people{
        peoples[value] = peoples[value] + 1
    }

    peoples[0] = peoples[limit]
    result := 0
    tail := 0
    index := 0
    value := 0
    half := limit/2
    for index <= limit{
        value = peoples[index]
        tail = limit-index
        for tail > index{
            value -= peoples[tail]
            if value > 0{
                result += peoples[tail]
                peoples[tail] = 0
                tail--
            }else{
                result += peoples[tail] + value
                peoples[tail] = -value
                value = 0
                break
            }
        }
        if index <= half{
            result += value/2 + (value%2)
        }else{
            result += value
        }
        index++
    }

    return result

}

总结

本题关键在于船的人数有限制, 只能两个人, 如果船的人数无限而重量有限的话, 要尽可能将船装满, 从后向前遍历时要根据当前遍历到的重量减去对应的比较轻的重量的人数直到将船装满为止.

day68 2024-05-05

237. Delete Node in a Linked List

There is a singly-linked list head and we want to delete a node node in it.

You are given the node to be deleted node. You will not be given access to the first node of head.

All the values of the linked list are unique, and it is guaranteed that the given node node is not the last node in the linked list.

Delete the given node. Note that by deleting the node, we do not mean removing it from memory. We mean:

The value of the given node should not exist in the linked list. The number of nodes in the linked list should decrease by one. All the values before node should be in the same order. All the values after node should be in the same order. Custom testing:

For the input, you should provide the entire linked list head and the node to be given node. node should not be the last node of the list and should be an actual node in the list. We will build the linked list and pass the node to your function. The output will be the entire list after calling your function.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0505H85m3c3E8Rdz.png

题解

本题是一道基本的链表操作问题, 给定要删除的节点指针, 则不知道节点的前置节点指针, 只能通过遍历链表并将将后一个节点的值赋给前一个节点的方式来删除掉(覆盖)当前节点的值. 注意对倒数第二个节点, 当将最后一个节点的值赋给它后将其Next指针置为nil从而删掉原来的最后一个节点.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func deleteNode(node *ListNode) {
    for node.Next.Next != nil{
        node.Val = node.Next.Val
        node = node.Next
    }
    node.Val = node.Next.Val
    node.Next = nil
}

day69 2024-05-06

2487. Remove Nodes From Linked List

You are given the head of a linked list.

Remove every node which has a node with a greater value anywhere to the right side of it.

Return the head of the modified linked list.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0506szTexdMUo5VW.png

题解

本题要删除的是链表右边存在比它值大的节点的节点. 最直接的思路就是将链表反向, 从头遍历, 保存当前遍历过的最大值, 小于该值的都删除, 大于则更新当前最大值, 删除完毕后再将链表反向, 就得到了删除后的结果.

代码

 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 singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func removeNodes(head *ListNode) *ListNode {
	newhead := reverse(head)
    curhead := newhead
    maxval := newhead.Val
    for newhead.Next != nil{
        if newhead.Next.Val < maxval{
            newhead.Next = newhead.Next.Next
        }else{
            maxval = newhead.Next.Val
            newhead = newhead.Next
        }
    }
    return reverse(curhead)


}

func reverse(head *ListNode) *ListNode {
	var pre *ListNode = nil
	cur := head
	for cur != nil {
		nextTemp := cur.Next
		cur.Next = pre
		pre = cur
		cur = nextTemp
	}
	return pre
}

day70 2024-05-07

2816. Double a Number Represented as a Linked List

You are given the head of a non-empty linked list representing a non-negative integer without leading zeroes.

Return the head of the linked list after doubling it

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0507RiUyH0JcuAtK.png

题解

和昨天类似的解题思路, 将链表逆转, 依次计算各位并保存进位, 修改值最后一个节点有进位增加一个新的值为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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func doubleIt(head *ListNode) *ListNode {
    newhead := reverse(head)
    curhead := newhead
    addon := 0
    for newhead.Next != nil{
        newhead.Val = newhead.Val * 2 + addon
        if newhead.Val >= 10{
            newhead.Val = newhead.Val - 10
            addon = 1
        }else{
            addon = 0
        }
        newhead = newhead.Next
    }
    newhead.Val = newhead.Val * 2 + addon
    if newhead.Val >= 10{
        newhead.Val = newhead.Val - 10
        newhead.Next = &ListNode{1,nil}
    }


    return reverse(curhead)

}

func reverse(head *ListNode) *ListNode{
    prev := head
    prev = nil
    next := head
    for head != nil{
        next = head.Next
        head.Next = prev
        prev = head
        head = next
    }
    return prev
}

总结

逆转其实是多余的, 每一位数仅取决于当前位的值和后一位是否有进位, 因此用两个指针分别指向当前位和下一位, 计算下一位的值若进位将进位加到当前位即可, 这里的核心思路在于将当前位的倍乘和进位的加两个操作分离了, 在当前位的上一位时执行当前位的倍乘操作, 在当前位下一位执行当前位的进位加. 因为进位不受后面的位的影响, 因此使用双指针即可快速解决.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */

func doubleIt(head *ListNode) *ListNode {

    head = &ListNode{Next: head}

    for curr, next := head, head.Next; next != nil; curr, next = next, next.Next {
        next.Val *= 2
        curr.Val += next.Val / 10
        next.Val %= 10
    }

    if head.Val == 0 {
        head = head.Next
    }

    return head
}

day71 2024-05-08

506. Relative Ranks

You are given an integer array score of size n, where score[i] is the score of the ith athlete in a competition. All the scores are guaranteed to be unique.

The athletes are placed based on their scores, where the 1st place athlete has the highest score, the 2nd place athlete has the 2nd highest score, and so on. The placement of each athlete determines their rank:

The 1st place athlete’s rank is “Gold Medal”. The 2nd place athlete’s rank is “Silver Medal”. The 3rd place athlete’s rank is “Bronze Medal”. For the 4th place to the nth place athlete, their rank is their placement number (i.e., the xth place athlete’s rank is “x”). Return an array answer of size n where answer[i] is the rank of the ith athlete.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0508E0Uhv5SM1DD8.png

题解

本题为简单题, 思路上比较清晰, 先排序, 然后保存对应score的排名, 再遍历一次数组, 根据score的排名赋予对应的rank即可, 特殊处理前三个即可.

代码

 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
func findRelativeRanks(score []int) []string {
    var copy []int
    copy = append(copy, score...)
    index, value := 0,0


    sort.Sort(sort.Reverse(sort.IntSlice(copy)))
    ranks := make([]int, 1000001)
    for index,value = range copy{
        ranks[value] = index+1
    }
    result := []string{}
    for _, value = range score{
        if ranks[value] == 1{
            result = append(result, "Gold Medal")
        }else if ranks[value] == 2{
            result = append(result, "Silver Medal")
        }else if ranks[value] == 3{
            result = append(result, "Bronze Medal")
        }else{
            result = append(result, strconv.Itoa(ranks[value]))
        }
    }
    return result


}

day72 2024-05-09

3075. Maximize Happiness of Selected Children

You are given an array happiness of length n, and a positive integer k.

There are n children standing in a queue, where the ith child has happiness value happiness[i]. You want to select k children from these n children in k turns.

In each turn, when you select a child, the happiness value of all the children that have not been selected till now decreases by 1. Note that the happiness value cannot become negative and gets decremented only if it is positive.

Return the maximum sum of the happiness values of the selected children you can achieve by selecting k children.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0509b7ce0zhtdOJ9.png

题解

本题思路上比较简单, 要求最大和, 将数组从大到小排序, 排序后依次取前k个元素加和即可, 注意处理每加一个元素, 下一个元素就要减掉目前已经加过的元素数量的值, 这可以通过数组下标来天然实现. 这类题目关键在于排序算法, 使用库中实现的排序算法一般都比较快. 但我们仍然要对常用的排序算法心中有数.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func maximumHappinessSum(happiness []int, k int) int64 {
    sort.Sort(sort.Reverse(sort.IntSlice(happiness)))
    result := 0
    temp := 0
    for index,value := range happiness{
        if index < k{
            temp = value - index
            if temp > 0{
                result += temp
            }
        }
    }
    return int64(result)
}

day73 2024-05-10

786. K-th Smallest Prime Fraction

You are given a sorted integer array arr containing 1 and prime numbers, where all the integers of arr are unique. You are also given an integer k.

For every i and j where 0 <= i < j < arr.length, we consider the fraction arr[i] / arr[j].

Return the kth smallest fraction considered. Return your answer as an array of integers of size 2, where answer[0] == arr[i] and answer[1] == arr[j].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0510L1MMRZ5XYJzh.png

题解

保存每个分数的值, 和对应的分子分母, 对对应的分数值排序, 取第k小的即可

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

func kthSmallestPrimeFraction(arr []int, k int) []int {
    type point struct{
        value float32
        num []int
    }
    points := []point{}
    length := len(arr)
    i := 0
    for index, value := range arr{
        float32val := float32(value)
        for i=index+1;i<length;i++{
            points = append(points, point{float32val/float32(arr[i]),[]int{value, arr[i]}})
        }
    }
    sort.Slice(points, func(i, j int)bool{return points[i].value < points[j].value})
    return points[k-1].num
}

总结

看到了一种很有趣的解法, 使用二分法, 每次计算出中间值右侧的分数的个数, 根据与k的相对大小更新中间值. 代码如下, 这种方法比其他许多解法快的多得多, 在最快的平均时长为694ms的情况下竟然只需用时4ms. 这种解法关键在于其通过一开始取中间值为0.5的方式大大减少了需要遍历的分数的数目. 而且没有遍历的分数在后续因为中间值的调整也不会再遍历了, 也就是说每一次遍历的数目都是原本遍历的分数个数的一小部分, 如果数量特别多的情况下可以近似的认为分数的值分布在0-0.5和0.5-1之间的分数个数大致相同. 这样从期望角度讲每次都只需要遍历一般个数的分数, 这样遍历过程总体的时间复杂度为O(nlogn), 并且不需要排序, 最终可以直接返回结果, 因此能有很高的效率.

 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
func kthSmallestPrimeFraction(arr []int, k int) []int {
    n := len(arr)
    left := 0.0
    right := 1.0
    result := make([]int, 2)

    for left < right {
        mid := (left + right) / 2
        count := 0
        maxFraction := [2]int{0, 1}

        for i, j := 0, 1; i < n-1; i++ {
            for j < n && float64(arr[i])/float64(arr[j]) > mid {
                j++
            }
            count += n - j
            if j < n && float64(arr[i])/float64(arr[j]) > float64(maxFraction[0])/float64(maxFraction[1]) {
                maxFraction = [2]int{arr[i], arr[j]}
            }
        }

        if count == k {
            return maxFraction[:]
        } else if count < k {
            left = mid
        } else {
            right = mid
        }
    }

    return result
}

day74 2024-05-11

857. Minimum Cost to Hire K Workers

There are n workers. You are given two integer arrays quality and wage where quality[i] is the quality of the ith worker and wage[i] is the minimum wage expectation for the ith worker.

We want to hire exactly k workers to form a paid group. To hire a group of k workers, we must pay them according to the following rules:

Every worker in the paid group must be paid at least their minimum wage expectation. In the group, each worker’s pay must be directly proportional to their quality. This means if a worker’s quality is double that of another worker in the group, then they must be paid twice as much as the other worker. Given the integer k, return the least amount of money needed to form a paid group satisfying the above conditions. Answers within 10-5 of the actual answer will be accepted.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0511yn4MGoLuDVDe.png

题解

本题是一道难题, 说实话, 本题只想到了可以用工资和工作量的比值来衡量员工这一步, 因为这种使用比值来决定如何选择的方式在这种同时有某个对象的价值和成本的场景下非常常见. 也就是常说的性价比. 但并没有想出来可以使用优先级队列来一直保存当前k个quality的最小和以及如何更新结果的步骤, 我陷入了一个误区即工资和工作量比值特别高的员工一定不会被选择, 实则不然, 考虑极端情况, 一个平均工作价值为7但工作量为200的员工, 我需要付给他1400, 而一个平均工作价值为200但工作量为1的员工我只需付给他200, 如果之前的员工工作量只有3那我总共需要付800, 显然小于另一个平均工作价值为7的员工. 以及对go优先级队列的实现不是很娴熟.

本题我查看了他人的题解, 🔗:https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/solutions/1815856/yi-bu-bu-ti-shi-ru-he-si-kao-ci-ti-by-en-1p00/

代码

 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
func mincostToHireWorkers(quality, wage []int, k int) float64 {

    type pair struct{ q, w int }

    pairs := make([]pair, len(quality))

    for i, q := range quality {

        pairs[i] = pair{q, wage[i]}

    }

    slices.SortFunc(pairs, func(a, b pair) int { return a.w*b.q - b.w*a.q }) // 按照 r 值排序



    h := hp{make([]int, k)}

    sumQ := 0

    for i, p := range pairs[:k] {

        h.IntSlice[i] = p.q

        sumQ += p.q

    }

    heap.Init(&h)



    ans := float64(sumQ*pairs[k-1].w) / float64(pairs[k-1].q) // 选 r 值最小的 k 名工人



    for _, p := range pairs[k:] { // 后面的工人 r 值更大

        if p.q < h.IntSlice[0] { // 但是 sumQ 可以变小,从而可能得到更优的答案

            sumQ -= h.IntSlice[0] - p.q

            h.IntSlice[0] = p.q

            heap.Fix(&h, 0) // 更新堆顶

            ans = min(ans, float64(sumQ*p.w)/float64(p.q))

        }

    }

    return ans

}



type hp struct{ sort.IntSlice }

func (h hp) Less(i, j int) bool { return h.IntSlice[i] > h.IntSlice[j] } // 最大堆

func (hp) Push(any)             {}

func (hp) Pop() (_ any)         { return }

day75 2024-05-12

2373. Largest Local Values in a Matrix

You are given an n x n integer matrix grid.

Generate an integer matrix maxLocal of size (n - 2) x (n - 2) such that:

maxLocal[i][j] is equal to the largest value of the 3 x 3 matrix in grid centered around row i + 1 and column j + 1. In other words, we want to find the largest value in every contiguous 3 x 3 matrix in grid.

Return the generated matrix.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0512hsexpYHaZP9c.png

题解

本题是深度学习中经典的最大池化操作, 用四层循环来完成即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func largestLocal(grid [][]int) [][]int {
    n := len(grid)
    maxLocal := make([][]int, n-2)

    for i := 0; i < n-2; i++ {
        maxLocal[i] = make([]int, n-2)
        for j := 0; j < n-2; j++ {
            max := grid[i][j]
            for k := 0; k < 3; k++ {
                for l := 0; l < 3; l++ {
                    if grid[i+k][j+l] > max {
                        max = grid[i+k][j+l]
                    }
                }
            }
            maxLocal[i][j] = max
        }
    }

    return maxLocal
}

day76 2024-05-13

861. Score After Flipping Matrix

You are given an m x n binary matrix grid.

A move consists of choosing any row or column and toggling each value in that row or column (i.e., changing all 0’s to 1’s, and all 1’s to 0’s).

Every row of the matrix is interpreted as a binary number, and the score of the matrix is the sum of these numbers.

Return the highest possible score after making any number of moves (including zero moves). https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/05132BP5UtTBDDVr.png

题解

考虑本题要求的是所有行的数的和. 因此某一行的值中某一位是0或者1对于和来说并不重要, 同一位上所有数中1的个数对和来说才比较重要. 只要某个位上的1的数量最多, 那么无论这些1分布在哪个数中, 最终的和都是最大的. 但每一行中的第一个1是比较特殊的, 考虑无论后面的位数怎么变动都不会超过最高位为1的数的大小, 因此最高位必须为1才能保证最大值.其余位按照列来翻转使得每一列的1的个数最多即可. 注意在遍历数组并执行首位为0的行进行行翻转的过程中可以记录每一列的1的个数, 行翻转后不需要再遍历数组, 只需根据每一列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
func matrixScore(grid [][]int) int {
    collen := len(grid[0])
    colonenum := make([]int, collen)
    rowlen := len(grid)
    result := 0
    for _, row := range grid{
        if row[0] == 0{
            for coli, value := range row{
                value = value ^ 1
                colonenum[coli] += value
            }
        }else{
            for coli, value := range row{
                colonenum[coli] += value
            }
        }
    }

    for index, value := range colonenum[1:]{
        value = max(value, rowlen-value)
        result += value * (1<<(collen-index-2))
    }
    result += rowlen * (1<<(collen-1))
    return result


}

day77 2024-05-14

1219. Path with Maximum Gold

In a gold mine grid of size m x n, each cell in this mine has an integer representing the amount of gold in that cell, 0 if it is empty.

Return the maximum amount of gold you can collect under the conditions:

Every time you are located in a cell you will collect all the gold in that cell. From your position, you can walk one step to the left, right, up, or down. You can’t visit the same cell more than once. Never visit a cell with 0 gold. You can start and stop collecting gold from any position in the grid that has some gold.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0514o5ln7hnoYDka.png

题解

本题需要找到所有的可行路径并比较每条路径上的金币和来求得最大值. 使用dfs结合回溯即可解决, 注意在数组上进行dfs时探索四个方向可以使用一个方向数组来表示方向, 每次将当前位置的坐标加上方向数组中的某一个方向并判断其是否超过边界及该方向是否有效(值大于0)即可. 注意对当前遍历的路径要先将当前坐标处的值置为0在dfs结束后要将值恢复便于后续回溯.

代码

 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
func getMaximumGold(grid [][]int) int {
    maxgold := 0
    rowlen := len(grid)
    collen := len(grid[0])
    var dfs func(int,int,[][]int)int
    direction := [][]int{{0,1},{0,-1},{1,0},{-1,0}}
    dfs = func(rowi int, coli int, grid [][]int)int{
        tempmax := 0
        temp := grid[rowi][coli]
        grid[rowi][coli] = 0
        for _, dir := range direction{
            temprow := rowi
            tempcol := coli
            temprow += dir[0]
            tempcol += dir[1]
            if temprow >= 0 && temprow < rowlen && tempcol >= 0 && tempcol < collen && grid[temprow][tempcol] > 0{
                tempmax = max(tempmax, dfs(temprow, tempcol, grid))
            }
        }
        grid[rowi][coli] = temp
        return tempmax+temp
    }


    for rowi, row := range grid{
        for coli, value := range row{
            if value != 0{
                maxgold = max(dfs(rowi, coli, grid),maxgold)
            }
        }
    }


    return maxgold
}

总结

其实上面代码还有优化的空间, 在遍历数组并走所有路径的过程中, 显然有些路径是重复的, 可以再使用一个三维数组保存二维数组中某一位向四个方向走的路径得到的四个方向的最大值. 这样在遇到重复路径时直接返回值即可. 不用再次递归遍历.

day78 2024-05-15

2812. Find the Safest Path in a Grid

You are given a 0-indexed 2D matrix grid of size n x n, where (r, c) represents:

A cell containing a thief if grid[r][c] = 1 An empty cell if grid[r][c] = 0 You are initially positioned at cell (0, 0). In one move, you can move to any adjacent cell in the grid, including cells containing thieves.

The safeness factor of a path on the grid is defined as the minimum manhattan distance from any cell in the path to any thief in the grid.

Return the maximum safeness factor of all paths leading to cell (n - 1, n - 1).

An adjacent cell of cell (r, c), is one of the cells (r, c + 1), (r, c - 1), (r + 1, c) and (r - 1, c) if it exists.

The Manhattan distance between two cells (a, b) and (x, y) is equal to |a - x| + |b - y|, where |val| denotes the absolute value of val.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0515hZwTUxsfESZf.png

题解

本题的安全因子定义为路径上的任一坐标到任何贼节点的最小曼哈顿距离. 因此应该从贼的节点出发, 通过BFS向外计算出每个节点到这个贼节点的距离, 通过对所有的贼节点进行BFS, 即可得到全部节点到任一贼节点的最小距离. 问题变为, 有了最小距离如何找到一条满足最小距离的可行路径. 可知我们要寻找的路径其路径上的所有点到贼的最小距离都应该大于或等于假设的最小距离, 否则这条路径的安全因子应该更小. 如何找到所有最小距离中可行的最大值? 可以通过从起点出发执行bfs寻找从(0,0)到(n-1,n-1)之间的可行路径, 在bfs过程中相邻节点的距离大于等于当前假设的最小距离节点才可达, 否则不可达, 如果相邻节点找不到可行路径则减小假设的最小值, 这里减小可以采用二分法进行增减. 如果找到了一条可行路径则增大假设的最小值再次判断是否有路径可达. 相当于在所有可行最小距离中二分查找到可行的最大值. 这样要比从最大的可行距离开始每次减少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
86
87
88
89
90
91
92
93
94
95
func maximumSafenessFactor(grid [][]int) int {
    rowlen := len(grid)
    collen := len(grid[0])
    stack := [][]int{}


    for rowi,row := range grid{
        for coli, value := range row{
            if value == 1{
                grid[rowi][coli] = 0
                stack = append(stack, []int{rowi,coli})
            }else{
                grid[rowi][coli] = -1
            }
        }
    }
    directions := [][]int{{-1,0},{1,0},{0,1},{0,-1}}
    depth := 1
    for len(stack) > 0{
        for _,point := range stack{
            temprow := point[0]
            tempcol := point[1]
            for _, dir := range directions{
                temprow = point[0]
                tempcol = point[1]
                temprow += dir[0]
                tempcol += dir[1]
                if temprow >=0 && temprow < rowlen && tempcol >= 0 && tempcol < collen && grid[temprow][tempcol] == -1{
                    grid[temprow][tempcol] = depth
                    stack = append(stack, []int{temprow,tempcol})
                }
            }
            stack = stack[1:]
        }
        depth++
    }

    max := 0
    for _,row := range grid{
        for _, value := range row{
            if value > max{
                max = value
            }
        }
    }

    start := 0
    mid := 0
    result := 0
    for start <= max{
        mid = (start + max) / 2
        if grid[0][0] < mid || grid[rowlen-1][rowlen-1] < mid{
            max = mid - 1
            continue
        }
        if validpath(grid, rowlen, mid){
            result = mid
            start = mid + 1
        }else{
            max = mid-1
        }
    }
    return result

}

func validpath(grid [][]int,rowlen int,mid int)bool{
    visited := make([][]bool, rowlen)
    for i:=0;i<rowlen;i++{
        visited[i] = make([]bool, rowlen)
    }
    stack := [][]int{{0,0}}
    directions := [][]int{{-1,0},{1,0},{0,1},{0,-1}}
    for len(stack) > 0{
        for _,point := range stack{
            temprow := point[0]
            tempcol := point[1]
            for _, dir := range directions{
                temprow = point[0]
                tempcol = point[1]
                temprow += dir[0]
                tempcol += dir[1]
                if temprow >=0 && temprow < rowlen && tempcol >= 0 && tempcol < rowlen && visited[temprow][tempcol] == false && grid[temprow][tempcol] >= mid{
                    if temprow == rowlen-1 && tempcol == rowlen - 1{
                        return true
                    }
                    visited[temprow][tempcol] = true
                    stack = append(stack, []int{temprow,tempcol})
                }
            }
            stack = stack[1:]
        }
    }
    return false
}

day 79 2024-05-16

2331. Evaluate Boolean Binary Tree

You are given the root of a full binary tree with the following properties:

Leaf nodes have either the value 0 or 1, where 0 represents False and 1 represents True. Non-leaf nodes have either the value 2 or 3, where 2 represents the boolean OR and 3 represents the boolean AND. The evaluation of a node is as follows:

If the node is a leaf node, the evaluation is the value of the node, i.e. True or False. Otherwise, evaluate the node’s two children and apply the boolean operation of its value with the children’s evaluations. Return the boolean result of evaluating the root node.

A full binary tree is a binary tree where each node has either 0 or 2 children.

A leaf node is a node that has zero children.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0516LXOTbf2QVjTC.png

题解

后续遍历计算各个节点的布尔值最终得到根节点的布尔值即可, 考查二叉树的后序遍历, 可以使用递归实现.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func evaluateTree(root *TreeNode) bool {
    result := caculatebool(root)
    return result
}

func caculatebool(father *TreeNode)bool{
    if father.Left == nil{
        return father.Val == 1
    }else {
        if father.Val == 2{
            return caculatebool(father.Left) || caculatebool(father.Right)
        }else if father.Val == 3{
            return caculatebool(father.Left) && caculatebool(father.Right)
        }
    }
    return false
}

day80 2024-05-17

1325. Delete Leaves With a Given Value

Given a binary tree root and an integer target, delete all the leaf nodes with value target.

Note that once you delete a leaf node with value target, if its parent node becomes a leaf node and has the value target, it should also be deleted (you need to continue doing that until you cannot).

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0517CoXoi4xFlfTN.png

题解

后序遍历即可, 使用递归遍历过程中返回当前节点是否被删除, 需要被删除返回true, 否则返回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
36
37
38
39
40
41
42
43
44
45
46
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func removeLeafNodes(root *TreeNode, target int) *TreeNode {
    if root == nil{
        return nil
    }
    result := deleteaNode(root, target)
    if result{
        return nil
    }else{
        return root
    }
}

func deleteaNode(father *TreeNode, target int)bool{
    deleted := true
    judge := false
    if father.Left != nil {
        judge = deleteaNode(father.Left, target)
        if judge{
            father.Left = nil
        }
        deleted = deleted && judge

    }
    if father.Right != nil{
        judge = deleteaNode(father.Right, target)
        if judge{
            father.Right = nil
        }
        deleted = deleted && deleteaNode(father.Right, target)
    }
    if deleted{
        if father.Val == target{
            return true
        }
    }

    return false
}

总结

直接返回节点会更简洁一些, 对于值符合的叶子节点, 递归时返回nil即可.

day81 2024-05-18

979. Distribute Coins in Binary Tree

You are given the root of a binary tree with n nodes where each node in the tree has node.val coins. There are n coins in total throughout the whole tree.

In one move, we may choose two adjacent nodes and move one coin from one node to another. A move may be from parent to child, or from child to parent.

Return the minimum number of moves required to make every node have exactly one coin. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0518KDVZu9LH37Ax.png

题解

考虑仅有一个根节点和两个叶子节点的情况. 因为最终要得到每个节点仅有一个硬币, 因此叶子节点多余的硬币一定会被传递上去, 没有硬币的节点一定能从父节点获得一个硬币. 将硬币传递上去和从父节点获得硬币都需要与硬币数相等的步数. 因此对于每个父节点, 分别递归计算两个子树能给父节点的硬币数, 需要硬币用负数表示, 同时计算两个子树将每个节点都变为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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func distributeCoins(root *TreeNode) int {
    _, steps := caculatecoin(root)
    return steps
}

func caculatecoin(father *TreeNode)(int, int){
    needcoinleft, stepsleft, needcoinright, stepsright := 0,0,0,0
    if father.Left == nil && father.Right == nil{
        if father.Val == 0{
            return -1,1
        }else if father.Val > 1{
            return father.Val-1, father.Val-1
        }
    }
    if father.Left != nil{
        needcoinleft, stepsleft = caculatecoin(father.Left)
    }
    if father.Right != nil{
        needcoinright, stepsright = caculatecoin(father.Right)
    }
    needcoin := needcoinleft + needcoinright + father.Val - 1
    steps := stepsleft + stepsright + abs(needcoin)
    return needcoin, steps

}

func abs(num int)int{
    if num < 0{
        return -num
    }
    return num
}

总结

本题的关键之一在于理解把多余的硬币传上去和从父节点拿硬币从从步数上来说是等价的. 另一重要的等价性就是父节点和一个子节点都有多余硬币, 另一个子节点需要硬币时, 无论是把父节点的硬币分配给子节点还是把另一个子节点多余的硬币分配给子节点, 最终对步数的贡献都是相同的. 因为若把父节点的硬币给子节点(1步), 另一个子节点的硬币要向上传递(2步), 总共要3步. 而将另一个子节点的硬币分给这个子节点(2步), 父节点硬币向上传递(1步)也需要3步. 所以对于每个子树来说, 重要的是其能传递或者需要多少硬币, 至于内部分配情况对于总步数没有影响.

day82 2024-05-19

3068. Find the Maximum Sum of Node Values

There exists an undirected tree with n nodes numbered 0 to n - 1. You are given a 0-indexed 2D integer array edges of length n - 1, where edges[i] = [ui, vi] indicates that there is an edge between nodes ui and vi in the tree. You are also given a positive integer k, and a 0-indexed array of non-negative integers nums of length n, where nums[i] represents the value of the node numbered i.

Alice wants the sum of values of tree nodes to be maximum, for which Alice can perform the following operation any number of times (including zero) on the tree:

Choose any edge [u, v] connecting the nodes u and v, and update their values as follows: nums[u] = nums[u] XOR k nums[v] = nums[v] XOR k Return the maximum possible sum of the values Alice can achieve by performing the operation any number of times.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0519eCBsOE5jrZYo.png

题解

本题为一道难题, 一个关键点在于意识到其实任意两个相邻节点可以同时与k进行异或运算等价于该树中任意两个节点可以同时与k进行异或运算而不影响其他节点的值, 原因在于对任一节点与k异或两次后会恢复原来节点的值, 因此这种与k进行异或的操作相当于具有传递性. 如a,b,c三个节点, a,b连通, b,c连通, a,b与k进行异或操作后, b,c再进行异或操作,此时b的值恢复为原来的值, 相当于a,c与k进行异或操作. 考虑这是一个连通图, 所有节点之间都有路径相连, 因此遍历所有节点将所有与k异或后值会变大的节点异或后的值保存下来, 如果有奇数个则找到与k异或后值变小的节点中减少的值最小的节点, 比较变大节点的增量和减少节点的减量, 根据值较大的节点进行操作. 在本图一定为连通图的情况下, 其实具体的边已经不重要了. 这种"操作"的传递性与昨天的题目在思路上有几分相似之处. 通过传递性去掉了相邻节点的限制, 进而可以直接通过全局的计算直接得到最终结果而不需要诸如动态规划等比较费时费空间的操作.

代码

 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
func maximumValueSum(nums []int, k int, edges [][]int) int64 {
    mindecrease := 0
    mindec := math.MaxInt
    decsum := 0
    incnum := 0
    minincrease := 0
    mininc := math.MaxInt
    incsum := 0
    result := 0
    for _,value := range nums{
        xor := value ^ k
        if xor > value{
            incamount := xor - value
            if incamount < mininc{
                mininc = incamount
                minincrease = xor
            }
            incsum += xor
            incnum++
        }else{
            decamount := value - xor
            if decamount < mindec{
                mindec = decamount
                mindecrease = value
            }
            decsum += value
        }
    }
    if incnum % 2 == 1{
        if mindec <= mininc{
            result = incsum + decsum - mindecrease + (mindecrease ^ k)
        }else{
            result = decsum + incsum - minincrease + (minincrease ^ k)
        }
    }else{
        result = incsum + decsum
    }
    return int64(result)

}

day83 2024-05-20

1863. Sum of All Subset XOR Totals

The XOR total of an array is defined as the bitwise XOR of all its elements, or 0 if the array is empty.

For example, the XOR total of the array [2,5,6] is 2 XOR 5 XOR 6 = 1. Given an array nums, return the sum of all XOR totals for every subset of nums.

Note: Subsets with the same elements should be counted multiple times.

An array a is a subset of an array b if a can be obtained from b by deleting some (possibly zero) elements of b.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0520iz2pn7M78pYk.png

题解

本题如何求出所有组合并且保存以前计算过的组合的状态是关键, 考虑只有两个数的情况, 一共有三个组合, 1,2,1-2. 在此基础上考虑三个数的情况,会发现共有7种组合, 1,2,1-2,3,1-3,2-3,1-2-3. 可以注意到, 除了3自己这种情况外, 其余包含3的情况是将3和只有两个数的所有情况组合. 因此可以得出规律, 对于前n+1个数, 可以将包含前n个数的所有组合与第n+1个数组合, 再加上第n+1个数自身即为前n+1个数的所有组合. 用数组保存到当前位置的所有已经计算过的组合即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func subsetXORSum(nums []int) int {
    save_state := []int{}
    result := 0
    for _, value := range nums{
        newstate := 0
        for _, saved := range save_state{
            newstate = value ^ saved
            result += newstate
            save_state = append(save_state, newstate)
        }
        result += value
        save_state = append(save_state, value)
    }
    return result
}

day84 2024-05-21

78. Subsets

Given an integer array nums of unique elements, return all possible subsets (the power set).

The solution set must not contain duplicate subsets. Return the solution in any order. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0521re4BxttZ1GeL.png

题解

本题在寻找全部的子数组的方法上和昨天题目的方法一致, 都是保存前n个数能构成的所有组合的集合,再与第n+1个数依次组合(空也算单独一个组合), 就得到了前n个数能构成的所有组合.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func subsets(nums []int) [][]int {
    result := [][]int{}
    result = append(result, []int{})
    for _, value := range nums{
        for _, subset := range result{
            newset := []int{}
            newset = append(newset, subset...)
            newset = append(newset, value)
            result = append(result, newset)
        }
    }
    return result
}

总结

还有一种通过dfs递归实现的方法也很有趣, 核心在于把握所有组合中原来数组中的数要么出现要么不出现, 那么就可以递归调用每次选择包含或者不包含下一个数, 通过将所有的包含与不包含组合, 就得到了所有的组合.

 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
var res [][]int

func subsets(nums []int) [][]int {
    res = nil
    if len(nums) == 0 {
        return res
    }

    var subset []int
    dfs(subset, nums)
    return res
}

func dfs(subset, nums []int) {
    if len(nums) == 0 {
        curr := make([]int, len(subset))
        copy(curr, subset)
        res = append(res, curr)

        return
    }

    //include
    included := append(subset, nums[0])
    dfs(included, nums[1:])

    //not include
    dfs(subset, nums[1:])
}

day85 2024-05-22

131. Palindrome Partitioning

Given a string s, partition s such that every substring of the partition is a palindrome . Return all possible palindrome partitioning of s.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0522DOnpFcYXV1k3.png

题解

本题需要枚举所有的子集分割并判断分割后的这些子集是否均为回文串. 这里可以使用回溯算法. 可以进行一些剪枝, 因为题目限制了所有字串都必须为回文串, 因此若产生的某个子串已经不是回文串则可以直接停止遍历其之后的串. 回溯算法可以使用递归实现. 本质上是一种深度优先搜索.

代码

 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
func partition(s string) [][]string {
    result := [][]string{}
    if len(s) == 1{
        return [][]string{[]string{s}}
    }
    for index, _ := range s[0:len(s)-1]{
        head := s[0:index+1]
        newtails := [][]string{}
        if ispalin(head){
            newtails = partition(s[index+1:])
            for _, tail := range newtails{
                newresult := []string{head}
                newresult = append(newresult, tail...)
                result = append(result, newresult)
            }
        }
    }
    if ispalin(s){
        result = append(result, []string{s})
    }
    return result
}

func ispalin(s string) bool{
    begin := 0
    tail := len(s) - 1
    for begin <= tail{
        if s[begin] != s[tail]{
            return false
        }
        begin++
        tail--
    }
    return true
}

day86 2024-05-23

2597. The Number of Beautiful Subsets

You are given an array nums of positive integers and a positive integer k.

A subset of nums is beautiful if it does not contain two integers with an absolute difference equal to k.

Return the number of non-empty beautiful subsets of the array nums.

A subset of nums is an array that can be obtained by deleting some (possibly none) elements from nums. Two subsets are different if and only if the chosen indices to delete are different.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0523SGYugmpe3CLf.png

题解

本题和求数组的全部子集有一些相似之处, 先排序, 排序可以使得在找与当前数字绝对值相差k的数时只需要看前面的数即可. 保存到当前位置处得到的全部合格的子数组(漂亮的). 在继续遍历时将新的数与之前的所有合格子数组比较, 看原来的子数组中是否有与新的数冲突的数. 若有则不能与新的数组合, 没有则可以组合. 这里为了快速寻找是否冲突, 可以用数组来保存之前的合格子数组, 将之前合格子数组中所有的数作为数组的下标.下标对应位置的值为1. 不包含的数的下标对应位置为0. 考虑数组中的数最大为1000. 这种方法是可以保存所有数的情况的.

代码

 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
func beautifulSubsets(nums []int, k int) int {
    sort.Ints(nums)
    result := 0
    subsets := [][]int{}
    new := make([]int,32)
    subsets = append(subsets, new)

    for _, value := range nums{
        if value <= k{
            for _, set := range subsets{
                newset := []int{}
                newset = append(newset, set...)
                newset[value/32] = newset[value/32] | 1 << (value % 32)
                subsets = append(subsets, newset)
                result++
            }
        }else{
            for _,set := range subsets{
                if (set[(value-k)/32] & (1 << ((value-k) % 32))) == 0{
                    newset := []int{}
                    newset = append(newset, set...)
                    newset[value/32] = newset[value/32] | 1 << (value % 32)
                    subsets = append(subsets, newset)
                    result++
                }
            }
        }
    }
    return result
}

总结

该方法时间复杂度比较高, 可参见下面的题解中讲解的动态规划解法, 这里要思考为什么同余的性质如此重要. 因为同余将数组中的数按照不同的性质分为了不同的组, 不同的组的数一定不会相差k因此可以随意组合. 最终结果相当于在这些组中任意选取组内一个可行组合进行组间组合. 这样通过将所有组的组内可行方案相乘就能得到最终结果. 而在组内, 通过排序, 因此各个数都同余, 只有相邻的数可能相差k, 不相邻的相差一定是k的2倍及以上, 这样就能用动态规划来求得组内的全部方案, 可以把问题转换为子问题, 如果不相邻的数也可能相差k, 那么问题就很难用动态规划来解决, 因为不知道什么时候才能解决一个子问题, 把相差k限制在相邻数, 我们就知道只要经过了这个相邻的数, 前面的数对后面一定没有影响了, 就解决了一个子问题, 这样相当于隐含携带了前面的数的信息. https://leetcode.cn/problems/the-number-of-beautiful-subsets/solutions/2177818/tao-lu-zi-ji-xing-hui-su-pythonjavacgo-b-fcgs/

day87 2024-05-24

1255. Maximum Score Words Formed by Letters

Given a list of words, list of single letters (might be repeating) and score of every character.

Return the maximum score of any valid set of words formed by using the given letters (words[i] cannot be used two or more times).

It is not necessary to use all characters in letters and each letter can only be used once. Score of letters ‘a’, ‘b’, ‘c’, … ,‘z’ is given by score[0], score[1], … , score[25] respectively.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0524pJqWG7GqQaky.png

题解

本题使用回溯算法, 遍历所有可能的组合并剪枝即可. 首先扫描保存所有可用的字母的个数. 循环当前的words数组, 判断当前词是否能由可用字母组成并计算score, 不能组成则跳到下一个词, 计算完成当前词的score后递归调用计算后面的词组成的词数组用剩余的可用字母能得到的最大值. 将其与当前词的score加和并更新全局最大值, 继续遍历下一个词. 注意这里的递归可能有重复状态, 但大部分是不重复的, 如第一个词用掉了两个a, 后面的词是在可用字母少了两个a的情况下的最大值. 这样求得的是包含第一个词的情况下能得到的最大值. 继续向后遍历, 此时求的是不包含第一个词的情况下的最大值, 意味着虽然也是求除第一个词之外数组中所有词能组成的最大值, 但此时能用全部可用字母, 刚才则是求少了两个a的情况, 因此得到的结果未必相同. 因为这两个a的影响我们不能预先知道, 所以不能直接记忆化上一次的结果拿来用. 可能的优化就是在剩余可用字母完全相同的情况下求包含相同的词的数组能取得的最大值可以记忆化.

代码

 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
func maxScoreWords(words []string, letters []byte, score []int) int {
    num := make([]int, 26)
    for _, char := range letters{
        num[char - 'a']++
    }
    maxscore := maxscoreword(words, num, score)
    return maxscore

}

func maxscoreword(words []string, num []int, score []int) int{
    maxscore := 0
    for index, word := range words{
        tempnum := []int{}
        tempnum = append(tempnum, num...)
        tempmax := 0
        flag := 0
        for _, char := range word{
            bytechar := byte(char)
            if tempnum[bytechar - 'a'] <= 0{
                flag = 1
                break
            }else{
                tempnum[bytechar - 'a']--
                if tempnum[bytechar - 'a'] < 0{
                    flag = 1
                    break
                }else{
                    tempmax += score[bytechar - 'a']
                }
            }
        }
        if flag == 1{
            continue
        }else{
            tempmax += maxscoreword(words[index+1:], tempnum, score )
            maxscore = max(maxscore, tempmax)
        }
    }
    return maxscore
}

day89 2024-05-25

140. Word Break II

Given a string s and a dictionary of strings wordDict, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences in any order.

Note that the same word in the dictionary may be reused multiple times in the segmentation. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0525rcv0Aguf2aYM.png

题解

为了快速检索分割出来的某个单词是否在词典中, 要先将词典数组转换为哈希表. 还可以注意到, 从原始字符串的某个下标开始能得到的所有可行分割的词的组合是固定的. 则可以扫描字符串, 分割出一个有效单词, 再对剩余字符串调用递归函数获取所有剩余字符串的可行分割, 在递归获取当前串的所有可行分割的过程中将当前下标对应的所有可行分割保存到全局记忆数组中. 后续若再次遇到从该下标开始分割, 直接从记忆数组中获取以该下标开始的所有可行分割串返回即可.

代码

 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
func wordBreak(s string, wordDict []string) []string {
    words := map[string]bool{}
    for _, word := range wordDict{
        words[word] = true
    }
    memorybreak := make([][]string, 21)
    result := breaks(s, words, memorybreak, 0)
    return result
}

func breaks(s string, words map[string]bool, memorybreak [][]string, pointer int)[]string{
    if len(memorybreak[pointer]) > 0{
        return memorybreak[pointer]
    }else{
        result := []string{}
        for index,_ := range s{
            if words[s[:index+1]] == true{
                if index == len(s)-1{
                    result = append(result, s)
                    return result
                }
                tails := breaks(s[index+1:], words, memorybreak, pointer+index+1)
                if len(tails) <= 0{
                    continue
                }
                for _,tailstring := range tails{
                    head := ""
                    head = head + s[:index+1] + " " + tailstring
                    result = append(result, head)
                }
            }
        }
        memorybreak[pointer] = result
        return result
    }
}

day90 2024-05-26

552. Student Attendance Record II

An attendance record for a student can be represented as a string where each character signifies whether the student was absent, late, or present on that day. The record only contains the following three characters:

‘A’: Absent. ‘L’: Late. ‘P’: Present. Any student is eligible for an attendance award if they meet both of the following criteria:

The student was absent (‘A’) for strictly fewer than 2 days total. The student was never late (‘L’) for 3 or more consecutive days. Given an integer n, return the number of possible attendance records of length n that make a student eligible for an attendance award. The answer may be very large, so return it modulo 109 + 7.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0526mSztEswHh8kF.png

题解

本题为一道难题, 一看有不同的状态, 状态还受到一些限制, 显然首先想到动态规划. 关键在于如何从子问题到当前问题. 首先A只能出现一次, 所以可以通过A0和A1表示对A是否出现这一状态的记忆. L不能连续出现三次, 则L出现可以用L1和L2表示L连续出现1次和2次. P表示正常的出勤. 那么全部状态可以表示成如下图所示的6种(图中下标b表示前一天). 接下来考虑对于每一种情况(A,L,P)其出现时与前一天这6种状态中的哪些有关. 可以看到对于出现L如果当前L连续出现了两次并且A从未出现过, 那么这种状态只有在前一天也为L并且A从未出现过才会发生. 如果L出现了一次, 那么在这之前的状态肯定是非L的即为P, A从未出现过, 则状态数与A0P相同. 对于A0P来说, 其表示当前采用的字母为P, A从未出现, 则无论前一天状态是P还是L已经连续出现了1次或2次今天的状态都可以选择P. 这样将今天的状态与前一天的状态的关系对应起来即可得到只与前一天状态有关的状态转移方式. 实现这一状态转移过程即可. 注意在转移的过程中要及时的将存在加和的转移过程模10^9+7因为数字很快就会变得很大, 而中间模与最后模结果相同. 这样可以避免计算过程的数字溢出.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/05265BDDZlIMG_7DF7686EEBC2-1.jpeg

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func checkRecord(n int) int {
    // 使用两个固定大小的数组交替使用
    before := [6]int{1, 0, 1, 0, 0, 1}
    now := [6]int{}

    for i := 1; i < n; i++ {
        now[0] = before[2]
        now[1] = before[0]
        now[2] = (before[0] + before[1] + before[2]) % 1000000007
        now[3] = before[5]
        now[4] = before[3]
        now[5] = (before[0] + before[1] + before[2] + before[3] + before[4] + before[5]) % 1000000007
        // 交换 before 和 now
        before, now = now, before
    }

    return (before[0] + before[1] + before[2] + before[3] + before[4] + before[5]) % 1000000007
}

day91 2024-05-27

1608. Special Array With X Elements Greater Than or Equal X

You are given an array nums of non-negative integers. nums is considered special if there exists a number x such that there are exactly x numbers in nums that are greater than or equal to x.

Notice that x does not have to be an element in nums.

Return x if the array is special, otherwise, return -1. It can be proven that if nums is special, the value for x is unique.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0527iTr6Az7fGaKl.png

题解

最简单的思路就是找到数组中的最大值, 再从最大值开始向下遍历各个整数, 并遍历数组查看是否满足条件. 但显然这样做时间复杂度很高. 如果先将数组排序, 则任一下标处的数后面的数必然大于等于这个数本身, 此时就可以用到数组末尾的距离来表示大于等于这个数的个数. 此时可以从后到前依次遍历并将当前下标到末尾的距离设为x, 并判断x是否大于前面的数, 如果大于则x满足条件. 对于这样在有序数组中依次遍历查找满足条件的x的值的问题都可以用二分法来加速解决. 设初始start为0, end为数组末尾下标, mid为二者和的一半. 因为题目中要求大于等于x的数的个数为x, 可以假设mid到数组末尾的距离为x, 判断这个数前面的数是否小于x并且当前mid下标对应的数大于等于x即可. 如果都满足则x为所求, 否则根据条件更新start或end的值.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0527aTgFw8IMG_D9B340F7B960-1.jpeg

代码

 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
func specialArray(nums []int) int {
    sort.Ints(nums)
    length := len(nums)
    start := 0
    end := length - 1
    mid := 0
    x := 0
    last := 0
    for start <= end{
        mid = (start + end) / 2
        x = length - mid
        if mid == 0{
            last = 0
        }else{
            last = nums[mid-1]
        }
        if x <= nums[mid] && x > last{
            return x
        }else if x > nums[mid]{
            start = mid + 1
        }else if x <= last{
            end = mid - 1
        }
    }
    return -1
}

day92 2024-05-28

1208. Get Equal Substrings Within Budget

You are given two strings s and t of the same length and an integer maxCost.

You want to change s to t. Changing the ith character of s to ith character of t costs |s[i] - t[i]| (i.e., the absolute difference between the ASCII values of the characters).

Return the maximum length of a substring of s that can be changed to be the same as the corresponding substring of t with a cost less than or equal to maxCost. If there is no substring from s that can be changed to its corresponding substring from t, return 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0528RccbVPDYGNL4.png

题解

本题先计算出两个字符串各个位上的差值, 再使用滑动窗口找到窗口内和小于maxCost的最长窗口即可.

代码

 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
func equalSubstring(s string, t string, maxCost int) int {
    differs := []int{}
    length := len(s)
    differ := 0
    for i,_ := range s{
        differ = abs(s[i], t[i])
        differs = append(differs, differ)
    }
    fmt.Println(differs)

    begin := 0
    end := 0
    cost := 0
    result := 0
    for end < length{
        // fmt.Println(end)
        cost += differs[end]
        if cost <= maxCost{
            result = max(result, end-begin+1)
        }else{
            for begin<=end{
                cost -= differs[begin]
                begin++
                if cost <= maxCost{
                    break
                }
            }
        }
        end++
    }
    return result
}

func abs(i byte, j byte)int{
    if i > j{
        return int(i - j)
    }else{
        return int(j - i)
    }
}

day93 2024-05-29

1404. Number of Steps to Reduce a Number in Binary Representation to One

Given the binary representation of an integer as a string s, return the number of steps to reduce it to 1 under the following rules:

If the current number is even, you have to divide it by 2.

If the current number is odd, you have to add 1 to it.

It is guaranteed that you can always reach one for all test cases. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0529LWtUuBN152FA.png

题解

本题要注意寻找规律, 因为题目一定有解, 因此最后一定会变为类似于10000…这样的形式, 即开头为1, 后面全部为0, 这样在不断除以2最终才能得到1. 我们只需考虑对于后面的1, 需要几次给最后一位的1加1操作最终能得到1000…这样的形式. 也就是说, 我们整体考虑题目中给出的两种操作, 本来应该是按照条件奇数加一, 偶数除以二, 偶数除以二实际上就是右移一位, 使得0的数量减少一个, 现在相当于先执行所有的奇数加1操作(这里可以理解为每次给最后一位1加一, 使其进位), 将数字化为1000…这样的形式后, 0的个数就是需要除以二的次数. 观察可以发现, 需要执行加1操作的次数与第一个1和最后一个1之间的0的个数有关, 需要加1的次数正好与首末1之间的0的个数相差1. 这点可以理解为, 通过一系列的加1操作将首末1之间的0移到了整个字符串的最后面, 最终得到开头为连续的1的串, 再加1就能得到以1开头,其余全为0 的串. 如图说明了这个过程. 理解了这一点, 只需要扫描串, 记录首末1之间的0的个数, 最终结果即为首末1之间0的个数加1与串的长度的和. 注意处理串本身就是开头为连续1和串本身即为只有开头为1这两种情况. 只有开头为1直接返回长度减1, 存在大于1个的连续1则为串的长度加1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0529hp7zXBIMG_78E8EB0BD2BE-1.jpeg

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func numSteps(s string) int {
    zeros := 0
    validzeros := 0
    ones := 0
    length := len(s)
    for i,_ := range s{
        if s[i] == '1'{
            ones++
            zeros = validzeros
        }else{
            validzeros++
        }
    }
    if zeros == 0{
        if ones == 1{
            return length - 1
        }else{
            return length + 1
        }
    }else{
        return zeros + length + 1
    }
}

总结

本题直接从最后一位开始向前遍历, 遇到0直接将结果加1, 遇到1设定一个标记进位的标记变量为1, 同时将结果加2, 如此重复即可. 注意在每次判断当前位是0还是1之前要将原本的当前位值加上进位的标记变量再判断.

day94 2024-05-30

1442. Count Triplets That Can Form Two Arrays of Equal XOR

Given an array of integers arr.

We want to select three indices i, j and k where (0 <= i < j <= k < arr.length).

Let’s define a and b as follows:

a = arr[i] ^ arr[i + 1] ^ … ^ arr[j - 1] b = arr[j] ^ arr[j + 1] ^ … ^ arr[k] Note that ^ denotes the bitwise-xor operation.

Return the number of triplets (i, j and k) Where a == b.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0530uVcNw3JX4b49.png

题解

题目中要求i-j和j-k之间的数组元素全部异或后的值相等, 而i<j<=k. 两个相同的值异或后的值为0, 因此可以得出结论, i-k之间的数异或后的值为0(i-j,j-k范围内的数异或后的值相同). i-k区间内的数异或后的值为0, 就意味着从头开始到i和从头开始到k这两个区间的数异或的值相等(不一定为0). 遍历数组, 计算到当前下标处的异或值. 并将异或值作为key, 下标作为value保存到map中, 计算得到异或值后查询map, 找到与当前下标异或值相等的全部下标, 并将这些区间内的所有可行分割(长度为n的区间有n-1种分割方法, 直观理解放木板到这个区间的数字之间, 一共能放几块就是几种分割方法)数目添加到结果中.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func countTriplets(arr []int) int {
    xormap := map[int][]int{}
    xormap[0] = []int{-1}
    xornow := 0
    result := 0
    beforeslice := []int{}
    for index, value := range arr{
        xornow = xornow ^ value
        beforeslice = xormap[xornow]
        if len(beforeslice) == 0{
            xormap[xornow] = []int{index}
        }else{
            for _, sameindex := range beforeslice{
                result += index - sameindex - 1
            }
            xormap[xornow] = append(xormap[xornow], index)
        }
    }
    return result
}

day95 2024-05-31

260. Single Number III

Given an integer array nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once. You can return the answer in any order.

You must write an algorithm that runs in linear runtime complexity and uses only constant extra space.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0531KWIXkzWwSPOp.png

题解

本题之前有一个前置问题, 即找到所有数中仅有一个出现了一次的数. 这个问题通过位运算的技巧, 两个相同的数异或之后得到的结果为0. 而唯一一个只出现了一次的数最终会被剩下. 那么对应有两个数都只出现了一次的情况怎么处理, 首先类似的,通过将所有的数异或最终实际上相当于只剩下两个数异或. 此时分辨不出来两个数分别是什么, 但仍保存了一些信息, 因为数字不同的位异或后的结果为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
func singleNumber(nums []int) []int {
    if len(nums) == 2{
        return nums
    }
    result := 0
    for _, value := range nums{
        result = result ^ value
    }
    bit := 0
    for i:=0;i<32;i++{
        if ((result >> i) & 1) == 1{
            bit = i
        }
    }
    result1 := 0
    result2 := 0
    for _, value := range nums{
        if ((value >> bit) & 1) == 1{
            result1 = result1 ^ value
        }else{
            result2 = result2 ^ value
        }
    }
    return []int{result1, result2}
}

day96 2024-06-01

3110. Score of a String

You are given a string s. The score of a string is defined as the sum of the absolute difference between the ASCII values of adjacent characters.

Return the score of s.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0601IP0RDAtOoMHP.png

题解

比较基础, 按照题目要求计算相邻字符的差的绝对值之和即可

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func scoreOfString(s string) int {
    result := 0
    for i,_ := range s[1:]{
        if s[i] < s[i+1]{
            result += int(s[i+1] - s[i])
        }else{
            result += int(s[i] - s[i+1])
        }
    }
    return result
}

day97 2024-06-02

344. Reverse String

Write a function that reverses a string. The input string is given as an array of characters s.

You must do this by modifying the input array in-place with O(1) extra memory.

Example 1:

Input: s = [“h”,“e”,“l”,“l”,“o”] Output: [“o”,“l”,“l”,“e”,“h”]

Example 2:

Input: s = [“H”,“a”,“n”,“n”,“a”,“h”] Output: [“h”,“a”,“n”,“n”,“a”,“H”]

题解

将字符串逆转也是一道简单题, 要求只能使用常数额外空间且直接在原始字符串上逆转, 因为原始字符串以数组形式保存, 可以分别从首尾开始向中间遍历数组并交换首尾的字符即可完成逆转操作.

代码

1
2
3
4
5
6
func reverseString(s []byte)  {
    length := len(s) - 1
    for i:=0;0+i <= length-i;i++{
        s[i], s[length-i] = s[length-i], s[i]
    }
}

day98 2024-06-03

2486. Append Characters to String to Make subsequence

You are given two strings s and t consisting of only lowercase English letters.

Return the minimum number of characters that need to be appended to the end of s so that t becomes a subsequence of s.

A subsequence is a string that can be derived from another string by deleting some or no characters without changing the order of the remaining characters.

Example 1:

Input: s = “coaching”, t = “coding” Output: 4 Explanation: Append the characters “ding” to the end of s so that s = “coachingding”. Now, t is a subsequence of s (“coachingding”). It can be shown that appending any 3 characters to the end of s will never make t a subsequence.

Example 2:

Input: s = “abcde”, t = “a” Output: 0 Explanation: t is already a subsequence of s (“abcde”).

Example 3:

Input: s = “z”, t = “abcde” Output: 5 Explanation: Append the characters “abcde” to the end of s so that s = “zabcde”. Now, t is a subsequence of s (“zabcde”). It can be shown that appending any 4 characters to the end of s will never make t a subsequence.

题解

本题从字符串s中删除任意位置的字符后再在末尾添加任意字符得到t, 这里从s中删除字符的位置是任意的,也就意味着只要在s中从头遍历并找到t的一个最长可行前缀即可。再用t的长度减去s中已经存在的前缀的长度级可得到需要补充的后缀长度。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func appendCharacters(s string, t string) int {
    prefix := 0
    tlen := len(t)
    t = t + "'"
    for i,_ := range s{
        if s[i] == t[prefix]{
            prefix++
        }
    }
    return tlen-prefix
}

day99 2024-06-04

409. Longest Palindrome

Given a string s which consists of lowercase or uppercase letters, return the length of the longest palindrome that can be built with those letters.

Letters are case sensitive, for example, “Aa” is not considered a palindrome.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0604VbbEvdynF0Yj.png

题解

本题也是一道简单题, 最近几天的每日一题都比较简单, 因为是能组成的最长回文串, 与顺序无关, 因此只需统计字母个数即可. 遍历字符串并统计字母, 每当同一个字母有两个时就将结果加2并将字母个数重置为0. 遍历一遍后如果仍有字母剩余1个,则将结果加1表示奇数长度的回文串在中间添加的字母.

先统计全部字母的数量再最终求和会快一些, 减少了不必要的判断和加法操作.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func longestPalindrome(s string) int {
    charslice := make([]int,52)
    for i,_ := range s{
        if s[i] >= 'a'{
            charslice[s[i]-'a']++
        }else{
            charslice[26+s[i]-'A']++
        }
    }
    result := 0
    odd := 0
    for _,num := range charslice{
        if num % 2 == 0{
            result += num
        }else{
            result += num - 1
            odd = 1
        }
    }
    return result + odd
}

day100 2024-06-05

1002. Find Common Characters

Given a string array words, return an array of all characters that show up in all strings within the words (including duplicates). You may return the answer in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0605COPNsAMdo0fH.png

题解

本题也是一道简单题, 仍然是寻找在所有单词中都存在的字母, 重复的字母单独计算, 同样不要求顺序, 对于没有顺序要求的字符串问题, 计数往往是很好的解决方法, 因此只需依次遍历所有单词, 对单词中所有字母计数, 再与之前的字母计数比较, 取二者之间的较小值, 最后根据每个字母的计数个数输出答案即可. 注意题目限制字符串长度最多为100, 因此初始化为101保存字母个数的数组即可正常被更小的值刷新.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func commonChars(words []string) []string {
    beforechars := []int{101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101,101}
    for _,word := range words{
        nowchars := make([]int,26)
        for i,_ := range word{
            nowchars[word[i]-'a']++
        }
        for i,value := range nowchars{
            beforechars[i] = min(beforechars[i],value)
        }
    }
    fmt.Println(beforechars)
    result := []string{}
    for i,chars := range beforechars{
        if chars > 0{
            for chars>0{
                result = append(result, string('a'+i))
                chars--
            }
        }
    }
    return result
}

day101 2024-06-06

846. Hand of Straights

Alice has some number of cards and she wants to rearrange the cards into groups so that each group is of size groupSize, and consists of groupSize consecutive cards.

Given an integer array hand where hand[i] is the value written on the ith card and an integer groupSize, return true if she can rearrange the cards, or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0606YDapQlDOY3WG.png

题解

因为要将数组中的数分为几组连续相邻的数且组内个数为groupSize大小, 显然每次都扫描整个数组寻找当前数字的下一个相邻数字效率极低, 所以可以先排序, 排序后数组中的数从小到大排列, 如果相邻的数存在应该在数组中是相邻的, 因此从头遍历排序后的数组, 依次将相邻的数放入一个groupSize大小的判别数组中并在数组中删掉放入的数, 遇到相同的数先继续向后遍历寻找下一个不同的相邻数字, 直到填满整个判别数组为止, 如果找不到相邻的数则可直接返回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
func isNStraightHand(hand []int, groupSize int) bool {
    sort.Ints(hand)
    arrangelen := 0
    current := 0
    for len(hand) > 0{
        arrangelen = 0
        remaingroup := []int{}
        current = hand[0]
        for _, value := range hand{
            if current == value && arrangelen < groupSize{
                arrangelen++
                hand = hand[1:]
                current++

            }else if arrangelen == groupSize{
                break
            }else{
                if value == current - 1{
                    hand = hand[1:]
                    remaingroup = append(remaingroup, value)
                }else{
                    return false
                }
            }
        }
        hand = append(remaingroup, hand...)
    }
    if arrangelen == groupSize{
        return true
    }else{
        return false
    }
}

总结

这种算法是可行的, 但由于大量的数组删除元素和数组连接操作, 使得实际耗时比较长. 本题还可以使用优先级队列(最小堆)来求解, 将元素全部放入最小堆中, 构造一个哈希表, 保存每个元素和其对应的元素个数. 每次从堆顶弹出一个元素, 即当前的最小元素, 如果弹出的元素不是相邻的数, 则返回false, 弹出后减少该元素在哈希表中对应的元素个数, 直到为0判断其前面是否还有其他更小的数, 如果有可直接返回false(这种情况下一定无法在group中再构成连续相邻的数了). 在函数开头可以先判断下数组的长度能否被groupSize整除, 不能直接返回false, 后面就可以不用再判断是否能填满最后一个group了. 开头的这个判断通过一些简单的方法过滤掉了不满足条件的解, 避免后续浪费时间对这些解进行判断, 做题时要先考虑一些这样可以通过简单判断排除不可行解的情况. 可以大大加快整体的运行效率.

 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
type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *MinHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
}

func (h *MinHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

func isNStraightHand(hand []int, groupSize int) bool {
    if len(hand) % groupSize != 0 {
        return false
    }
    h := MinHeap{}
    heap.Init(&h)
    hm := make(map[int]int)
    for _, elem := range hand {
        hm[elem]++
    }
    for key, _ := range hm {
        heap.Push(&h,key)
    }
    for h.Len() > 0 {
        startGroup := h[0]
        for i := startGroup; i < startGroup + groupSize; i++ {
            if _, ok := hm[i]; !ok {
                return false
            }
            hm[i]--
            if hm[i] == 0 {
                if i != h[0] {
                    return false
                }
                heap.Pop(&h)
            }
        }
    }
    return true

}

day102 2024-06-07

648. Replace Words

In English, we have a concept called root, which can be followed by some other word to form another longer word - let’s call this word derivative. For example, when the root “help” is followed by the word “ful”, we can form a derivative “helpful”.

Given a dictionary consisting of many roots and a sentence consisting of words separated by spaces, replace all the derivatives in the sentence with the root forming it. If a derivative can be replaced by more than one root, replace it with the root that has the shortest length.

Return the sentence after the replacement.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0607mDCIwsOF5xfV.png

题解

读懂题是第一步, 本题要将句子中所有的单词替换为在词典中对应的它的前缀形式(如果字典中存在这个单词的前缀的话), 最直接的思路就是遍历句子中的所有单词, 将每个单词依次和词典中的所有单词比较, 看词典中是否存在它的前缀, 但这种方法无疑效率极低, 这个场景和我们日常查词典有些相似, 想想我们日常在词典中查一个单词是如何快速定位的, 当然是按照字母顺序在词典中先定位第一个字母的范围, 再定位第二个字母的范围…,最后找到需要的单词. 现在只要把这种方法表示出来就可以大大加快寻找前缀的速度. 这样就可以构造一棵词典树, 树的边表示不同的字母, 节点可以用来标定是否是一个可行单词. 再去查找某个单词的前缀的时候, 只需要去树中执行bfs找到最短的可行前缀即可.

代码

 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
type TreeNodes struct{
    node [26]*TreeNodes
    over bool
}

func construct(dic []string) *TreeNodes{
    root := TreeNodes{}
    var point *TreeNodes
    for _, str := range dic{
        point = &root
        for i,_ := range str{
            if point.node[str[i]-'a']==nil{
                newnode := &TreeNodes{}
                point.node[str[i]-'a'] = newnode
                point = newnode
            }else{
                point = point.node[str[i]-'a']
            }
        }
        point.over = true
    }
    return &root
}

func findWord(root *TreeNodes, word string)(bool, string){
    var point *TreeNodes
    point = root
    prefix := []byte{}
    for i,_ := range word{
        if point.node[word[i]-'a'] == nil{
            return false,""
        }else{
            prefix = append(prefix, word[i])
            point = point.node[word[i]-'a']
            if point.over{
                return true, string(prefix)
            }
        }
    }
    return false,""
}

func replaceWords(dictionary []string, sentence string) string {
    root := construct(dictionary)
    sentenceArray := strings.Split(sentence, " ")
    exist, str := false, ""
    for i, word := range sentenceArray{
        exist, str = findWord(root, word)
        if exist{
            sentenceArray[i] = str
        }else{
            sentenceArray[i] = word
        }
    }
    return strings.Join(sentenceArray, " ")
}

总结

注意直接使用"+“在go中进行字符串连接是非常耗时的, 因此可以将原句子分割后得到字符串数组, 查询是否存在单词的可行前缀并直接替换对应数组中的字符串, 最后再调用strings.Join将数组中的所有字符串使用空格连接, 可以自行测试如果对数组中每个字符串在查找其可行前缀后都使用”+“连接到结果字符串上耗时远远大于这种方案.

day103 2024-06-08

523. Continuous Subarray Sum

Given an integer array nums and an integer k, return true if nums has a good subarray or false otherwise.

A good subarray is a subarray where:

its length is at least two, and the sum of the elements of the subarray is a multiple of k. Note that:

A subarray is a contiguous part of the array. An integer x is a multiple of k if there exists an integer n such that x = n * k. 0 is always a multiple of k.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0608mnBipVgAEri7.png

题解

本题要求连续子数组和为k的倍数, 则实际有影响的为各个位置上的元素对k取模后的余数, 这种求连续子数组和满足某种条件的问题, 常常可以使用前缀和求解. 在这个问题中使用前缀和保存从数组开头到当前位置元素的所有数字对k取模后求和再对k取模的值, 并将对应的值和当前的下标保存到长度为k的数组中, 数组下标表示前缀和(因为最终要对k取模,因此取值范围一定在k以内), 对应的数组位置的值为这个前缀和对应nums数组中的下标. 这种用数组保存的方式比用map来保存要快得多.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func checkSubarraySum(nums []int, k int) bool {
    prefixsum := map[int]int{}
    prefixsum[0] = -1
    sum := 0
    exist := false
    val := 0
    for i,value := range nums{
        sum = (sum + value) % k
        val, exist = prefixsum[sum]
        if !exist{
            prefixsum[sum] = i
        }else if i - val > 1{
                return true
        }
    }
    return false
}

总结

没有注意到k的取值范围, 因为k的取值范围非常大, 使用数组来保存前缀和对应的下标需要开始k大小的数组, 在实践中会导致超出leetcode的内存限制. 因此这里还是要用map来保存前缀和和对应的下标, 但思路仍然是相同的.

day104 2024-06-09

974. Subarray Sums Divisible by K

Given an integer array nums and an integer k, return the number of non-empty subarrays that have a sum divisible by k.

A subarray is a contiguous part of an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0609hbNSLTxR2sb5.png

题解

本题和昨天的题目类似, 通过前缀和相同来得到能被k整除的连续子数组, 注意本题中k的范围为 $10^4$, 因此可以用数组来保存前缀和对k取模后得到某个余数对应的下标位置个数. 后续遇到相同余数在结果中加上之前相同余数的位置个数, 并更新该余数对应的位置个数.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func subarraysDivByK(nums []int, k int) int {
    remain := make([]int, k)
    remain[0] = 1
    sum  := 0
    result := 0
    for _, value := range nums{
        sum = ((sum + value)%k + k) % k
        result += remain[sum]
        remain[sum]++
    }
    return result
}

总结

注意对负数取模的处理, 在这里为了将相同余数的位置个数保存到数组中, 要将负数取模后的数转换为正数(如对5取模得-4转换为1, 在求和过程中这二者的效果相同), 先取模再加一个k再取模(如果是正数通过再取模将加上的k消掉)即可

day105 2024-06-10

1051. Height Checker

A school is trying to take an annual photo of all the students. The students are asked to stand in a single file line in non-decreasing order by height. Let this ordering be represented by the integer array expected where expected[i] is the expected height of the ith student in line.

You are given an integer array heights representing the current order that the students are standing in. Each heights[i] is the height of the ith student in line (0-indexed).

Return the number of indices where heights[i] != expected[i].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0610E8iaynEKzsYW.png

题解

本题思路上很简单, 只需将数组复制一份并排序, 再与原数组比较即可. 这里主要意图应该是让你练习一下排序算法, 因此我们手动实现一下归并排序这种比较经典的排序算法. 归并算法是经典的分治法思想, 分治法分为"分"和"治"两部分, 分即将数组一分为二, 治即对于每个子数组进行排序, 最后将排好序的两个子数组合并(归并的"并”). 对于某一类分治法, 其时间复杂度可以使用主定理进行计算(感兴趣可自行了解).

https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms)

代码

 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
func heightChecker(heights []int) int {
    length := len(heights)
    sorted := mergeSort(heights, length)
    result := 0
    for i,value := range sorted{
        if value != heights[i]{
            result++
        }
    }
    return result
}

func mergeSort(input []int, length int) []int{
    if length == 1{
        return input
    }else{
        mid := length / 2
        left := mergeSort(input[0:mid],mid)
        right := mergeSort(input[mid:length],length-mid)
        result := []int{}
        j := 0
        for _,value := range right{
            for j<mid && left[j]<value{
                result = append(result, left[j])
                j++
            }
            result = append(result, value)
        }
        for j<mid{
                result = append(result, left[j])
                j++
        }
        return result
    }
}

day 106 2024-06-11

1122. Relative Sort Array

Given two arrays arr1 and arr2, the elements of arr2 are distinct, and all elements in arr2 are also in arr1.

Sort the elements of arr1 such that the relative ordering of items in arr1 are the same as in arr2. Elements that do not appear in arr2 should be placed at the end of arr1 in ascending order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0611UKAWksUXQOZm.png

题解

考虑数组中数字的大小范围在1000以内, 因此直接对数组排序可以使用计数排序算法. 本题中要求将数组1中的数字按照数组2中数字的顺序排序, 这部分也可以使用计数排序的思想, 即统计数组1中各个数的出现次数, 并保存在一个计数数组中, 这个数组中下标i处的值为数字i对应的数组1中出现的次数. 这种用下标指示值, 用元素指示个数的思路在前面的题中也多次出现. 得到计数数组后, 先将数组中包含在arr2的数按照arr2中的顺序放置, 并将对应位置次数归零, 再从头遍历数组, 按升序将其余数字放在末尾即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func relativeSortArray(arr1 []int, arr2 []int) []int {
    count := make([]int, 1001)
    for _,value := range arr1{
        count[value]++
    }
    result := []int{}
    for _,value := range arr2{
        for count[value]>0{
            result = append(result, value)
            count[value]--
        }
    }
    for index,value := range count{
        for value > 0{
            result = append(result, index)
            value--
        }
    }
    return result
}

day107 2024-06-12

75. Sort Colors

Given an array nums with n objects colored red, white, or blue, sort them in-place so that objects of the same color are adjacent, with the colors in the order red, white, and blue.

We will use the integers 0, 1, and 2 to represent the color red, white, and blue, respectively.

You must solve this problem without using the library’s sort function.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0612gdOHBokPpjjH.png

题解

本题只有0,1,2三个数, 对只包含这三个数的数组进行排序, 直接在原数组上修改, 充分考虑题目条件, 没有必要使用排序算法来排序, 当题目中只有两个数的时候, 对整个数组排序相当于通过两变量交换将小的数都交换到前面, 大的都交换到后面. 现在有三个数, 则通过只需通过交换位置将所有的2都放到数组末尾, 将所有的0放到数组开头, 1自然就位于中间位置. 交换的思路也很简单, 通过两个变量分别标记被移到数组开头的0的个数和移到数组末尾的2的个数, 遇到0就将其移到之前的0的后面并将开头0的个数加一, 对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
func sortColors(nums []int)  {
    zeros := 0
    twos := len(nums) - 1
    Loop:
    for i, value := range nums{
        if value == 0{
            nums[i],nums[zeros] = nums[zeros],nums[i]
            zeros++
        }else if value == 2{
            for twos >= 0 && nums[i] == 2{
                if i >= twos{
                    break Loop
                }
                fmt.Println()
                nums[i],nums[twos] = nums[twos],nums[i]
                twos--
            }
            if nums[i] == 0{
            nums[i],nums[zeros] = nums[zeros],nums[i]
            zeros++
            }
        }
    }
    return
}

总结

思路是正确的, 但在实现时有一些细节要注意, 如遇到2将其交换到末尾时, 如果被交换过来的数也是2则要继续将其移到末尾, 直到交换过来的数不是2或者已经将当前下标后面的所有数都交换为2为止.

day108 2024-06-13

2037. Minimum Number of Moves to Seat Everyone

There are n seats and n students in a room. You are given an array seats of length n, where seats[i] is the position of the ith seat. You are also given the array students of length n, where students[j] is the position of the jth student.

You may perform the following move any number of times:

Increase or decrease the position of the ith student by 1 (i.e., moving the ith student from position x to x + 1 or x - 1) Return the minimum number of moves required to move each student to a seat such that no two students are in the same seat.

Note that there may be multiple seats or students in the same position at the beginning.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0613UdQQlEcf5U82.png

题解

本题总体思路比较直观, 因为找最小的移动次数让每个同学都坐在某个位置的椅子上, 因此只要找到同学位置和椅子位置最小的差值和就可以得到答案. 首先将椅子位置和同学位置均排序, 再从头遍历数组将二者差加和即得最终结果, 是一种贪心算法, 这里分析一下为什么排序后将椅子和同学的位置依次做差就能得到正确答案. 考虑数组中的任意两个位置, 假设椅子对应的位置为i和i+k, 同学对应的位置为j和j+m, 若则无论j,j+m和i的大小关系如何, 这两个同学挪到两个椅子的最小值均为|j-i|+|j+m-i-k|(可以自行考虑i,i+k,j,j+m这四个数的全部不同位置关系, 一一列举即可得出结论). 由两个位置的情况可推得一般情况, 即两个数组长度相同且均从小到大排序的情况下, 两个数组各个元素的差值的最小和就是各个位置元素差的和(还可以思考无论哪个同学坐到哪个位置上, 总要有另一个同学坐在另一个位置上从而将这两个同学的距离补上, 因此按照顺序一一就坐就是最优的).

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func minMovesToSeat(seats []int, students []int) int {
    sort.Ints(seats)
    sort.Ints(students)
    result := 0
    for i,value := range seats{
        if value > students[i]{
            result += value - students[i]
        }else{
            result += students[i] - value
        }
    }
    return result
}

day109 2024-06-14

945. Minimum Increment to Make Array Unique

You are given an integer array nums. In one move, you can pick an index i where 0 <= i < nums.length and increment nums[i] by 1.

Return the minimum number of moves to make every value in nums unique.

The test cases are generated so that the answer fits in a 32-bit integer.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0614xHuev6LLva9M.png

题解

本题思路上和昨天的题目基本一致, 先排序再从头遍历排好序的数组, 将重复的数字增加到最小不重复为止(如2,2,2,3, 将第二个2增加到3,将第三个2增加到4, 将3增加到5). 使用一个变量记录当前已经遍历过的数字修改后不重复数字中的最大值. 对遍历的每个数字, 判断其与当前最大值的关系, 若相等则将最大值加一并将结果加一, 小于最大值将最大值加一并将结果加上当前数字与最大值的差值, 大于最大值将最大值更新为当前数字. 考虑排序在这里的作用, 排序给每一个数字都提供了隐含的信息, 即前面的数字都比当前数字小, 后面的数字都比当前数字大, 这一信息使得我们可以做出更好的决策. 在未排序前, 对数组中的数我们无法知道是应该保持原样还是增加到某个数字可以满足题目条件, 但在排序后, 只需要增加到最小不重复的数即可, 考虑两个数字2,4, 假如当前不重复需要数字4,5(注意题目中只允许增加, 不允许减小). 无论是将2增加到4,4增加到5. 还是4不变,2增加到5, 增加的差值均为3. 这是因为任何比4大的值, 2都需要先增加到4, 再增加到这个值. 这一过程无论是在同一个数字上完成还是拆分为在两个数字上完成, 过程中需要改变的差值都是一样的.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func minIncrementForUnique(nums []int) int {
    sort.Ints(nums)
    current := -1
    result := 0
    for _,value := range nums{
        if value == current{
            value++
            current++
            result++
        }else if value < current{
            current++
            result += current - value
        }else {
            current = value
        }
    }
    return result
}

day110 2024-06-15

502. IPO

Suppose LeetCode will start its IPO soon. In order to sell a good price of its shares to Venture Capital, LeetCode would like to work on some projects to increase its capital before the IPO. Since it has limited resources, it can only finish at most k distinct projects before the IPO. Help LeetCode design the best way to maximize its total capital after finishing at most k distinct projects.

You are given n projects where the ith project has a pure profit profits[i] and a minimum capital of capital[i] is needed to start it.

Initially, you have w capital. When you finish a project, you will obtain its pure profit and the profit will be added to your total capital.

Pick a list of at most k distinct projects from given projects to maximize your final capital, and return the final maximized capital.

The answer is guaranteed to fit in a 32-bit signed integer.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0615dPP8QZHqCRwX.png

题解

本题是一道难题, 思路总体还是比较清晰的, 本题目标是取k个不同的project, 使得最后能得到的利润最大. 每一个project都有对应的利润和成本. 要使最后的整体利润最大, 无疑只要在当前可以选择(即成本在当前已获得的资金范围内)的所有project中选择利润最大的, 再更新可选择的project, 再次选择利润最大的. 如此反复直到选择完全部的k个project即可. 有了这个思路, 我们要解决两个问题. 1. 如何高效的选择利润最大的project, 最大堆显然是一个不错的选择. 2. 如何更新可选择的project的范围, 我们将project的成本排序, 每次获得了更多的本金后就将小于当前本金的所有project的利润加入最大堆, 为了每次只放入新增加的project, 可以用一个标记变量标记之前已经选择过的project位置. 获得更多本金后从标记位置开始继续遍历数组, 并将成本小于本金的项的利润放入最大堆中. 代码实现以上解题思路即可.

代码

 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
type ProHeap []int

func (h ProHeap) Len()int {
        return len(h)
    }

func (h ProHeap) Less(i,j int)bool {
        return h[i] > h[j]
    }

func (h *ProHeap) Swap(i, j int) {
	(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}

func (h *ProHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
}

func (h *ProHeap) Pop() interface{}{
	res := (*h)[len(*h)-1]
	*h = (*h)[:len(*h)-1]
	return res
}

type Project struct {
	capital int
	profit  int
}

func findMaximizedCapital(k int, w int, profits []int, capital []int) int {
    n := len(profits)
	projects := make([]Project, n)

	for i := 0; i < n; i++ {
		projects[i] = Project{capital: capital[i], profit: profits[i]}
	}

    sort.Slice(projects, func(i, j int) bool {
		return projects[i].capital < projects[j].capital
	})



    proheap := make(ProHeap, 0)
    heap.Init(&proheap)

    mark := 0
    for k>0{
        for _,value := range projects[mark:n]{
            if value.capital > w{
                break
            }else{
                heap.Push(&proheap, projects[mark].profit)
                mark++
            }
        }
        if proheap.Len() == 0{
            return w
        }
        w += heap.Pop(&proheap).(int)
        k--
    }
    return w
}

总结

go中最大堆可以使用容器类heap来实现, 只要实现了heap接口的所有方法, 就可以直接使用这个堆了.

day111 2024-06-16

330. Patching Array

Given a sorted integer array nums and an integer n, add/patch elements to the array such that any number in the range [1, n] inclusive can be formed by the sum of some elements in the array.

Return the minimum number of patches required.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0616NgRLjhl1dyd6.png

题解

本题是一道难题, 难点在于根据题目题意, 如何确定某个数组中数的和能够覆盖的数字范围. 按照我们一贯使用的思考方式, 先考虑一下简单情况, 若只有一个数字1, 则能覆盖的数字只有1, 添加上一个数字2, 则能覆盖的数字有1,2,1+2. 如果在1,2基础上再添加一个数字3, 则能覆盖的数字有1,2,1+2,3,1+3,2+3,1+2+3. 则由这几个简单例子可以发现, 如果之前能够覆盖的数字为集合{x}, 则加上一个数字r后能覆盖的数字为集合 $x \cup (x + r)$ . 若之前能够覆盖的为一个连续的区间[x,x+k],则加上数字r后能够覆盖的区间为 $ [x,x+k] \cup [x+r, x+k+r]$. 则此时想到可以用贪心算法, 如果已经覆盖了[x, x+k], 则只需要让x+r = x+k+1, 即新增加的数恰好可以使得新覆盖区间的开头为已经覆盖区间的末尾. 就能得到[x,x+k+r]的新覆盖区间. 如果r比这种情况小, 那么新覆盖的区间没有我们当前得到的区间大. 如果比这种情况大, 则x+r和x+k之间会有几个数字没有覆盖上, 还需要再用一个新的数覆盖一次, 显然很难使得添加数字的个数最小.

将这个思路在代码中实现, 初始化一个数字表示当前覆盖的区间最大值sum, 遍历数组, 设x为nums[i], 判断当前sum是否大于nums[i](即已经覆盖的区间是否包含当前这个数组中的数字). 如果小于, 则添加一个数字sum+1 前面分析中的x+k+1 (因为题目只要求添加的数字的最小个数,因此不需要真的添加到数组中, 只需扩大可行区间的范围即可), 将区间最大值扩展为sum+sum+1 前面分析中的x+k+r , 并将添加数字的个数加一. 再次判断与x的关系. 如此反复直到覆盖的区间包含x为止, 继续向下遍历. 遍历完成后, 判断当前区间是否已经可以覆盖n, 如果不能, 则按照之前的方法继续扩展区间直到能覆盖n为止.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func minPatches(nums []int, n int) int {
    midsum := 0
    result := 0
    for _,value := range nums{
        for midsum < value-1 && midsum < n{
            result++
            midsum += midsum + 1
        }
        midsum += value
    }
    for midsum < n{
        result++
        midsum += midsum + 1
    }
    return result
}

day112 2024-06-17

633. Sum of Square Numbers

Given a non-negative integer c, decide whether there’re two integers a and b such that a2 + b2 = c.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0617sYGtK80QtKIS.png

题解

本题计算出c的平方根, 用两个变量low和high分别标记0和c的平方根(取下整), 计算两变量的平方和, 若比c大则减小high, 否则增大low, 直到得到平方和为c或者low=high为止.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func judgeSquareSum(c int) bool {
    high := int(math.Sqrt(float64(c)))
    low := 0
    for low <= high{
        if low*low + high*high == c{
            return true
        }else if low*low + high*high > c{
            high--
        }else{
            low++
        }
    }
    return false
}

总结

本题还有一些有趣的相关数学知识, 费马两平方和定理和两数平方和定理, 前者说明了一个奇素数何时能被写为两平方数之和. 满足条件的奇素数也被称为毕达哥拉斯质数, 后者给出了所有大于1的整数在什么情况下能被写为两平方数之和. 是前者的推广. 可参考

Fermat’s theorem on sums of two squares

Sum of two squares theorem

day113 2024-06-18

826. Most Profit Assigning Work

You have n jobs and m workers. You are given three arrays: difficulty, profit, and worker where:

difficulty[i] and profit[i] are the difficulty and the profit of the ith job, and worker[j] is the ability of jth worker (i.e., the jth worker can only complete a job with difficulty at most worker[j]). Every worker can be assigned at most one job, but one job can be completed multiple times.

For example, if three workers attempt the same job that pays $1, then the total profit will be $3. If a worker cannot complete any job, their profit is $0. Return the maximum profit we can achieve after assigning the workers to the jobs.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0618rpZxsUIGJ5f6.png

题解

本题仍然使用贪心算法, 首先给worker排序, 在排好序后遍历worker数组, 并把当前worker可以做的工作中profit最大的分配给他, 即给结果加上可以做的工作中的最大profit, 这个最大profit可以使用一个变量来保存, 当遍历的下一个worker时, 根据他的工作能力更新可以做的工作中的最大值. 如此反复, 最终得到结果.

代码

 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
func maxProfitAssignment(difficulty []int, profit []int, worker []int) int {
    sort.Ints(worker)
    length := len(profit)
    type w_p struct{
        diff int
        profit int
    }
    d_p := make([]w_p, length)
    for i,value := range profit{
        d_p[i].diff = difficulty[i]
        d_p[i].profit = value
    }
    sort.Slice(d_p, func(i, j int) bool {return d_p[i].diff < d_p[j].diff})

    max_pro := 0
    index := 0
    result := 0
    for _, value := range worker{
        for index < length && d_p[index].diff <= value{
            max_pro = max(max_pro, d_p[index].profit)
            index++
        }
        result += max_pro
    }
    return result
}

总结

为什么排序如此重要, 正如之前的题解中多次提到的, 一个排好序的数组在遍历的时候天然包含了更多信息, 即前面的数字比当前的小, 后面的比当前的大. 使用贪心的重要思想就是, 在当前能获得的结果中取最好的, 那么对于排好序的数组, 前面的必然比当前小, 我只需要依次遍历找到我能拿到的最大值即可. 如果没有排序, 那么我们每次都要将数组整体遍历一遍才能确定我当前能拿到的最大值是多少(后面可能有难度更小但利润更大的任务, 显然我们都想做这种任务). 每次都遍历一遍数组整体是 $n^2$ 的复杂度, 排好序后可以用下标标记之前遍历到哪里了, 只需要遍历一遍数组即可. 是 $nlogn(排序)+n(遍历数组) = O(nlogn) $ 的复杂度. 只有在排好序的数组里标记之前遍历到哪里才有意义, 未排序的数组标记之前遍历到哪里没什么意义(后面既可能有更大的, 也可能有更小的)

day114 2024-06-19

1482. Minimum Number of Days to Make m Bouquets

You are given an integer array bloomDay, an integer m and an integer k.

You want to make m bouquets. To make a bouquet, you need to use k adjacent flowers from the garden.

The garden consists of n flowers, the ith flower will bloom in the bloomDay[i] and then can be used in exactly one bouquet.

Return the minimum number of days you need to wait to be able to make m bouquets from the garden. If it is impossible to make m bouquets return -1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0619sTVISIXdeV4b.png

题解

不能做出足够的花束的条件是很容易确定的, 只要花园里花的总数比需要的数量小, 则直接返回-1. 否则只要等待足够长的时间, 总能做出足够的花束. 如果等待时间为花盛开时间的最大值, 显然一定满足条件, 若为最小值, 不一定满足条件, 即实际上本题转化为在所有盛开时间中搜索能满足条件的最小值. 对于给定一个时间, 只需要遍历一遍bloomDay数组即可得知其是否满足条件. 这种搜索其实与在一个数组中搜索指定的数没有本质上的区别, 将数组排序后使用二分查找, 对于搜索某一固定数字, 我们采取的方式是如果中间值比目标大, 则将右边界设为中间值, 否则将左边界设为中间值. 如此反复. 而在本题中, 虽然不能直接比较目标值与中间值的相对大小, 但通过使用中间值对bloomDay数组遍历判断是否能做成足够数量的花束(设一个标记变量保存当前连续可使用的花的数量,大于等于k时将当前花束数量加1,最后判断花束数量是否大于等于m)我们可以了解到目标值与中间值的相对大小. 如果此中间值满足题目条件, 那么目标值应该小于等于这个中间值, 否则大于等于这个中间值, 因此回到了最熟悉的二分搜索部分.

代码

 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
func minDays(bloomDay []int, m int, k int) int {
    if len(bloomDay) < m*k{
        return -1
    }

    left := 1
    right := 0
    for _,value := range bloomDay{
        if value > right{
            right = value
        }
    }
    mid := 0
    begin := 0
    bouquet := 0
    for left < right{
        mid = (left + right) / 2
        begin = 0
        bouquet = 0
        for _,value := range bloomDay{
            if value <= mid{
                begin++
                if begin >= k{
                    begin = 0
                    bouquet++
                    if bouquet >= m{
                        break
                    }
                }
            }else{
                begin = 0
            }
        }
        if bouquet >= m{
            right = mid
        }else{
            left = mid+1
        }
    }
    return left
}

day115 2024-06-20

1552. Magnetic Force Between Two Balls

In the universe Earth C-137, Rick discovered a special form of magnetic force between two balls if they are put in his new invented basket. Rick has n empty baskets, the ith basket is at position[i], Morty has m balls and needs to distribute the balls into the baskets such that the minimum magnetic force between any two balls is maximum.

Rick stated that magnetic force between two different balls at positions x and y is |x - y|.

Given the integer array position and the integer m. Return the required force.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0620ljAJmy7N6uA7.png

题解

竟然是一道和<瑞克和莫蒂>的联动题(瑞克和莫蒂真的很好看, 强推!!), 要求我们分配这些球, 使得两个球的最小间隔最大. 典型的max-min问题. 解答这种问题因为题目中是要求分配球来获得最终的结果, 使得我们的思路往往起初就会停留在如何分配球这个问题上, 试图通过比较各种分配方式的最小间隔来找到最大的值, 但显然是低效的, 原因在于可能有很多种分配组合都对应着同一个最小间隔. 因此应该转换思路, 给定一个最小间隔, 判断是否有分配方式可以符合这个最小间隔, 只要有一个符合的分配方式, 那么这个最小间隔就是可以取得的, 相当于用这个分配方式代表了所有相同最小间隔的分配方式(代表元思想). 如何判断? 只要在排序后的数组从头到尾遍历, 并采用贪心算法, 和昨天的花束问题类似, 每当间隔大于等于给定的最小间隔就分配一个球, 最后看能分配的球的数量和给定的球的数量之间的相对大小. 大于给定的球则给定最小间隔可以取得. 如何得到最大值? 一样通过二分法, 通过上述判断方法判定中间数值与目标最大值的相对大小, 中间值能取得则将左边界设为中间值, 否则右边界设为中间值. 最终即可得到所求的最大值.

代码

 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
func maxDistance(position []int, m int) int {
    sort.Ints(position)
    right := position[len(position)-1]-position[0]
    left := 1

    for left <= right{
        mid := (left + right) / 2
        last := position[0]
        ballnum := 1
        dis := 0
        for _,value := range position[1:]{
            dis += value - last
            last = value
            if dis >= mid{
                ballnum++
                if ballnum >= m{
                    break
                }
                dis = 0
            }
        }
        if ballnum >= m{
            left = mid + 1
        }else{
            right = mid - 1
        }
    }
    return right
}

总结

本题和昨天的花束问题有几分相似之处, 体现了一种很重要的思想, 当通过某种组合寻找一由组合产生的属性值, 而多种组合对应同一个属性值时, 可以先假设一个属性值, 再去验证某个组合是否符合. 这样就把问题从求解问题转换为了验证问题. 这里其实和我们平常常用的证明思路正好相反, 平常很多问题证伪只需要举一个反例, 证实则要严格证明,而在今天的问题中, 我们只要找到一个能证实假设的距离成立的组合就足够了, 即证实只要举一个正例即可. 这种题要结合题目的要求, 很多实际生活问题只要能找到一个可行解即可. 这是我们能够用这种方法解题的基础.

很多问题在原问题不容易解决的时候都可以通过一些方式转换成等价的更容易解决的问题, 如计数问题中的自归约问题可将近似求解算法转换成某种采样算法, 通过采样即可得到原问题的近似解(蒙特卡罗)

day116 2024-06-21

1052. Grumpy Bookstore Owner

There is a bookstore owner that has a store open for n minutes. Every minute, some number of customers enter the store. You are given an integer array customers of length n where customers[i] is the number of the customer that enters the store at the start of the ith minute and all those customers leave after the end of that minute.

On some minutes, the bookstore owner is grumpy. You are given a binary array grumpy where grumpy[i] is 1 if the bookstore owner is grumpy during the ith minute, and is 0 otherwise.

When the bookstore owner is grumpy, the customers of that minute are not satisfied, otherwise, they are satisfied.

The bookstore owner knows a secret technique to keep themselves not grumpy for minutes consecutive minutes, but can only use it once.

Return the maximum number of customers that can be satisfied throughout the day.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0621cuTPeiqgImKT.png

题解

本题中minutes是固定的, 相当于固定长度的minutes可以放置在grumpy数组的任意位置, 将这些位置的数全部变为0, 显然我们需要遍历所有可以放置的位置从而找出能满足的顾客最大值, 因此可以采用滑动窗口从头开始遍历grumpy窗口, 并随着窗口滑动计算当前能满足的顾客数量. 考虑到窗口之外的能满足顾客数量是固定的, 且每次滑动实际上只需要增加窗口后一个的顾客数量且减去窗口最前面的顾客数量, 即可得到每次滑动后能满足的顾客总数. 则初始化先计算出窗口外能满足的顾客总数, 再单独计算窗口内满足的顾客数量. 再将二者加和即得当前能满足的顾客总数. 持续遍历, 按照之前的算法更新能满足的顾客总数并更新最大值.

代码

 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
func maxSatisfied(customers []int, grumpy []int, minutes int) int {
    outwindow := 0
    for i,value := range grumpy[minutes:]{
        if value == 0{
            outwindow += customers[i+minutes]
        }
    }
    innerwindow := 0
    for _,value := range customers[:minutes]{
        innerwindow += value
    }


    maxcus := innerwindow + outwindow
    temp := maxcus
    for i,value := range grumpy[:len(grumpy)-minutes]{
        if value == 1{
            temp -= customers[i]
        }
        if grumpy[i+minutes] == 1{
            temp += customers[i+minutes]
        }

        maxcus = max(maxcus, temp)
    }
    return maxcus
}

day117 2024-06-22

1248. Count Number of Nice Subarrays

Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it.

Return the number of nice sub-arrays.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0622ujf0vWP4qK3X.png

题解

这种连续子数组问题是老朋友了, 尤其是这种问符合某个条件的连续子数组个数, 这种题目都采用类似前缀和的思想. 计算从数组开始到当前下标的子数组中奇数的个数, 以奇数个数作为下标, 将从头开始包含这个奇数个数的子数组数目作为数组中的值. 只需要将j下标和j-k下标的数字相乘即可得到从j-k到j包含k个奇数的子数组个数. 从下标k开始遍历"前缀和"数组. 并按照前述方法计算包含k个奇数的子数组个数并加和即可得到最终结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func numberOfSubarrays(nums []int, k int) int {
    state := []int{1}
    length := 0
    for _,value := range nums{
        if value % 2 == 1{
            length++
            state = append(state, 1)
        }else{
            state[length]++
        }
    }

    if length < k{
        return 0
    }


    result := 0
    for i, value := range state[k:]{
        result += value * state[i]
    }
    return result
}

总结

其实这种类型的问题核心也是一种转化问题的思路, 即对于求解某一性质为一定值的问题, 可以转化为求解两个区间再做差. 如求解某属性等于k的问题, 可以转化为求解某属性小于等于k和小于等于k-1两个区间后做差的问题. 这种转化适用求解一个区间比求解精准值要容易的多的场景(求解小于等于某个值 往往比求解等于某个值容易得多).

day118 2024-06-23

1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit

Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0623LJjsHEwXWx9i.png

题解

本题虽是一道中等难度的题, 但实际比较复杂. 题目很好理解, 需要找到满足子数组内任意两数字差的绝对值小于等于k的所有子数组中最长子数组的长度. 将问题分解开, 首先解决如何确定某个子数组中任意两数字差绝对值是否小于等于k, 只需要知道这个子数组中的最大值和最小值, 二者的差小于等于k则子数组中任意两数字差绝对值都小于等于k. 那么解决这个问题可以使用滑动窗口, 记录当前窗口中的最大值和最小值, 如果满足小于等于k的条件, 则扩大窗口, 判断新加入的数字是否在最小值和最大值的范围内, 在则继续扩大窗口. 否则判断最值更新后当前窗口还能否继续满足小于等于k的条件, 如果不满足则缩小窗口直到满足为止. 满足则继续扩大窗口. 对于某一个特定的子数组, 获知其最小值和最大值比较容易, 遍历一遍数组即可得到. 但使用滑动窗口解题时, 如果每次扩大或者缩小窗口都对窗口内的数字进行遍历显然时间复杂度极高. 我们只需要根据扩大窗口时新增加的数字和缩小窗口时减少的数字来更新最值. 需要解决的问题是, 缩小窗口直到满足条件这一步骤中, 如何判断缩小窗口已经满足条件了呢. 以最小值被更新为例, 如果最小值更新和与当前最大值的差大于k, 则应该缩小窗口直到窗口内的最大值和最小值的差小于等于k. 这意味着, 我们缩小窗口时可能不仅要将窗口缩小到不包含当前最大值, 还可能继续缩小, 直到满足条件为止. 如当前最小值为2, k为3, 而当前窗口中最大值为8, 则我们应缩小窗口直到窗口内最大值小于等于5为止, 这期间我们可能要缩小到不包含8, 再缩小到不包含7, 再缩小到不包含6… 因此我们需要知道数字中大于5的数字都有什么. 这可以用单调队列来解决, 单调减队列保证了后面的数字一定比前面的数字小, 不满足条件的数字都被舍弃. 用一个哈希表保存当前窗口中所有数字和其对应的个数, 缩小窗口时, 减少哈希表中窗口左端数字对应的个数, 直到遇到当前单调队列的队首数字, 同样减少哈希表中的个数, 并判断减少后是否为0, 为0则将其从单调队列中弹出, 否则继续缩小窗口. 直到单调队列队首数字为满足条件的数字. 对于最大值被更新则做类似处理.

因此解答本题我们需要: 1. 哈希表 : 保存当前窗口中各个数字的个数 2. 单调减队列: 保存当前窗口中从最大值开始以及最大值右侧比最大值小且满足单调减顺序的数字(如对于7,5,6 队列中为7,6). 3. 单调增队列 : 保存窗口中从最小值开始及最小值右侧比最小值大且满足单调增顺序的数字(如对于5,7,6 队列中为5,6). 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
func longestSubarray(nums []int, limit int) int {
	maxDeque := []int{} // 单调减队列,用来维护最大值
	minDeque := []int{} // 单调增队列,用来维护最小值
	left := 0
	result := 0
    number := map[int]int{}
    maxlen := 0
    minlen := 0

	for right := 0; right < len(nums); right++ {
        number[nums[right]]++
		// 维护 maxDeque 为单调减
		for maxlen != 0 && maxDeque[maxlen-1] <= nums[right] {
			maxDeque = maxDeque[0:maxlen-1]
            maxlen--
		}
		maxDeque = append(maxDeque, nums[right])
        maxlen++

		// 维护 minDeque 为单调增
		for minlen != 0 && minDeque[minlen-1] >= nums[right] {
			minDeque = minDeque[0:minlen-1]
            minlen--
		}
		minDeque = append(minDeque, nums[right])
        minlen++

		// 检查当前窗口中的最大值和最小值之差是否超过 limit
		if maxDeque[0] - minDeque[0] > limit {
			if nums[right] == minDeque[0]{
                for maxDeque[0] -  minDeque[0] > limit{
                    number[nums[left]]--
                    left++
                    if number[maxDeque[0]] == 0{
                        // 最大值出队列
                        maxDeque = maxDeque[1:]
                        maxlen--
                    }
                }
            }else if nums[right] == maxDeque[0]{
                for maxDeque[0] -  minDeque[0] > limit{
                    number[nums[left]]--
                    left++
                    if number[minDeque[0]] == 0{
                        // 最小值出队列
                        minDeque = minDeque[1:]
                        minlen--
                    }
                }
            }
		}

		// 更新结果
		if right-left+1 > result {
			result = right - left + 1
		}
	}

	return result
}

总结

最大值单调队列中的在最大值右侧这一含义为什么如此重要, 因为通过单调队列隐含了一个信息, 即位于当前窗口中单调队列中队列首的数字左侧的数字都比队首数字小, 这一信息使我们可以丢弃一些不必要保存的数字, 因为如果需要缩小窗口, 那么在缩小到这个最大值之前, 窗口中都包含这个最大值, 那么一定不满足要求, 只有在缩小到不包括这个最大值, 才可能满足要求. 这时候再根据单调队列中的数字继续依次判断. 这就是"单调"带来的隐含信息, 这一信息对于简化计算极为重要.

day119 2024-06-24

995. Minimum Number of K Consecutive Bit Flips

You are given a binary array nums and an integer k.

A k-bit flip is choosing a subarray of length k from nums and simultaneously changing every 0 in the subarray to 1, and every 1 in the subarray to 0.

Return the minimum number of k-bit flips required so that there is no 0 in the array. If it is not possible, return -1.

A subarray is a contiguous part of an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0624zB6pKwwpsD6O.png

题解

本题为一道难题, 要解决最小的翻转次数之前先思考如何判断一个数组能否通过几次翻转最终使得数组内数字全为1, 再考虑如何求得翻转次数的最小值.

如何解决能否通过翻转使得数组内数字全为1的问题呢. 看上去数组中的0可能在任何一个位置出现, 我们也不知道101翻转后的010末尾的0能否和后面的数字组合到一起成为新的一串0. 想让计算机解题也不可能像我们看这个数组一样扫视过去大概估计哪些部分可以翻转, 再通过尝试修正一小部分错误就能得出判断. 计算机不会"估计". 但计算机会"硬算". 因此我们不必如此贪心想一步到位得出答案, 大可以从头开始, 每次只将一个0翻转为1, 遍历到下一个0, 翻转为1, 如此重复每次只将数组中最前面的0翻转为1. 最终如果整个数组都能变成1则该数组可以翻转为全1. 否则不能. 而翻转要相邻的k个位同时翻转, 可用一个固定窗口来表示相邻的k位. 遍历数组, 让窗口内窗口首部的数字为0, 翻转窗口, 将窗口移动到下一个0, 如此重复最终即可判断能否使数组内为全1.

巧合的是, 这种算法同时解决了最小的问题, 即通过这种方式移动窗口并翻转到最后翻转的次数就是最小次数. 原因是最终目标是将数组全部置为1, 因此想实现这个目标, 任何一个0都需要通过翻转变成1, 那么无论哪个位置的0迟早都要被翻转, 意味着任意位置的0对最终结果数量的贡献是均等的, 即时先翻转了后面的0, 前面遗留的0最后还是需要一次翻转, 因此直接从前到后依次翻转就能得到正确答案.

但到这里还没有结束, 这种算法每次都要将整个窗口内的全部数字翻转一遍, 最坏情况下, 窗口每次移动一位, 则总共需要翻转n*k次. 在k接近n时, 时间复杂度相当于 $n^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
28
29
30
31
32
33
34
func minKBitFlips(nums []int, k int) int {
    window := 0
    numlen := len(nums)
    result := 0
    fliptail := []int{}
    for window < numlen{
        fliplen := len(fliptail)
        if fliplen != 0{
            if window <= fliptail[0]{
                if nums[window] ^ (fliplen % 2) == 0{
                    fliptail = append(fliptail, window+k-1)
                    result++
                }
            }else{
                fliptail = fliptail[1:]
                fliplen--
                if nums[window] ^ (fliplen % 2) == 0{
                    fliptail = append(fliptail, window+k-1)
                    result++
                }
            }
        }else{
            if nums[window]  == 0{
                    fliptail = append(fliptail, window+k-1)
                    result++
                }
        }
        window++
    }
    if len(fliptail) == 0 || (len(fliptail) == 1 && fliptail[0] == numlen-1){
        return result
    }
    return -1
}

day120 2024-06-25

1038. Binary Search Tree to Greater Sum Tree

Given the root of a Binary Search Tree (BST), convert it to a Greater Tree such that every key of the original BST is changed to the original key plus the sum of all keys greater than the original key in BST.

As a reminder, a binary search tree is a tree that satisfies these constraints:

The left subtree of a node contains only nodes with keys less than the node’s key. The right subtree of a node contains only nodes with keys greater than the node’s key. Both the left and right subtrees must also be binary search trees.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0625JppqmJoxVzA1.png

题解

考虑到树为一棵二叉搜索树, 因此对于任何一个节点, 比它大的节点有其右子树的节点, 若其父节点为祖父节点的左子节点, 则还有其祖父节点以及祖父节点的右子树的节点. 以此类推. 考虑最简单的仅有三个节点的情况: 一个父节点和左右两个子节点. 对于左子节点而言, 需要加父节点和右子节点的值. 对于父节点要加右子节点的值. 右子节点不用处理. 因此通过对树遍历将问题转化为类似三个节点的情况. 左右子树均可视为一个节点. 从根节点开始, 对节点进行如下操作, 递归遍历右子树, 计算右子树所有节点的和返回. 将当前节点的值加上右子树的和, 将和传递给左子树并递归遍历左子树, 返回左子树所有节点的和. 最终返回根节点和左右两个子树的和. 注意更新节点值和计算子树和是分开的, 更新节点值要将传递的值和当前值以及右子树的和相加. 但返回时返回的是原来的节点值对应的子树和.

代码

 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.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func bstToGst(root *TreeNode) *TreeNode {
    rightFirst(root, 0)
    return root
}

func rightFirst(root *TreeNode, pass int)int{
    temp := root.Val
    if root.Left == nil && root.Right == nil{
        root.Val += pass
        return temp
    }
    rightSum := 0
    if root.Right != nil{
        rightSum = rightFirst(root.Right, pass)
    }
    leftSum := 0
    if root.Left != nil{
        leftSum = rightFirst(root.Left, rightSum+temp+pass)
    }
    root.Val = root.Val + pass + rightSum
    return temp + rightSum + leftSum
}

day121 2024-06-26

1382. Balance a Binary Search Tree

Given the root of a binary search tree, return a balanced binary search tree with the same node values. If there is more than one answer, return any of them.

A binary search tree is balanced if the depth of the two subtrees of every node never differs by more than 1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0626MDuY3BC9YtnG.png

题解

本题要求将二叉搜索树转换为二叉平衡搜索树, 这就是在二叉平衡树新插入节点时常用的平衡操作. 但在本题中不需要使用常规的二叉平衡树中常用的左旋, 右旋等操作, 原因是二叉平衡树中常用的平衡操作是在插入一个新节点时用于调整树的平衡性的, 所以最多只需两次旋转即可调整平衡. 但在本题中, 树的不平衡性可能非常大, 且我们只需要在原来的树的基础上获得一棵新树. 只需考虑构造出一棵平衡树即可, 不需要考虑多次调整的情况.

因此遍历当前整棵二叉搜索树, 将树中的值从小到大保存到数组中, 二分法构造一棵新的平衡树就能得到最终结果.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */

func balanceBST(root *TreeNode) *TreeNode {
    nums := []*TreeNode{}
    nums = traverse(root)
    length := len(nums)
    root = build(nums, length)
    return root
}

func traverse(root *TreeNode)[]*TreeNode{
    if root.Left == nil && root.Right == nil{
        return []*TreeNode{root}
    }
    left := []*TreeNode{}
    if root.Left != nil{
        left = traverse(root.Left)
    }
    right := []*TreeNode{}
    if root.Right != nil{
        right = traverse(root.Right)
    }
    left = append(left, root)
    left = append(left, right...)
    return left
}

func build(nums []*TreeNode, leng int) *TreeNode{
    if leng == 0{
        return nil
    }else if leng == 1{
        nums[0].Left = nil
        nums[0].Right = nil
        return nums[0]
    }else{
        leftRoot := build(nums[0:leng/2], leng/2)
        rightRoot := build(nums[leng/2+1:], leng-(leng/2+1))
        root := nums[leng/2]
        root.Left = leftRoot
        root.Right = rightRoot
        return root
    }
    return nil
}

day122 2024-06-27

1791. Find Center of Star Graph

There is an undirected star graph consisting of n nodes labeled from 1 to n. A star graph is a graph where there is one center node and exactly n - 1 edges that connect the center node with every other node.

You are given a 2D integer array edges where each edges[i] = [ui, vi] indicates that there is an edge between the nodes ui and vi. Return the center of the given star graph.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0627D3O8tjqrKhmI.png

题解

本题n个节点共有n-1条边, 存在一个中心节点使得其余节点都与这个节点相连. 只需要看edges中的任意两条边, 找到这两条边共有的节点即为中心节点.

代码

1
2
3
4
5
6
7
func findCenter(edges [][]int) int {
    if edges[1][0] == edges[0][0] || edges[1][0] == edges[0][1]{
        return edges[1][0]
    }else{
        return edges[1][1]
    }
}

总结

考虑只有一个中心节点与其余n-1个节点相连, 但其余节点也可能相连的情况, 统计所有节点的度, 度最高的即为中心节点.

27. Remove Element

Given an integer array nums and an integer val, remove all occurrences of val in nums in-place. The order of the elements may be changed. Then return the number of elements in nums which are not equal to val.

Consider the number of elements in nums which are not equal to val be k, to get accepted, you need to do the following things:

Change the array nums such that the first k elements of nums contain the elements which are not equal to val. The remaining elements of nums are not important as well as the size of nums. Return k.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/06277nzgocBBZMpz.png

题解

本题最简单的解法当然是再创建一个相同大小的数组, 将原数组中非val的数依次复制到数组中. 另外一种方法则是双指针, 遍历数组, 头指针遇到val则与尾指针指向的非val值交换, 直到头尾指针相遇即可.

代码

 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
func removeElement(nums []int, val int) int {
    length := len(nums)
    if length == 0{
        return 0
    }

    if length == 1{
        if nums[0] == val{
            return 0
        }else{
            return 1
        }
    }

    k := 0
    head := 0
    tail := len(nums) - 1
    for head <= tail{
        if nums[head] == val{
            for nums[tail] == val && head < tail{
                tail--
            }
            if nums[tail] != val{
                nums[head],nums[tail] = nums[tail],nums[head]
                k++
            }
        }else{
            k++
        }
        head++
    }
    return k
}

day123 2024-06-28

2285. Maximum Total Importance of Roads

You are given an integer n denoting the number of cities in a country. The cities are numbered from 0 to n - 1.

You are also given a 2D integer array roads where roads[i] = [ai, bi] denotes that there exists a bidirectional road connecting cities ai and bi.

You need to assign each city with an integer value from 1 to n, where each value can only be used once. The importance of a road is then defined as the sum of the values of the two cities it connects.

Return the maximum total importance of all roads possible after assigning the values optimally.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0628laAsmqEvIGOQ.png

题解

本题要给每个节点赋值, 使得最终边的权重和(边连接的两个节点的权重和)最大. 统计出每个节点相连的边的数量, 从大到小排序, 按序依次从最大到最小赋值并计算权重和. 这里边的权重和可以转化为节点能贡献的权重和的和, 即用节点的权重乘以和这个节点相连的边的数量. 再依次对各个节点求值并加和.

题解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func maximumImportance(n int, roads [][]int) int64 {
    vers := make([]int, n)
    for _,value := range roads{
        vers[value[0]]++
        vers[value[1]]++
    }
    sort.Ints(vers)
    result := 0
    for i:=n-1;i>=0;i--{
        result += vers[i]*(i+1)
    }
    return int64(result)
}

day124 2024-06-29

2192. All Ancestors of a Node in a Directed Acyclic Graph

You are given a positive integer n representing the number of nodes of a Directed Acyclic Graph (DAG). The nodes are numbered from 0 to n - 1 (inclusive).

You are also given a 2D integer array edges, where edges[i] = [fromi, toi] denotes that there is a unidirectional edge from fromi to toi in the graph.

Return a list answer, where answer[i] is the list of ancestors of the ith node, sorted in ascending order.

A node u is an ancestor of another node v if u can reach v via a set of edges.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0629uNWG4lSJqpOB.png

题解

本题要求出每个节点的所有祖先节点, 按照节点标号从小到大列出. 既然是有向无环图, 那么本图可以进行拓扑排序. 统计所有节点的入度, 从入度为0的节点开始, 依次遍历入度为0的所有节点, 在其全部后继节点对应的祖先数组中中加入遍历的节点标号. 删掉与这些节点相连的边, 再次查看入度为0的的节点, 重复以上操作. 直到数组中只有入度为0的节点即剩余节点都没有后继节点为止.

但这种方式要重复向多个数组中加入相同的值. 如果在统计入度的时候将所有边反向并统计反向边对应的各个节点的入度, 则从入度为0的节点开始按入度从小到大遍历节点并将其所有后继节点(通过bfs进行遍历)放入其在结果数组中对应位置的数组内并排序即可.

代码

 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
func getAncestors(n int, edges [][]int) [][]int {
    innodes := make(map[int]int)
    outnodes := make([][]int, n)
    maxin := 0
    for i, edge := range edges {
        outnodes[edge[1]] = append(outnodes[edge[1]], edge[0])
        innodes[edge[0]]++
        maxin = max(maxin, innodes[edge[0]])
        edges[i][0], edges[i][1] = edges[i][1], edges[i][0]
    }
    result := make([][]int, n)

    // 处理入度为0的节点
    for i := 0; i < n; i++ {
        if _, exist := innodes[i]; !exist {
            result[i] = bfs(i, outnodes)
        }
    }

    for inDegree := 1; inDegree <= maxin; inDegree++ {
        for node, degree := range innodes {
            if degree == inDegree {
                result[node] = bfs(node, outnodes)
                delete(innodes, node)
            }
        }
    }

    return result
}

func bfs(start int, outnodes [][]int) []int {
    queue := []int{start}
    visited := make(map[int]bool)
    ancestors := make([]int, 0)

    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]

        for _, neighbor := range outnodes[node] {
            if !visited[neighbor] {
                visited[neighbor] = true
                queue = append(queue, neighbor)
                ancestors = append(ancestors, neighbor)
            }
        }
    }

    sort.Ints(ancestors)
    return ancestors
}

day125 2024-06-30

1579. Remove Max Number of Edges to Keep Graph Fully Traversable

Alice and Bob have an undirected graph of n nodes and three types of edges:

Type 1: Can be traversed by Alice only. Type 2: Can be traversed by Bob only. Type 3: Can be traversed by both Alice and Bob. Given an array edges where edges[i] = [typei, ui, vi] represents a bidirectional edge of type typei between nodes ui and vi, find the maximum number of edges you can remove so that after removing the edges, the graph can still be fully traversed by both Alice and Bob. The graph is fully traversed by Alice and Bob if starting from any node, they can reach all other nodes.

Return the maximum number of edges you can remove, or return -1 if Alice and Bob cannot fully traverse the graph.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/06308ceBjTn41NK5.png

题解

本题是一道难题, type3的路径是Alice和Bob都可以使用的, 如果想让每个人都能遍历整个图并且使图中的边最少, 无疑首先要尽量使用type3的路径. 通过type3的路径能连通的点Alice和Bob不需要再使用type1和type2连通, 在type3的连通图中的type1和type2的边都可以删去. 不能删除的边是连通type3边构成的连通图之间连通的type1和type2的边. 使用并查集将type3的边构成的连通图中同一个连通图的节点用一个代表节点表示出来. 遍历type1的边, 将那些能将type3构成的不同连通图连通起来的边保留, 其余的均删除并计数, 若最后所有节点均在同一个集合中, 则Alice能遍历所有节点. 同理对type2的边也执行以上操作, 最终将删除的边数目加和即可得到最终结果.

代码

 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
86
func maxNumEdgesToRemove(n int, edges [][]int) int {
    // 初始化两个并查集,一个用于Alice,一个用于Bob
    ufAlice := newUnionFind(n)
    ufBob := newUnionFind(n)

    removedEdges := 0

    // 首先处理类型3的边(Alice和Bob都可以使用的边)
    for _, edge := range edges {
        if edge[0] == 3 {
            if !ufAlice.union(edge[1]-1, edge[2]-1) {
                removedEdges++
            } else {
                ufBob.union(edge[1]-1, edge[2]-1)
            }
        }
    }

    // 处理类型1的边(只有Alice可以使用的边)
    for _, edge := range edges {
        if edge[0] == 1 {
            if !ufAlice.union(edge[1]-1, edge[2]-1) {
                removedEdges++
            }
        }
    }

    // 处理类型2的边(只有Bob可以使用的边)
    for _, edge := range edges {
        if edge[0] == 2 {
            if !ufBob.union(edge[1]-1, edge[2]-1) {
                removedEdges++
            }
        }
    }

    // 检查是否所有节点都连通
    if ufAlice.count != 1 || ufBob.count != 1 {
        return -1
    }

    return removedEdges
}

// 并查集结构
type UnionFind struct {
    parent []int
    rank   []int
    count  int
}

func newUnionFind(n int) *UnionFind {
    uf := &UnionFind{
        parent: make([]int, n),
        rank:   make([]int, n),
        count:  n,
    }
    for i := range uf.parent {
        uf.parent[i] = i
    }
    return uf
}

func (uf *UnionFind) find(x int) int {
    if uf.parent[x] != x {
        uf.parent[x] = uf.find(uf.parent[x])
    }
    return uf.parent[x]
}

func (uf *UnionFind) union(x, y int) bool {
    rootX, rootY := uf.find(x), uf.find(y)
    if rootX == rootY {
        return false
    }
    if uf.rank[rootX] < uf.rank[rootY] {
        uf.parent[rootX] = rootY
    } else if uf.rank[rootX] > uf.rank[rootY] {
        uf.parent[rootY] = rootX
    } else {
        uf.parent[rootY] = rootX
        uf.rank[rootX]++
    }
    uf.count--
    return true
}

总结

要熟悉并查集的实现, 能快速根据需要实现一个简单的并查集出来.

day126 2024-07-01

1550. Three Consecutive Odds

Given an integer array arr, return true if there are three consecutive odd numbers in the array. Otherwise, return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0701xCkXpUEp7aQv.png

题解

本题是一道简单题, 设定一个标记位标记当前连续的奇数个数, 如果达到三个则返回true. 如果遍历到末尾还没有达到三个则返回false.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func threeConsecutiveOdds(arr []int) bool {
    num := 0
    for _,value := range arr{
        if value % 2 == 1{
            num++
            if num >= 3{
                return true
            }
        }else{
            num = 0
        }
    }
    return false
}

day127 2024-07-02

350. Intersection of Two Arrays II

Given two integer arrays nums1 and nums2, return an array of their intersection. Each element in the result must appear as many times as it shows in both arrays and you may return the result in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/070274XGKa0XOzSu.png

题解

本题是一道简单题, 要获取在nums1和nums2中都出现的数组元素和其个数. 则可以用map记录nums1和nums2中出现的数字和出现次数. 遍历其中一个map, 在另一个map中查找当前map中的key出现的次数, 取比较小的次数n将对应key重复n次放入结果数组中.

代码

 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
func intersect(nums1 []int, nums2 []int) []int {
    map1 := map[int]int{}
    map2 := map[int]int{}
    len1 := len(nums1)
    len2 := len(nums2)
    for _,value := range nums1{
        map1[value]++
    }
    for _,value := range nums2{
        map2[value]++
    }

    shorter := map[int]int{}
    longer := map[int]int{}

    if len1 <= len2{
        shorter = map1
        longer = map2
    }else{
        shorter = map2
        longer = map1
    }
    result := []int{}
    for key, value := range shorter{
        num, exist := longer[key]
        if exist{
            if value <= num{
                for i:=0;i<value;i++{
                    result = append(result, key)
                }
            }else{
                for i:=0;i<num;i++{
                    result = append(result, key)
                }
            }
        }
    }
    return result
}

day128 2024-07-03

1509. Minimum Difference Between Largest and Smallest Value in Three Moves

You are given an integer array nums.

In one move, you can choose one element of nums and change it to any value.

Return the minimum difference between the largest and smallest value of nums after performing at most three moves.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0703KBCausAlS1vQ.png

题解

本题可以改变三个数字的值和可以删掉三个数字效果上差不多, 问题变成了应该删掉哪三个数字, 显然为了让删掉后剩余数组中数字的极差最小, 应该从数组的最小值或者最大值中删除, 问题在于应该删掉的三个数字中哪些从最大值方向删除, 哪些从最小值方向删除. 考虑最终的目标为让极差最小, 从最小值方向删除和从最大值方向删除三个数字一共只有四种组合, 小3大0, 小1大2, 小2大1, 小0大3. 直接计算四种组合对应的减少极差的值, 比较大小选择能减少极差最大的组合并删除对应元素, 最后计算删除后数组的极差.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func minDifference(nums []int) int {
    length := len(nums)
    if length <= 4{
        return 0
    }
    sort.Ints(nums)
    maxchoice := [][]int{{3,0},{2,1},{1,2},{0,3}}
    finalchoice := []int{}
    maxcover := -1
    for index, choice := range maxchoice{
        temp := nums[choice[0]]-nums[0]+nums[length-1]-nums[length-1-choice[1]]
        if temp > maxcover{
            maxcover = temp
            finalchoice = maxchoice[index]
        }
    }
    nums = nums[finalchoice[0]:length-finalchoice[1]]
    return nums[length-4]-nums[0]
}

总结

这种方法属于暴力枚举的方式, 但在题目限制只能改变3个数的条件下, 需要枚举的情况极少, 因此可以奏效.

day129 2024-07-04

2181. Merge Nodes in Between Zeros

You are given the head of a linked list, which contains a series of integers separated by 0’s. The beginning and end of the linked list will have Node.val == 0.

For every two consecutive 0’s, merge all the nodes lying in between them into a single node whose value is the sum of all the merged nodes. The modified list should not contain any 0’s.

Return the head of the modified linked list.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0704FKp6L7iDJHNv.png

题解

本题要将0分隔的链表中两个0之间的数字求和合并为一个节点, 并删掉0节点, 则可以遍历链表, 遇到0记录起始0的节点指针, 对后面的非0数字求和, 再次遇到0则用和替换起始0节点的值, 并将这个节点的Next指针指向新遇到的0, 将新遇到的0设为起始0, 如此反复. 注意当出现连续0时, 直接忽视, 按照前述方法正常求和即可.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeNodes(head *ListNode) *ListNode {
    var startzero *ListNode
    tempsum := 0
    var current *ListNode
    for current=head;current.Next!=nil;current=current.Next{
        if current.Val == 0{
            if startzero == nil{
                startzero = current
                continue
            }else if tempsum != 0{
                startzero.Val = tempsum
                startzero.Next = current
                startzero = current
                tempsum = 0
            }
        }else{
            tempsum += current.Val
        }
    }
    if tempsum != 0{
        startzero.Val = tempsum
        startzero.Next = nil
    }
    return head
}

day130 2024-07-05

2058. Find the Minimum and Maximum Number of Nodes Between Critical Points

A critical point in a linked list is defined as either a local maxima or a local minima.

A node is a local maxima if the current node has a value strictly greater than the previous node and the next node.

A node is a local minima if the current node has a value strictly smaller than the previous node and the next node.

Note that a node can only be a local maxima/minima if there exists both a previous node and a next node.

Given a linked list head, return an array of length 2 containing [minDistance, maxDistance] where minDistance is the minimum distance between any two distinct critical points and maxDistance is the maximum distance between any two distinct critical points. If there are fewer than two critical points, return [-1, -1].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0705GyHjjK7eutFL.png

题解

本题使用双指针法, 一个指针指向前一个节点, 一个指针指向当前节点, 这样就可以判断当前节点是否是关键点(critical point). 将节点在链表中的位置也使用下标表示出来, 这样方便判断两个关键点之间的距离, 使用一个变量保存初始关键点的下标, 一个保存前一个关键点的下标, 每次遇到新的关键点计算与前一个关键点的距离并更新距离最小值. 遍历整个链表后, 用最后一个关键点下标和第一个关键点下标作差即得距离最大值.

代码

 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 singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func nodesBetweenCriticalPoints(head *ListNode) []int {
    var before *ListNode
    var current *ListNode
    before = head
    current = head.Next
    beforeindex := -1
    currentindex := 1
    startindex := -1
    mindis := 1000001
    for current.Next != nil{
        if (current.Val > before.Val && current.Val > current.Next.Val) || (current.Val < before.Val && current.Val < current.Next.Val){
            if beforeindex == -1{
                beforeindex = currentindex
                startindex = currentindex
            }else{
                mindis = min(mindis, currentindex-beforeindex)
                beforeindex = currentindex
            }
        }
        currentindex++
        before = before.Next
        current = current.Next
    }
    if mindis == 1000001{
        return []int{-1,-1}
    }
    return []int{mindis, beforeindex-startindex}
}

day131 2024-07-06

2582. Pass the Pillow

There are n people standing in a line labeled from 1 to n. The first person in the line is holding a pillow initially. Every second, the person holding the pillow passes it to the next person standing in the line. Once the pillow reaches the end of the line, the direction changes, and people continue passing the pillow in the opposite direction.

For example, once the pillow reaches the nth person they pass it to the n - 1th person, then to the n - 2th person and so on. Given the two positive integers n and time, return the index of the person holding the pillow after time seconds.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0706R8U9WzanmoQP.png

题解

本题是简单题, 使用time对n-1(4个人只需传承3次即可到最后一人)求模和求商, 商的奇偶可以决定传递的起点是队首还是队尾, 求模得到的余数可以决定从本次传递起点开始传递了几次. 再使用对应的传递起点计算当前传递到哪个人.

代码

1
2
3
4
5
6
7
8
9
func passThePillow(n int, time int) int {
    label := time % (n-1)
    dir := time / (n-1)
    if dir % 2 == 1{
        return n - label
    }else{
        return label + 1
    }
}

2. Add Two Numbers

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0706OGiRpS80mHq2.png

题解

本题在两个链表中较长的链表上直接加上另一个链表的中对应节点的值并用标志位标记进位. 当一个链表遍历结束时将另一个链表的剩余节点直接连接到链表后面, 注意处理进位.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
    flag := 0
    current1 := l1
    current2 := l2
    head := current1
    for current1.Next != nil && current2.Next != nil{
        current1.Val = current1.Val + current2.Val + flag
        if current1.Val >= 10{
            current1.Val -= 10
            flag = 1
        }else{
            flag = 0
        }
        current1 = current1.Next
        current2 = current2.Next
    }
    if current1.Next == nil && current2.Next == nil{
        current1.Val = current1.Val + current2.Val + flag
        if current1.Val >= 10{
            current1.Val -= 10
            current1.Next = &ListNode{1, nil}
        }
        return head
    }else if current1.Next == nil{
        current1.Val = current1.Val + current2.Val + flag
        if current1.Val >= 10{
            current1.Val -= 10
            flag = 1
        }else{
            flag = 0
        }
        current1.Next = current2.Next
    }else{
        current1.Val = current1.Val + current2.Val + flag
        if current1.Val >= 10{
            current1.Val -= 10
            flag = 1
        }else{
            flag = 0
        }
    }
    current1 = current1.Next
    for current1.Next != nil && flag == 1{
        current1.Val += flag
        if current1.Val >= 10{
            current1.Val -= 10
            flag = 1
        }else{
            flag = 0
        }
        current1 = current1.Next
    }
    if flag == 1{
        current1.Val += flag
        if current1.Val >= 10{
            current1.Val -= 10
            current1.Next = &ListNode{1,nil}
        }
    }
    return head
}

day132 2024-07-07

1518. Water Bottles

There are numBottles water bottles that are initially full of water. You can exchange numExchange empty water bottles from the market with one full water bottle.

The operation of drinking a full water bottle turns it into an empty bottle.

Given the two integers numBottles and numExchange, return the maximum number of water bottles you can drink.

题解

本题是一道简单题, 和生活关联比较紧密, 只需通过循环不断判断当前手里的饮料喝完后的空瓶子数量能否换新的饮料, 将当前空瓶子个数(由余数和新换的饮料个数加和得到)除以numExchange, 商即为能换的新饮料数量, 保存余数用于下一次兑换饮料. 如此反复直至商为0. 每次将新换的饮料数量都加到最终结果中.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func numWaterBottles(numBottles int, numExchange int) int {
    result := numBottles
    current := numBottles
    new := numBottles / numExchange
    remain := numBottles % numExchange
    for new > 0{
        result += new
        current = new + remain
        new = current / numExchange
        remain = current % numExchange
    }
    return result
}

day133 2024-07-08

1823. Find the Winner of the Circular Game

There are n friends that are playing a game. The friends are sitting in a circle and are numbered from 1 to n in clockwise order. More formally, moving clockwise from the ith friend brings you to the (i+1)th friend for 1 <= i < n, and moving clockwise from the nth friend brings you to the 1st friend.

The rules of the game are as follows:

Start at the 1st friend. Count the next k friends in the clockwise direction including the friend you started at. The counting wraps around the circle and may count some friends more than once. The last friend you counted leaves the circle and loses the game. If there is still more than one friend in the circle, go back to step 2 starting from the friend immediately clockwise of the friend who just lost and repeat. Else, the last friend in the circle wins the game. Given the number of friends, n, and an integer k, return the winner of the game.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0708SPpMsBmYFF3W.png

题解

本题直接模拟这一游戏过程即可, 直观思路是使用一个循环队列模拟圆桌上的所有人, 模拟游戏过程, 每次数到的人就将其从循环队列中删除, 循环队列可以使用一个数组来构建, 每次超出队列长度从头开始遍历即可. 数组初始化为1-n的节点值. 每次根据数到的下标直接删除数组中的这个元素. 直到数组中只有一个元素为止.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func findTheWinner(n int, k int) int {
    length := n
    current := -1
    list := []int{}
    for i:=1;i<=n;i++{
        list = append(list, i)
    }
    for length > 1{
        current  = (current + k) % length
        list = append(list[0:current],list[current+1:]...)
        current--
        length--
    }
    return list[0]
}

总结

本题也可以采用数学方法解决, 理解数学方法的关键还是在于问题的转化, 考虑之前循环队列的解法, 当我们已知队列中有两个人并且计数为k时的获胜者编号, 则当队列中有三个人时, 计数为k, 通过计数将某个人删除后问题又变为两个人并且计数为k时的获胜者编号, 只需要将原来的编号加上三个人时删掉的人的编号即可得到最终的编号. 这种解决方法将直观的从整体的队列根据计数挨个从队列中删除对应计数编号的人直到只剩一个人从而得到获胜者编号反向转换为从只有一个人时找到最终的获胜者编号到n-1时的获胜者编号最终到n个人时的获胜者编号. 是经典的递归解决问题的思想. 而在想明白这个思路后, 将递归部分转换为迭代, 即可得到最终的简单解法.

具体数学证明可见:

找出游戏的获胜者

1
2
3
4
5
6
7
func findTheWinner(n, k int) int {
    winner := 1
    for i := 2; i <= n; i++ {
        winner = (k+winner-1)%i + 1
    }
    return winner
}

day134 2024-07-09

1701. Average Waiting Time

There is a restaurant with a single chef. You are given an array customers, where customers[i] = [arrivali, timei]:

arrivali is the arrival time of the ith customer. The arrival times are sorted in non-decreasing order. timei is the time needed to prepare the order of the ith customer. When a customer arrives, he gives the chef his order, and the chef starts preparing it once he is idle. The customer waits till the chef finishes preparing his order. The chef does not prepare food for more than one customer at a time. The chef prepares food for customers in the order they were given in the input.

Return the average waiting time of all customers. Solutions within 10-5 from the actual answer are considered accepted.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0709eXlrAsmWtCid.png

题解

本题思路比较清晰, 直接模拟顾客来和等待上菜的过程, 用一个变量保存当前顾客能吃上菜的时间, 继续遍历,判断下一个顾客的到达时间是否在前一个顾客能吃上菜时间之前, 如果在之前, 则该顾客的等待时间为上一个顾客能吃上菜的时间减去当前顾客的到达时间与当前顾客菜的等待时间之和. 否则该顾客只需等待自己的菜品制作时间即可. 此处是典型的动态规划, 只需要保存前一个顾客能吃上菜的时间(之前最后一个顾客能吃上菜的时间)就已经包含了之前所有顾客等菜和菜品制作的信息. 根据前一个顾客的时间判断当前顾客的状态并更新当前最后一个顾客吃上菜的时间.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func averageWaitingTime(customers [][]int) float64 {
    current := 0
    waiting := 0
    for _, customer := range customers{
        if customer[0] >= current{
            waiting += customer[1]
            current = customer[0]+customer[1]
        }else{
            waiting += current - customer[0] + customer[1]
            current = current + customer[1]
        }
    }
    return float64(waiting)/float64(len(customers))
}

总结

本题其实就是先来先服务场景的简单应用.

day135 2024-07-10

1598. Crawler Log Folder

The Leetcode file system keeps a log each time some user performs a change folder operation.

The operations are described below:

“../” : Move to the parent folder of the current folder. (If you are already in the main folder, remain in the same folder). “./” : Remain in the same folder. “x/” : Move to the child folder named x (This folder is guaranteed to always exist). You are given a list of strings logs where logs[i] is the operation performed by the user at the ith step.

The file system starts in the main folder, then the operations in logs are performed.

Return the minimum number of operations needed to go back to the main folder after the change folder operations.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0710BQdXlyk0sUKW.png

题解

本题为一道简单题, 只需用一个变量记录当前目录的深度, 遍历logs数组, 根据规定的操作修改深度数值即可, ../即为-1,x/即为+1, ./不变, 注意当深度已经为0时回到父目录的操作使得深度仍然为0.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func minOperations(logs []string) int {
    depth := 0
    for _, op := range logs{
        if op == "../"{
            if depth == 0{
                continue
            }
            depth--
        }else if op == "./"{
            continue
        }else{
            depth++
        }
    }
    return depth
}

3. Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0707bo6XVX54rJMj.png

题解

本题用经典的滑动窗口求解, 当遇到重复字符时, 缩小窗口直到字符不重复. 继续扩大窗口. 更新窗口最大长度, 直到遍历完整个字符串为止. 要注意的是本题是不能有重复字符, 包括空格和特殊字符等, 因此用map是最方便的.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func lengthOfLongestSubstring(s string) int {
    left := 0
    result := 0
    chars := make(map[byte]int)
    for i, _ := range s{
        if chars[s[i]] < 1{
            chars[s[i]] = chars[s[i]]+1
            result = max(result, i-left+1)
        }else{
            for left < i && chars[s[i]] >= 1{
                chars[s[left]]--
                left++
            }
            chars[s[i]] = 1
        }
    }
    return result
}

day136 2024-07-11

1190. Reverse Substrings Between Each Pair of Parentheses

You are given a string s that consists of lower case English letters and brackets.

Reverse the strings in each pair of matching parentheses, starting from the innermost one.

Your result should not contain any brackets.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0711Txx0hnCNHwjS.png

题解

本题要将每个括号内的字符串都翻转, 则字符串被翻转偶数次后仍为原来的字符串本身, 因此可以用一个栈表示当前已经读入的待匹配的左括号和字符. 当匹配到右括号时, 就翻转匹配的左右括号之间的字符构成新的字符串, 并将这个字符串放入栈中, 继续向下匹配, 当再次匹配到右括号时, 重复此操作直到到字符串末尾为止, 即使用栈模拟整个字符串对应的括号内的翻转过程.

代码

 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
func reverseParentheses(s string) string {
    var stack []string
    length := 0

	for _, char := range s {
		if char == '(' {
			stack = append(stack, "(")
            length++
		} else if char == ')' {
			var rev strings.Builder
			for length > 0 && stack[length-1] != "(" {
                fmt.Println(length)
				str := stack[length-1]
				stack = stack[:length-1]
                length--
				rev.WriteString(reverse(str))
			}
			stack = stack[:length-1]
			stack = append(stack, rev.String())
		} else {
			stack = append(stack, string(char))
            length++
		}
	}

	var result strings.Builder
	for _, str := range stack {
		result.WriteString(str)
	}

	return result.String()
}

func reverse(s string) string {
    runes := []rune(s)
    length := len(runes)

    for i := 0; i < length/2; i++ {
        runes[i], runes[length-1-i] = runes[length-1-i], runes[i]
    }

    return string(runes)
}

day137 2024-07-12

1717. Maximum Score From Removing Substrings

You are given a string s and two integers x and y. You can perform two types of operations any number of times.

Remove substring “ab” and gain x points. For example, when removing “ab” from “cabxbae” it becomes “cxbae”. Remove substring “ba” and gain y points. For example, when removing “ba” from “cabxbae” it becomes “cabxe”. Return the maximum points you can gain after applying the above operations on s.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0712VRksdxp5LeQU.png

题解

本题可以注意到由于x,y一个值比另一个值更大, 则ab,ba两个组合中只要优先选择组合成价值更大的组合就能得到最大值. 即通过贪心即可得到连续的ab串的最大值, 而优先组合可以通过栈来实现, 如果ab组合值更大, 遍历ab串并将读取的字符入栈, 当栈顶为a并且当前字符为b时将a出栈, 为a则继续入栈, 直到遍历到ab串末尾, 再将栈中剩余的a和b组合为ba. 这里可以不将只能和a组合成ba的b入栈, 而是用变量保存其个数, 最后直接通过个数计算结果即可. ba值更大同理.

代码

 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
func maximumGain(s string, x int, y int) int {
    cont := false
    start := 0
    result := 0
    for i,_ := range s{
        if (s[i] == 'a' || s[i] == 'b'){
            if !cont{
                cont = true
                start = i
            }
        }else{
            if cont{
                result += caculate(s[start:i],x,y)
                cont = false
            }
        }
    }
    if cont{
        result += caculate(s[start:],x,y)
    }
    return result
}

func caculate(s string,x int,y int)int{
    result := 0
    bignumber := 0
    smallnumber := 0
    var stacktarget byte
    if x > y{
        stacktarget = 'a'
        bignumber = x
        smallnumber = y
    }else{
        stacktarget = 'b'
        bignumber = y
        smallnumber = x
    }
    stack := 0
    front := 0
    for i,_ := range s{
        if s[i] == stacktarget{
             stack++
        }else{
            if stack > 0{
                stack--
                result += bignumber
            }else{
                front++
            }
        }
    }
    result += min(front, stack) * smallnumber
    return result
}

day138 2024-07-13

2751. Robot Collisions

There are n 1-indexed robots, each having a position on a line, health, and movement direction.

You are given 0-indexed integer arrays positions, healths, and a string directions (directions[i] is either ‘L’ for left or ‘R’ for right). All integers in positions are unique.

All robots start moving on the line simultaneously at the same speed in their given directions. If two robots ever share the same position while moving, they will collide.

If two robots collide, the robot with lower health is removed from the line, and the health of the other robot decreases by one. The surviving robot continues in the same direction it was going. If both robots have the same health, they are both removed from the line.

Your task is to determine the health of the robots that survive the collisions, in the same order that the robots were given, i.e. final heath of robot 1 (if survived), final health of robot 2 (if survived), and so on. If there are no survivors, return an empty array.

Return an array containing the health of the remaining robots (in the order they were given in the input), after no further collisions can occur.

Note: The positions may be unsorted.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0713WjsqGNYwxVip.png

题解

本题是一道难题, 但核心算法只要熟悉栈操作即可, 本题机器人的位置并不是排好序的, 将机器人的编号和其位置, 方向, 血量绑定在同一个结构体后, 根据位置对结构体排序. 建立一个新栈(使用数组实现), 遍历机器人, 若机器人是向右运行的直接入栈. 向左则判断栈是否为空, 如果不为空则根据栈顶机器人的不同状态做出不同的操作, 若栈顶机器人运行方向向右且血量高于当前机器人, 则将栈顶机器人血量-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
func survivedRobotsHealths(positions []int, healths []int, directions string) []int {
    type Robot struct{
        position int
        health int
        direction byte
        index int
    }

    robots := []Robot{}

    for i,value := range positions{
        newrobot := Robot{value, healths[i], directions[i],i+1}
        robots = append(robots, newrobot)
    }

    sort.Slice(robots, func(i, j int)bool{
        return robots[i].position < robots[j].position
    })

    stack := []Robot{}

    for _, robot := range robots{
        if robot.direction == 'R'{
            stack = append(stack, robot)
        }else{
                length := len(stack)
                for length > 0 && stack[length-1].direction == 'R'  && robot.health > stack[length-1].health{
                    robot.health--
                    stack = stack[:length-1]
                    length--
                }
                if length > 0{
                    if stack[length-1].direction == 'R' && robot.health == stack[length-1].health{
                        stack = stack[:length-1]
                    }else if stack[length-1].direction == 'R' {
                        stack[length-1].health--
                    }else{
                        stack = append(stack, robot)
                    }
                }else{
                    stack = append(stack, robot)
                }
        }
    }
    sort.Slice(stack, func(i,j int)bool{
        return stack[i].index < stack[j].index
    })

    result := []int{}
    for _,value := range stack{
        result = append(result, value.health)
    }
    return result
}

day139 2024-07-14

726. Number of Atoms

Given a string formula representing a chemical formula, return the count of each atom.

The atomic element always starts with an uppercase character, then zero or more lowercase letters, representing the name.

One or more digits representing that element’s count may follow if the count is greater than 1. If the count is 1, no digits will follow.

For example, “H2O” and “H2O2” are possible, but “H1O2” is impossible. Two formulas are concatenated together to produce another formula.

For example, “H2O2He3Mg4” is also a formula. A formula placed in parentheses, and a count (optionally added) is also a formula.

For example, “(H2O2)” and “(H2O2)3” are formulas. Return the count of all elements as a string in the following form: the first name (in sorted order), followed by its count (if that count is more than 1), followed by the second name (in sorted order), followed by its count (if that count is more than 1), and so on.

The test cases are generated so that all the values in the output fit in a 32-bit integer.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0714e2kIjx5sE9Oo.png

题解

本题是一道难题, 关键在于如何处理括号, 从括号匹配问题开始, 对于这种有括号的问题, 栈一直是解决问题的核心. 括号代表着优先级的变化, 而栈的先入后出特性其实包含了隐含的优先级, 即后入栈的先出(优先级更高), 这与括号代表的优先级恰好一致(内层括号的优先级更高, 要先算). 本题通过使用栈将全部括号展开, 遇到左括号将其入栈, 继续遍历读入原子和对应的个数, 直到遇到右括号从栈顶向下遍历并将原子个数乘以右括号后面的数字, 直到遇到左括号为止, 删掉左括号. 这不是一个标准的栈操作, 但思想上仍是栈的思想. 将全部括号展开后, 从头遍历无括号的原子, 将相同原子个数合并最后输出.

代码

 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
func countOfAtoms(formula string) string {
    type atom struct {
	name  string
	count int
}
	stack := []atom{}
	length := len(formula)
	i := 0

	for i < length {
		if formula[i] == '(' {
			stack = append(stack, atom{name: "(", count: 0})
			i++
		} else if formula[i] == ')' {
			i++
			start := i
			for i < length && unicode.IsDigit(rune(formula[i])) {
				i++
			}
			multiplier := 1
			if start < i {
				multiplier, _ = strconv.Atoi(formula[start:i])
			}
            j := 0
			for j = len(stack) - 1; j >= 0 && stack[j].name != "("; j-- {
				stack[j].count *= multiplier
			}
            stack = append(stack[0:j],stack[j+1:]...)
			// Remove the '(' from the stack

		} else {
			start := i
			i++
			for i < length && unicode.IsLower(rune(formula[i])) {
				i++
			}
			name := formula[start:i]
			start = i
			for i < length && unicode.IsDigit(rune(formula[i])) {
				i++
			}
			count := 1
			if start < i {
				count, _ = strconv.Atoi(formula[start:i])
			}
			stack = append(stack, atom{name: name, count: count})
		}
	}

	atomCount := map[string]int{}
	for _, a := range stack {
		atomCount[a.name] += a.count
	}

	keys := make([]string, 0, len(atomCount))
	for k := range atomCount {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var result strings.Builder
	for _, key := range keys {
		result.WriteString(key)
		if atomCount[key] > 1 {
			result.WriteString(strconv.Itoa(atomCount[key]))
		}
	}

	return result.String()
}

day140 2024-07-15

2196. Create Binary Tree From Descriptions

You are given a 2D integer array descriptions where descriptions[i] = [parenti, childi, isLefti] indicates that parenti is the parent of childi in a binary tree of unique values. Furthermore,

If isLefti == 1, then childi is the left child of parenti. If isLefti == 0, then childi is the right child of parenti. Construct the binary tree described by descriptions and return its root.

The test cases will be generated such that the binary tree is valid.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0715MS1tzkYMjKZ4.png

题解

本题先构建二叉树, 再找到二叉树的根. 构建二叉树并不困难, 可以将每个节点的指针都保存在map中, key为节点的值, value为节点的指针. 再根据描述构建每个节点. 找二叉树的根可以在构建二叉树的同时使用类似并查集的思想, 将当前描述的孩子节点的值作为下标, 父节点的值作为下标对应的值, 构造一个father数组. 若父节点作为下标之前没有指向任何值, 则将父节点的下标处的值指向父节点自身的值. 最终从任意一个节点值开始, 在father数组中查看其作为下标的值, 直到该下标处的值指向自身即为根节点的节点值. 返回根节点.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func createBinaryTree(descriptions [][]int) *TreeNode {
    trees := map[int]*TreeNode{}
    father := make([]int, 100001)
    for _,des := range descriptions{
        if father[des[0]] == 0{
            father[des[0]] = des[0]
        }
        father[des[1]] = des[0]
        var child *TreeNode
            if trees[des[1]] == nil{
                child = &TreeNode{des[1], nil, nil}
                trees[des[1]] = child
            }else{
                child = trees[des[1]]
            }
        if trees[des[0]] == nil{
            if des[2] == 1{
                trees[des[0]] = &TreeNode{des[0], child, nil}
            }else{
                trees[des[0]] = &TreeNode{des[0], nil, child}
            }

        }else{
            if des[2] == 1{
                trees[des[0]].Left = child
            }else{
                trees[des[0]].Right = child
            }
        }
    }
    i := descriptions[0][0]
    for father[i] != i{
        i = father[i]
    }
    return trees[i]
}

day141 2024-07-16

2096. Step-By-Step Directions From a Binary Tree Node to Another

You are given the root of a binary tree with n nodes. Each node is uniquely assigned a value from 1 to n. You are also given an integer startValue representing the value of the start node s, and a different integer destValue representing the value of the destination node t.

Find the shortest path starting from node s and ending at node t. Generate step-by-step directions of such path as a string consisting of only the uppercase letters ‘L’, ‘R’, and ‘U’. Each letter indicates a specific direction:

‘L’ means to go from a node to its left child node. ‘R’ means to go from a node to its right child node. ‘U’ means to go from a node to its parent node. Return the step-by-step directions of the shortest path from node s to node t.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0716cNylqTGph1XZ.png

题解

本题正常遍历二叉树, 根据遍历过程记录’L’,‘R’直到分别找到起点和终点为止. 这里可以使用先根遍历, 如果根即为目标节点, 则设置对应的路径变量, 因为这里有两个目标节点, 需使用dfs查找两次分别找到到两个目标点的路径, 遍历过程中将之前经过的路径作为参数传递给递归函数. 这里使用字符数组要比使用字符串快得多.

得到起点和终点的路径后, 同时从头遍历二者的路径, 直到找到路径中的第一个不同点, 此时获取到达起点剩余路径的长度, 构造相同长度的重复’U’字符串并与终点的剩余路径连接即得到从起点到终点的路径.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func getDirections(root *TreeNode, startValue int, destValue int) string {
    arrToS := dfs(root, []byte{}, startValue)
    arrToD := dfs(root, []byte{}, destValue)
    i := 0
    for len(arrToS) > i && len(arrToD) > i && arrToS[i] == arrToD[i] {
        i++
    }
	pathUp := strings.Repeat("U", len(arrToS)-i)
	pathDown := string(arrToD[i:])
    return pathUp + pathDown
}

func dfs(root *TreeNode, arr []byte, target int) []byte {
    if root.Val == target {
        return arr
    }
    if root.Left != nil {
        newArr := append(arr, 'L')
        if found := dfs(root.Left, newArr, target); found != nil {
            return found
        }
    }
    if root.Right != nil  {
        newArr := append(arr, 'R')
        if found := dfs(root.Right, newArr, target); found != nil {
            return found
        }
    }
    return nil
}

day142 2024-07-17

1110. Delete Nodes And Return Forest

Given the root of a binary tree, each node in the tree has a distinct value.

After deleting all nodes with a value in to_delete, we are left with a forest (a disjoint union of trees).

Return the roots of the trees in the remaining forest. You may return the result in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0717NX9V70B0DCGG.png

题解

本题关键在于了解什么样的节点会成为森林中树的根节点, 根据描述可以得出结论: 如果父节点是一个要被删除的节点且子节点不被删除, 那么这个子节点将成为森林中一棵树的根节点. 使用dfs遍历树并根据这一结论将根节点加入最终结果中, 注意处理当父节点不被删除但其子节点被删除时将父节点的这个子节点设置为空. 根据我们的结论, 每次只要对父节点和其左右子节点做出判断即可, 这样就转换为了仅包含父节点和左右子节点的小问题, 就可以使用递归来遍历整棵树得出最终结果.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func delNodes(root *TreeNode, to_delete []int) []*TreeNode {
    to_delete_map := map[int]bool{}
    for _,value := range to_delete{
        to_delete_map[value] = true
    }
    result := []*TreeNode{}
    if !to_delete_map[root.Val]{
        result = append(result, root)
    }
    var dfs func(*TreeNode)
    dfs = func(root *TreeNode){
        if to_delete_map[root.Val]{
            if root.Left != nil{
                if !to_delete_map[root.Left.Val]{
                    result = append(result, root.Left)
                }
                dfs(root.Left)
            }
            if root.Right != nil{
                if !to_delete_map[root.Right.Val]{
                    result = append(result, root.Right)
                }
                dfs(root.Right)
            }
        }else{
            if root.Left != nil{
                dfs(root.Left)
                if to_delete_map[root.Left.Val]{
                    root.Left = nil
                }
            }
            if root.Right != nil{
                dfs(root.Right)
                if to_delete_map[root.Right.Val]{
                    root.Right = nil
                }
            }
        }
    }
    dfs(root)
    return result
}

day143 2024-07-18

1530. Number of Good Leaf Nodes Pairs

You are given the root of a binary tree and an integer distance. A pair of two different leaf nodes of a binary tree is said to be good if the length of the shortest path between them is less than or equal to distance.

Return the number of good leaf node pairs in the tree.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0718DCKotaG6SnQm.png

题解

考虑对于一个节点来说, 只要知道了到其左子树的所有叶子节点的距离, 和到其右子树的所有叶子节点的距离, 就可以将左子树的所有叶子节点距离和右子树所有叶子节点距离依次加和并与distance比较. 则通过后序遍历, 先遍历左右子树最后将左右子树的叶子节点的距离(数组)返回, 按照上面的思路与distance做比较, 直到遍历完整棵树为止. 这里可以将到叶子节点的最短距离记录下来用于优化, 当左右子树到叶子节点的最小值和已经大于distance时, 其余节点没必要继续进行比较. 直接返回即可.

代码

 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
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func countPairs(root *TreeNode, distance int) int {
    result := 0
    var dfs func(*TreeNode)([]int, int)
    dfs = func(root *TreeNode)([]int, int){
        if root.Left == nil && root.Right == nil{
            return []int{0},0
        }
        leftTree := []int{}
        leftmin := 0
        rightTree := []int{}
        rightmin := 0
        if root.Left != nil{
            leftTree, leftmin = dfs(root.Left)
        }
        if root.Right != nil{
            rightTree, rightmin = dfs(root.Right)
        }
        minvalue := 2000
        returnTree := []int{}
        for _,value := range leftTree{
            returnTree = append(returnTree, value+1)
            minvalue = min(minvalue, value+1)
        }
        for _,value := range rightTree{
            returnTree = append(returnTree, value+1)
            minvalue = min(minvalue, value+1)
        }
        if leftmin+rightmin+2 > distance{
            return returnTree, minvalue
        }
        for _,value := range leftTree{
            for _, value2 := range rightTree{
                if value+value2+2 <= distance{
                    result++
                }
            }
        }
        return returnTree, minvalue
    }
    dfs(root)
    return result
}

总结

该种解法的时间复杂度与叶子节点的数量有关(约为节点总数乘以叶子节点数量的平方), 考虑节点总数最多为2的10次方, 则叶子节点最多为2的9次方即512个, 即便是平方的复杂度也在可以接受的范围内.

day144 2024-07-19

1380. Lucky Numbers in a Matrix

Given an m x n matrix of distinct numbers, return all lucky numbers in the matrix in any order.

A lucky number is an element of the matrix such that it is the minimum element in its row and maximum in its column.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0719ZtW5ztQktBOJ.png

题解

本题遍历两遍, 将每行最小的值的下标和每列最大的值的下标分别放入两个数组, 数组的下标代表对应的行数和列数, 遍历最小值数组, 根据对应的下标在最大值数组中找到对应的值看是否与最小值数组中的下标相同. 相同则表名找到了一个lucky number. 通过提前构建最大值数组, 可以仅遍历一遍数组即同时找到每行的最小值和每列的最大值, 只需在遍历行的时候将每个数与当前该列的最大值比较并更新即可通过一遍遍历完成任务.

代码

 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
func luckyNumbers (matrix [][]int) []int {
    rowlen := len(matrix)
    colen := len(matrix[0])
    minrow := make([]int, rowlen)
    maxcol := make([]int, colen)
    maxcolval := make([]int, colen)
    for i, row := range matrix{
        tempmin := 1000000
        for j, num := range row{
            if num < tempmin{
                tempmin = num
                minrow[i] = j
            }
            if num > maxcolval[j]{
                maxcolval[j] = num
                maxcol[j] = i
            }
        }
    }
    result := []int{}
    for i, val := range minrow{
        if maxcol[val] == i{
            result = append(result, matrix[i][val])
        }
    }
    return result
}

day145 2024-07-20

1605. Find Valid Matrix Given Row and Column Sums

You are given two arrays rowSum and colSum of non-negative integers where rowSum[i] is the sum of the elements in the ith row and colSum[j] is the sum of the elements of the jth column of a 2D matrix. In other words, you do not know the elements of the matrix, but you do know the sums of each row and column.

Find any matrix of non-negative integers of size rowSum.length x colSum.length that satisfies the rowSum and colSum requirements.

Return a 2D array representing any matrix that fulfills the requirements. It’s guaranteed that at least one matrix that fulfills the requirements exists.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0720Jr3skSqkPy0Y.png

题解

本题使用贪心算法, 在填充每一行的数字时, 每次都取当前位置行和和列和的较小值. 再分别从行和和列和中减去当前取的值. 继续填充下一个位置的数字. 这样可以保证填充的每个数字都不会使当前行和列超过和的限制(实际按这个思路只要取小于等于二者较小值即可), 直接取二者中的较小值可以保证二者中的一个变为0. 避免了后续继续填充时可能会因行或列的和的限制而无法取得合适的数的问题(0对和没有影响). 而当和为0时只需将该位置填充为0即可.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func restoreMatrix(rowSum []int, colSum []int) [][]int {
    result := [][]int{}
    collen := len(colSum)
    rowlen := len(rowSum)
    for i:=0;i<rowlen;i++{
        temprow := []int{}
        for j:=0;j<collen;j++{
            current := min(rowSum[i],colSum[j])
            temprow = append(temprow, current)
            rowSum[i] -= current
            colSum[j] -= current
        }
        result = append(result, temprow)
    }
    return result
}

day146 2024-07-21

2392. Build a Matrix With Conditions

You are given a positive integer k. You are also given:

a 2D integer array rowConditions of size n where rowConditions[i] = [abovei, belowi], and a 2D integer array colConditions of size m where colConditions[i] = [lefti, righti]. The two arrays contain integers from 1 to k.

You have to build a k x k matrix that contains each of the numbers from 1 to k exactly once. The remaining cells should have the value 0.

The matrix should also satisfy the following conditions:

The number abovei should appear in a row that is strictly above the row at which the number belowi appears for all i from 0 to n - 1. The number lefti should appear in a column that is strictly left of the column at which the number righti appears for all i from 0 to m - 1. Return any matrix that satisfies the conditions. If no answer exists, return an empty matrix.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0721PAEBPtA5CUTD.png

题解

本题的条件实际上隐含了一种先后顺序, above在below的上面可以理解为above在below之前, 拓扑排序就是解决这种广义上的"顺序关系", 从a->b存在一种后继关系. 可以将above和below的关系视为有向图中的一条边, 从above指向below. 但本题中可能会出现环, 出现环即产生了冲突, 无法进行拓扑排序. 此时无解, 因此先判断由两个条件数组构建的有向图是否包含环, 包含环则无解, 不包含环继续进行拓扑排序, 得到拓扑排序后的数组, 按照数组中数对应的下标将其放入对应的行或者列. 注意对行进行拓扑排序得到的结果和对列进行拓扑排序得到的结果二者并不冲突. 所以分别排序并按顺序填入数组就能得到正确答案.

代码

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
func buildMatrix(k int, rowConditions [][]int, colConditions [][]int) [][]int {
    // 定义检查环和拓扑排序的辅助函数
    hasCycle := func(graph [][]int, k int) bool {
        inDegree := make([]int, k+1)
        for _, edge := range graph {
            for _,value := range edge{
                inDegree[value]++
            }
        }

        queue := []int{}
        for i := 1; i <= k; i++ {
            if inDegree[i] == 0 {
                queue = append(queue, i)
            }
        }

        visited := 0
        for len(queue) > 0 {
            node := queue[0]
            queue = queue[1:]
            visited++
            for _, neighbor := range graph[node] {
                inDegree[neighbor]--
                if inDegree[neighbor] == 0 {
                    queue = append(queue, neighbor)
                }
            }
        }
        return visited != k
    }

    topologicalSort := func(graph [][]int, k int) []int {
        inDegree := make([]int, k+1)
        for _, edge := range graph {
            for _,value := range edge{
                inDegree[value]++
            }
        }

        queue := []int{}
        for i := 1; i <= k; i++ {
            if inDegree[i] == 0 {
                queue = append(queue, i)
            }
        }

        order := []int{}
        for len(queue) > 0 {
            node := queue[0]
            queue = queue[1:]
            order = append(order, node)
            for _, neighbor := range graph[node] {
                inDegree[neighbor]--
                if inDegree[neighbor] == 0 {
                    queue = append(queue, neighbor)
                }
            }
        }
        if len(order) != k {
            return nil
        }
        return order
    }

    // 构建行和列的有向图
    rowGraph := make([][]int, k+1)
    colGraph := make([][]int, k+1)
    for i := 0; i <= k; i++ {
        rowGraph[i] = []int{}
        colGraph[i] = []int{}
    }
    for _, cond := range rowConditions {
        rowGraph[cond[0]] = append(rowGraph[cond[0]], cond[1])
    }
    for _, cond := range colConditions {
        colGraph[cond[0]] = append(colGraph[cond[0]], cond[1])
    }

    // 检查是否存在环
    if hasCycle(rowGraph, k) || hasCycle(colGraph, k) {
        return [][]int{}
    }

    // 获取拓扑排序
    rowOrder := topologicalSort(rowGraph, k)
    colOrder := topologicalSort(colGraph, k)
    if rowOrder == nil || colOrder == nil {
        return [][]int{}
    }

    // 构建结果矩阵
    matrix := make([][]int, k)
    for i := range matrix {
        matrix[i] = make([]int, k)
    }

    rowPos := make(map[int]int)
    colPos := make(map[int]int)
    for i, num := range rowOrder {
        rowPos[num] = i
    }
    for i, num := range colOrder {
        colPos[num] = i
    }

    for num := 1; num <= k; num++ {
        matrix[rowPos[num]][colPos[num]] = num
    }

    return matrix
}

day147 2024-07-22

2418. Sort the People

You are given an array of strings names, and an array heights that consists of distinct positive integers. Both arrays are of length n.

For each index i, names[i] and heights[i] denote the name and height of the ith person.

Return names sorted in descending order by the people’s heights.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0722oZxFqSgIHFvB.png

题解

本题先将人名和身高绑定成一个结构体, 再根据身高排序, 思路是比较简单, 因此手动实现快排用于身高排序.

代码

 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
func sortPeople(names []string, heights []int) []string {
    type people struct{
        name string
        height int
    }
    peoples := []people{}
    for i,name := range names{
        peoples = append(peoples, people{name, heights[i]})
    }
    var quicksort func(sortpeople []people)
    quicksort = func(sortpeople []people){
        if len(sortpeople) <= 1{
            return
        }
        flag := sortpeople[0]
        i := 1
        j := len(sortpeople)-1
        for i<j{
            if sortpeople[i].height < flag.height{
                for sortpeople[j].height < flag.height && j>i{
                    j--
                }
                sortpeople[i],sortpeople[j] = sortpeople[j],sortpeople[i]
            }
            i++
        }
        if sortpeople[j].height > sortpeople[0].height{
            sortpeople[0],sortpeople[j] = sortpeople[j],sortpeople[0]
        }
        quicksort(sortpeople[0:j])
        quicksort(sortpeople[j:])
        return
    }
    quicksort(peoples)
    result := []string{}
    for _,value := range peoples{
        result = append(result, value.name)
    }
    return result
}

day148 2024-07-23

1636. Sort Array by Increasing Frequency

Given an array of integers nums, sort the array in increasing order based on the frequency of the values. If multiple values have the same frequency, sort them in decreasing order.

Return the sorted array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0723bKEQAXFqTMve.png

题解

本题可使用计数排序, 先使用map统计各个数字出现的次数, 再直接对原始数组进行排序, 首先根据出现的次数排序, 如果两个数字出现次数相同则根据数字大小排序, 将数字更大的放在前面, 这里只需根据上面的条件写好go内置的sort函数即可完成排序. 最后返回结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func frequencySort(nums []int) []int {
    mapp:= make(map[int]int)
    n:= len(nums)
    if n==1{
        return nums
    }
    for i:=0;i<n;i++{
        mapp[nums[i]]++
    }
    sort.Slice(nums, func(i, j int)bool{
        if mapp[nums[i]]==mapp[nums[j]]{
            return nums[i]>nums[j]
        }
        return mapp[nums[i]]<mapp[nums[j]]
    })
    return nums
}

day149 2024-07-24

2191. Sort the Jumbled Numbers

You are given a 0-indexed integer array mapping which represents the mapping rule of a shuffled decimal system. mapping[i] = j means digit i should be mapped to digit j in this system.

The mapped value of an integer is the new integer obtained by replacing each occurrence of digit i in the integer with mapping[i] for all 0 <= i <= 9.

You are also given another integer array nums. Return the array nums sorted in non-decreasing order based on the mapped values of its elements.

Notes:

Elements with the same mapped values should appear in the same relative order as in the input. The elements of nums should only be sorted based on their mapped values and not be replaced by them.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0724RmELEDU4yGW7.png

题解

本题要解决两个问题, 一是将原始数字映射为其对应的映射数字, 二是将映射数字排序并按序排列对应的原始数字并返回. 将原始数字映射为对应数字只需每次除以10, 按位处理即可, 注意单独处理0的情况. 对映射后数字排序并反映到原来的数组上需要注意体重明确说明了对于映射数字相同的数字需保持原始数组的顺序, 因此需要稳定排序, 在go中使用sort.SliceStable即可进行稳定排序.

代码

 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
func sortJumbled(mapping []int, nums []int) []int {
    type mapNum struct{
        raw int
        mapped int
    }
    mapNums := []mapNum{}
    for _,value := range nums{
        mapNums = append(mapNums, mapNum{value, toMap(value, mapping)})
    }
    sort.SliceStable(mapNums, func(i,j int)bool{
        return mapNums[i].mapped < mapNums[j].mapped
    })
    result := []int{}
    for _,num := range mapNums{
        result = append(result, num.raw)
    }
    return result

}

func toMap(raw int, mapping []int)int{
    if raw == 0{
        return mapping[0]
    }else{
        result := 0
        i := 1
        remain := 0
        quote := 0
        for raw != 0{
            remain = raw % 10
            quote = raw / 10
            raw = quote
            result += mapping[remain] * i
            i *= 10
        }
        return result
    }
}

day150 2024-07-25

912. Sort an Array

Given an array of integers nums, sort the array in ascending order and return it.

You must solve the problem without using any built-in functions in O(nlog(n)) time complexity and with the smallest space complexity possible.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0725AQCvbUrAA3cY.png

题解

本题是一道基础题, 要求手动实现一种排序方式进行排序, 之前已经实现过归并排序, 快排, 计数排序, 因此本次实现堆排序, 实现堆排序的关键在于构建一个最小堆.

代码

 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

type IntHeap []int

func (h IntHeap) Len() int {
	return len(h)
}

func (h IntHeap) Less(i, j int) bool {
	return h[i] < h[j]
}

func (h IntHeap) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
}

func (h *IntHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
}

func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

func sortArray(nums []int) []int {
	h := &IntHeap{}
	heap.Init(h) // 初始化堆

	// 将所有元素推入堆中
	for _, num := range nums {
		heap.Push(h, num)
	}

	sortedArray := make([]int, 0, len(nums))
	for h.Len() > 0 {
		sortedArray = append(sortedArray, heap.Pop(h).(int))
	}
	return sortedArray
}

day151 2024-07-26

1334. Find the City With the Smallest Number of Neighbors at a Threshold Distance

There are n cities numbered from 0 to n-1. Given the array edges where edges[i] = [fromi, toi, weighti] represents a bidirectional and weighted edge between cities fromi and toi, and given the integer distanceThreshold.

Return the city with the smallest number of cities that are reachable through some path and whose distance is at most distanceThreshold, If there are multiple such cities, return the city with the greatest number.

Notice that the distance of a path connecting cities i and j is equal to the sum of the edges’ weights along that path.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0726o56eEoMULQLn.png

题解

本题思路是比较明确的, 使用dijistra算法找到每个点到其他点的最短距离并保留在阈值距离以内的点, 比较每个点在阈值距离以内的点的数量, 找到点的数量最少的点并返回值最大的即可, 此处也可以使用Floyd算法来计算最短路径.

代码

 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
type Edge struct {
	to     int
	weight int
}

type MinHeap struct {
	edges []Edge
}

func (h *MinHeap) Len() int           { return len(h.edges) }
func (h *MinHeap) Less(i, j int) bool { return h.edges[i].weight < h.edges[j].weight }
func (h *MinHeap) Swap(i, j int)      { h.edges[i], h.edges[j] = h.edges[j], h.edges[i] }

func (h *MinHeap) Push(x interface{}) {
	h.edges = append(h.edges, x.(Edge))
}

func (h *MinHeap) Pop() interface{} {
	old := h.edges
	n := len(old)
	x := old[n-1]
	h.edges = old[0 : n-1]
	return x
}

func dijkstra(n int, edges [][]int, start int, distanceThreshold int) []int {
	graph := make([][]Edge, n)
	for _, edge := range edges {
		graph[edge[0]] = append(graph[edge[0]], Edge{edge[1], edge[2]})
		graph[edge[1]] = append(graph[edge[1]], Edge{edge[0], edge[2]})
	}

	dist := make([]int, n)
	for i := range dist {
		dist[i] = math.MaxInt32
	}
	dist[start] = 0

	h := &MinHeap{}
	heap.Push(h, Edge{start, 0})

	for h.Len() > 0 {
		current := heap.Pop(h).(Edge)
		currentCity := current.to
		currentDistance := current.weight

		if currentDistance > distanceThreshold {
			break
		}

		for _, edge := range graph[currentCity] {
			if newDist := currentDistance + edge.weight; newDist < dist[edge.to] {
				dist[edge.to] = newDist
				heap.Push(h, Edge{edge.to, newDist})
			}
		}
	}

	return dist
}

func findTheCity(n int, edges [][]int, distanceThreshold int) int {
	minReachable := n + 1
	cityWithMinReachable := -1

	for i := 0; i < n; i++ {
		distance := dijkstra(n, edges, i, distanceThreshold)
		reachableCount := 0

		for _, d := range distance {
			if d <= distanceThreshold {
				reachableCount++
			}
		}

		if reachableCount < minReachable || (reachableCount == minReachable && i > cityWithMinReachable) {
			minReachable = reachableCount
			cityWithMinReachable = i
		}
	}

	return cityWithMinReachable
}

day152 2024-07-27

2976. Minimum Cost to Convert String I

You are given two 0-indexed strings source and target, both of length n and consisting of lowercase English letters. You are also given two 0-indexed character arrays original and changed, and an integer array cost, where cost[i] represents the cost of changing the character original[i] to the character changed[i].

You start with the string source. In one operation, you can pick a character x from the string and change it to the character y at a cost of z if there exists any index j such that cost[j] == z, original[j] == x, and changed[j] == y.

Return the minimum cost to convert the string source to the string target using any number of operations. If it is impossible to convert source to target, return -1.

Note that there may exist indices i, j such that original[j] == original[i] and changed[j] == changed[i].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0727giQdhU1gMved.png

题解

本题和昨天的题目核心思路上基本相同, 只需要遍历原始字母和其映射字母以及相应的花费, 将字母对应的映射字母和其花费保存起来, 这里可以使用一个map, 也可以把字母当成下标, 用一个26*26的数组来保存(只有小写字母). 关键还是当原始字母和映射字母均相同时如果有多个不同的花费, 要将最小的花费保存下来. 因为这里我们只关心最小花费, 每个映射都是最小花费最终得到的即为最小花费, 是一个比较清晰的贪心法. 需要注意的是可能存在c->e, e->b这样的转换的花费比c->b小得多, 是不是感觉很熟悉, 实际上这个问题完全可以把字母当成节点, 花费当成边的花费, 这样问题就转换成了和昨天完全一样的问题. 使用同样的方法求解即可. 本题中求解最短路径我们使用Floyd-Warshall算法, 该算法的原理为动态规划.

代码

 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
func minimumCost(source string, target string, original []byte, changed []byte, cost []int) int64 {
    costs := make([][]int, 26)
    for i,_ := range costs{
        costs[i] = make([]int, 26)
        for j:= range costs[i]{
            costs[i][j] = math.MaxInt32
        }
        costs[i][i] = 0
    }
    for i,_ := range original{
        costs[original[i]-'a'][changed[i]-'a'] = min(cost[i],costs[original[i]-'a'][changed[i]-'a'])
    }
    for k:=0;k<26;k++{
        for i:=0;i<26;i++{
            for j:=0;j<26;j++{
                if costs[i][j] > costs[i][k]+costs[k][j] {
						costs[i][j] = costs[i][k] + costs[k][j]
				}
            }
        }
    }
    result := 0
    for i := range source{
        if costs[source[i]-'a'][target[i]-'a'] == math.MaxInt32{
            return -1
        }else{
            result += costs[source[i]-'a'][target[i]-'a']
        }
    }
    return int64(result)
}

总结

如果点特别多并且图为稀疏图, 用map来保存边的权重并执行Floyd算法效率会更高, 避免了不连通的边的遍历.

day153 2024-07-28

2045. Second Minimum Time to Reach Destination

A city is represented as a bi-directional connected graph with n vertices where each vertex is labeled from 1 to n (inclusive). The edges in the graph are represented as a 2D integer array edges, where each edges[i] = [ui, vi] denotes a bi-directional edge between vertex ui and vertex vi. Every vertex pair is connected by at most one edge, and no vertex has an edge to itself. The time taken to traverse any edge is time minutes.

Each vertex has a traffic signal which changes its color from green to red and vice versa every change minutes. All signals change at the same time. You can enter a vertex at any time, but can leave a vertex only when the signal is green. You cannot wait at a vertex if the signal is green.

The second minimum value is defined as the smallest value strictly larger than the minimum value.

For example the second minimum value of [2, 3, 4] is 3, and the second minimum value of [2, 2, 4] is 4. Given n, edges, time, and change, return the second minimum time it will take to go from vertex 1 to vertex n.

Notes:

You can go through any vertex any number of times, including 1 and n. You can assume that when the journey starts, all signals have just turned green.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0728Jbh6QjYRGlCE.png

题解

本题是一道难度, 解决本题要将问题分解开. 首先考虑找到第二小时长到达目的地的路的问题, 因为经过每条路需要的时间都相同, 因此经过的节点数量与最终需要的时长成正比, 因此只需要找到经过节点第二多的路径即可. 注意当到达目的地的路径长度有多个不同值时, 直接存在第二长的路径, 而到达目的地的路径长度均相同时, 需要回退一步, 这种情况只需将最短路径经过的节点+2即得到第二长的路径. 另一个需要解决的问题是如何找到所有能到达目的地的路径, 这里可以使用bfs来解决, 使用bfs的好处在于不需要考虑只有一个长度的路径和不同长度的路径时不同的处理方式, 设置两个距离数组表示第一次访问到某节点需要的时间和第二次访问到某节点需要的时间, 只需要在bfs时不断填充这两个数组, 当某个节点在这两个数组中都存在值时, 说明该节点已经找到了最短路径和次短路径, 其余路径我们并不关心, 直接跳过该节点的其余处理即可.

代码

 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
func secondMinimum(n int, edges [][]int, time int, change int) int {
	g := make([][]int, n+1)
	for _, edge := range edges {
		u, v := edge[0], edge[1]
		g[u] = append(g[u], v)
		g[v] = append(g[v], u)
	}

	dist1 := make([]int, n+1)
	dist2 := make([]int, n+1)
	for i := 1; i <= n; i++ {
		dist1[i] = -1
		dist2[i] = -1
	}
	dist1[1] = 0

	q := list.New()
	q.PushBack([]interface{}{1, 1})

	for q.Len() > 0 {
		front := q.Remove(q.Front()).([]interface{})
		x := front[0].(int)
		freq := front[1].(int)

		t := dist1[x]
		if freq == 2 {
			t = dist2[x]
		}

		if (t/change)%2 == 1 {
			t = (t/change + 1)*change + time
		} else {
			t += time
		}

		for _, y := range g[x] {
			if dist1[y] == -1 {
				dist1[y] = t
				q.PushBack([]interface{}{y, 1})
			} else if dist2[y] == -1 && dist1[y] != t {
				if y == n {
					return t
				}
				dist2[y] = t
				q.PushBack([]interface{}{y, 2})
			}
		}
	}

	return 0
}

day154 2024-07-29

1395. Count Number of Teams

There are n soldiers standing in a line. Each soldier is assigned a unique rating value.

You have to form a team of 3 soldiers amongst them under the following rules:

Choose 3 soldiers with index (i, j, k) with rating (rating[i], rating[j], rating[k]). A team is valid if: (rating[i] < rating[j] < rating[k]) or (rating[i] > rating[j] > rating[k]) where (0 <= i < j < k < n). Return the number of teams you can form given the conditions. (soldiers can be part of multiple teams).

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0729PmaRN33Zh8l6.png

题解

这种类型的问题核心在于找到通过保存什么样的状态, 可以简化枚举的过程, 避免重复枚举. 思考一下可知只要知道某个数前面和后面比该数大和比该数小的数字的个数, 即可得到以这个数为中间数的符合要求的三个数字的组合. 如某个数前面比它小(大)的数字有n个, 后面比它大(小)的数字有m个, 则以该数为中间数字满足条件的三数组合有m*n个. 依次遍历每个数字, 将数字作为三个数字中的中间数字来找到满足条件的组合, 这样寻找组合不会出现重复计数的问题. 因为每次用于固定的中间数字都不相同. 可以使用两个数组来保存所有位置前后比其大或者小的数字个数. 这里每个数字都要遍历一遍数组来找到前后比这个数字大或者小的数字个数, 总体时间复杂度为n^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
28
29
30
31
32
33
34
35
36
37
38
39
func numTeams(rating []int) int {
    length := len(rating)
    type frontback struct{
        front int
        back int
    }
    small := make([]frontback, length)
    big := make([]frontback, length)
    for i:=1;i<length;i++{
        countsmall := 0
        countbig := 0
        for j:=0;j<i;j++{
            if rating[j] < rating[i]{
                countsmall++
            }else if rating[j] > rating[i]{
                countbig++
            }
        }
        small[i].front = countsmall
        big[i].front = countbig
        countsmall = 0
        countbig = 0
        for j:=i+1;j<length;j++{
            if rating[j] < rating[i]{
                countsmall++
            }else if rating[j] > rating[i]{
                countbig++
            }
        }
        small[i].back = countsmall
        big[i].back = countbig
    }
    result := 0
    for i:=1;i<length;i++{
        result += small[i].front * big[i].back
        result += small[i].back * big[i].front
    }
    return result
}

day155 2024-07-30

1653. Minimum Deletions to Make String Balanced

You are given a string s consisting only of characters ‘a’ and ‘b’​​​​.

You can delete any number of characters in s to make s balanced. s is balanced if there is no pair of indices (i,j) such that i < j and s[i] = ‘b’ and s[j]= ‘a’.

Return the minimum number of deletions needed to make s balanced.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0730MZPahToxI3xk.png

题解

本题想实现题中所述的平衡字符串, 需要在前面删掉一定数量的b, 后面删掉一定数量的a以满足题目要求的b都在a的后面的条件, 则先统计字符串中a和b的数量, 再进行计算, 每次遇到b时计算该b前面需要删除的b的个数和该b后面的a的个数并加和作为需要删除的字符总数. 极端情况为当第一次遇到b且b后存在a时, 相当于删掉所有b后的a. 对于ababa这样的字符串, 第一次遇到b我们计算的是删掉后面的两个a, 第二次遇到b我们计算的是第一个b和第二个b后面的a. 通过这种方式遍历数组并计算需要删掉的字符个数即可.

代码

 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
func minimumDeletions(s string) int {
    counta := 0
    prea := 0
    delb := 0
    preb := 0
    countb := 0
    for i := range s{
        if s[i] == 'a'{
            counta++
        }else if s[i] == 'b'{
            countb++
        }
    }
    result := 1000000
    for i := range s{
        if s[i] == 'a'{
            prea++
            delb += preb
            preb = 0
        }else if s[i] == 'b'{
            preb++
            result = min(result, counta - prea + delb)
        }
    }
    if result == 1000000{
        result = 0
    }
    result = min(result, countb)
    return result
}

总结

这个思路还是有些复杂, 实际上只要不断保证局部最优最终就能得到全局最优, 而局部最优可以通过如下方式得到, 对于一个同时包含a和b的字符串, 如果a前面出现了b, 那么可以删掉b后面的a, 也可以删掉a前面的b, 但当b后面没有出现a时, b不需要被删除. 因此可以先将出现的b的次数保存, 当遇到a时再判断在之前已经删除的字符基础上再多删掉一个a和删掉前面的全部b哪种方式需要删除的字符个数更少.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func minimumDeletions(s string) int {
    var res, bCount int

    for i := range s {
        if s[i] == 'a' {
            res = min(res+1, bCount)
        } else {
            bCount++
        }
    }
    return res
}

day156 2024-07-31

1105. Filling Bookcase Shelves

You are given an array books where books[i] = [thicknessi, heighti] indicates the thickness and height of the ith book. You are also given an integer shelfWidth.

We want to place these books in order onto bookcase shelves that have a total width shelfWidth.

We choose some of the books to place on this shelf such that the sum of their thickness is less than or equal to shelfWidth, then build another level of the shelf of the bookcase so that the total height of the bookcase has increased by the maximum height of the books we just put down. We repeat this process until there are no more books to place.

Note that at each step of the above process, the order of the books we place is the same order as the given sequence of books.

For example, if we have an ordered list of 5 books, we might place the first and second book onto the first shelf, the third book on the second shelf, and the fourth and fifth book on the last shelf. Return the minimum possible height that the total bookshelf can be after placing shelves in this manner.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0731wJnxtMCrafvs.png

题解

本题是典型的动态规划的题目, 要求必须按顺序放置书, 则需要放置新的一本书时我们有两个选择. 1. 在之前放过书的最后一排继续放书 2. 开始在新的一排放书 这两种选择最终哪种会得到更好的结果是未知的. 假设f(i)为前i本书能得到的最小高度. 对于放置第j本书而言, 可以将其单独作为一排, 则总高度为第j本书的高度j+f(j-1). 也可以将其与前面的书一块放在同一排, 如将j和j-1本书放在同一排, 此时总高度为max(j,j-1)+f(j-2). 以此类推(max(j,j-1,j-2…)+f(j-4))直到多本书不能够放在同一排为止. 将已经计算过的f(i)保存起来可以减少重复运算, f(0)=0,且f(1)=books[0][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
func minHeightShelves(books [][]int, shelfWidth int) int {
    return arrangeBooks(books, shelfWidth)
}

func arrangeBooks(books [][]int, maxShelfWidth int) int {
    minHeights := make([]int, len(books)+1)
    for i := range minHeights {
        minHeights[i] = math.MaxInt32
    }
    minHeights[0] = 0

    for bookIndex := 1; bookIndex <= len(books); bookIndex++ {
        currentShelfHeight := 0
        currentShelfWidth := 0

        for lastBook := bookIndex - 1; lastBook >= 0; lastBook-- {
            currentBookThickness := books[lastBook][0]
            currentBookHeight := books[lastBook][1]

            if currentShelfWidth+currentBookThickness > maxShelfWidth {
                break
            }

            currentShelfWidth += currentBookThickness
            currentShelfHeight = max(currentShelfHeight, currentBookHeight)

            currentArrangementHeight := minHeights[lastBook] + currentShelfHeight
            minHeights[bookIndex] = min(minHeights[bookIndex], currentArrangementHeight)
        }
    }

    return minHeights[len(books)]
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

day157 2024-08-01

2678. Number of Senior Citizens

You are given a 0-indexed array of strings details. Each element of details provides information about a given passenger compressed into a string of length 15. The system is such that:

The first ten characters consist of the phone number of passengers. The next character denotes the gender of the person. The following two characters are used to indicate the age of the person. The last two characters determine the seat allotted to that person. Return the number of passengers who are strictly more than 60 years old.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0801B5jDYOOc70ze.png

题解

本题是一道简单题, 最基本的字符串操作, 一道只需要能把题读懂并且懂如何通过数组下标访问数组就能解决的问题, 通过下标找到年龄对应的两个字符并且将其转换为数字和60比较即可.

代码

1
2
3
4
5
6
7
8
9
func countSeniors(details []string) int {
    result := 0
    for _, detail := range details{
        if (detail[11]-'0')*10+(detail[12]-'0') > 60{
            result++
        }
    }
    return result
}

12. Integer to Roman

Seven different symbols represent Roman numerals with the following values:

Symbol Value I 1 V 5 X 10 L 50 C 100 D 500 M 1000 Roman numerals are formed by appending the conversions of decimal place values from highest to lowest. Converting a decimal place value into a Roman numeral has the following rules:

If the value does not start with 4 or 9, select the symbol of the maximal value that can be subtracted from the input, append that symbol to the result, subtract its value, and convert the remainder to a Roman numeral. If the value starts with 4 or 9 use the subtractive form representing one symbol subtracted from the following symbol, for example, 4 is 1 (I) less than 5 (V): IV and 9 is 1 (I) less than 10 (X): IX. Only the following subtractive forms are used: 4 (IV), 9 (IX), 40 (XL), 90 (XC), 400 (CD) and 900 (CM). Only powers of 10 (I, X, C, M) can be appended consecutively at most 3 times to represent multiples of 10. You cannot append 5 (V), 50 (L), or 500 (D) multiple times. If you need to append a symbol 4 times use the subtractive form. Given an integer, convert it to a Roman numeral.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0801yrz4KdPhaYyL.png

题解

本题主要也是读懂题意, 读懂题目分别获取数字千分位, 百分位, 十分位, 个位的数字, 并根据题目内容进行转换即可. 本题的巧妙之处在于可以将对应的字母不同的数字都列出来并从数字中按从大到小减去这些数字并加上对应的罗马字母, 这很像组合硬币的问题.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import "strings"

func intToRoman(num int) string {
    values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
    symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}

    var builder strings.Builder

    for i := 0; num > 0; i++ {
        for num >= values[i] {
            builder.WriteString(symbols[i])
            num -= values[i]
        }
    }

    return builder.String()
}

day158 2024-08-02

2134. Minimum Swaps to Group All 1’s Together II

A swap is defined as taking two distinct positions in an array and swapping the values in them.

A circular array is defined as an array where we consider the first element and the last element to be adjacent.

Given a binary circular array nums, return the minimum number of swaps required to group all 1’s present in the array together at any location.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0802KouymBwKOk9y.png

题解

本题中每个数组中1的数量是固定的, 我们先计数数组中1的个数后只要找到包含1数量最多的长度为数组全部1的数量的子数组, 则此子数组中0的个数即为最少的交换次数. 如何找到这样的子数组呢, 对于一个给定的数组, 因为1的数量固定, 则要找到满足条件的长度固定的子数组, 可以使用滑动窗口, 窗口长度设置为1的数量, 随着窗口滑动不断更新窗口中0的个数的最小值. 从数组开头开始滑动直到数组结尾. 最终得到的最小值即为最少交换次数.

代码

 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
func minSwaps(nums []int) int {
    countone := 0
    for _,num := range nums{
        if num == 1{
            countone++
        }
    }
    length := len(nums)
    min0 := 0
    current0 := 0
    left := 0
    right := countone-1
    for _,num := range nums[0:countone]{
            if num == 0{
                min0++
            }
    }
    current0 = min0
    for left < length{
        if nums[(right+1)%length] == 0{
            current0++
        }
        if nums[left] == 0{
            current0--
        }
        min0 = min(current0, min0)
        left++
        right++
    }
    return min0
}

day159 2024-08-03

1460. Make Two Arrays Equal by Reversing Subarrays

You are given two integer arrays of equal length target and arr. In one step, you can select any non-empty subarray of arr and reverse it. You are allowed to make any number of steps.

Return true if you can make arr equal to target or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0803cdry9pcOd9hx.png

题解

本题要抓住问题的核心, arr数组中可以对任意子数组进行翻转, 则实际上通过不同长度的数组翻转的组合可以实现交换数组中任意两个数的位置, 因为题目只要求判断arr能否通过子数组翻转变成target, 则只要两个数组中有相同数量的相同字母, 根据之前的结论对arr数组中任意两个数都可以交换的结论可知一定可以通过不断交换位置使得arr和target相同. 先遍历target数组, 保存数组中的字母及其对应的个数, 再遍历arr数组, 看是否字母及其个数均与target相同即得结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func canBeEqual(target []int, arr []int) bool {
    targets := make([]int, 1001)
    for _, num := range target{
        targets[num]++
    }
    for _,num := range arr{
        targets[num]--
        if targets[num] < 0{
            return false
        }
    }
    return true
}

13. Roman to Integer

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

Symbol Value I 1 V 5 X 10 L 50 C 100 D 500 M 1000 For example, 2 is written as II in Roman numeral, just two ones added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

I can be placed before V (5) and X (10) to make 4 and 9. X can be placed before L (50) and C (100) to make 40 and 90. C can be placed before D (500) and M (1000) to make 400 and 900. Given a roman numeral, convert it to an integer.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0803G9q8SYYzSAm5.png

题解

本题是之前整数转换成罗马数字的题目的反向问题, 将罗马数字转换成整数, 则采取类似的思路, 只不过这次从后向前遍历罗马字符串并将对应的数字加到最终结果中, 用一个变量保存前一次加的值, 如果当前数字比前一次加上的值小则应减去当前数字, 最终得到结果.

代码

 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
func romanToInt(s string) int {
    romanMap := map[rune]int{
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }

    total := 0
    prevValue := 0

    for i := len(s) - 1; i >= 0; i-- {
        currentValue := romanMap[rune(s[i])]

        if currentValue < prevValue {
            total -= currentValue
        } else {
            total += currentValue
        }

        prevValue = currentValue
    }

    return total
}

day160 2024-08-04

1508. Range Sum of Sorted Subarray Sums

You are given the array nums consisting of n positive integers. You computed the sum of all non-empty continuous subarrays from the array and then sorted them in non-decreasing order, creating a new array of n * (n + 1) / 2 numbers.

Return the sum of the numbers from index left to index right (indexed from 1), inclusive, in the new array. Since the answer can be a huge number return it modulo 109 + 7.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0804D4KV3I3rptIz.png

题解

本题先考虑计算子数组的和时, 如果固定子数组的开始下标, 不断扩展子数组的长度, 则这些子数组的和具有天然的递增顺序. 即可以通过固定子数组的开始下标得到以某下标开始的子数组和的递增序列. 遍历到以下一个数组下标开始的子数组并计算子数组和时, 相当于合并两个递增的数组, 将和不断插入到之前已经有序的数组中, 为了快速插入到之前的有序数组并且保持数组仍为有序数组, 可以使用优先级队列.

代码

 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
type Pq []int
func (pq Pq)Len() int{
        return len(pq)
    }

func (pq Pq) Less(i, j int)bool{
        return pq[i] < pq[j]
    }
func (pq Pq) Swap(i, j int){
        pq[i],pq[j] = pq[j],pq[i]
    }
func (pq *Pq) Pop() interface{} {
        n := len(*pq)
        item := (*pq)[n-1]
        (*pq) = (*pq)[0:n-1]
        return item
    }
func (pq *Pq) Push(x interface{}) {
        *pq = append(*pq, x.(int))
    }

func rangeSum(nums []int, n int, left int, right int) int {
    pq := Pq{}
    sum := 0
    for j:=0;j<n;j++{
            sum += nums[j]
            pq = append(pq, sum)
    }
    heap.Init(&pq)
    for i:=1;i<n;i++{
        sum = 0
        for j:=i;j<n;j++{
            sum += nums[j]
            heap.Push(&pq, sum)
        }
    }
    for i:=1;i<left;i++{
        heap.Pop(&pq)
    }
    result := 0
    for left<=right{
        result += heap.Pop(&pq).(int)
        left++
    }
    return result % (1000000007)
}

总结

这种解法时间复杂度很高, 可以使用前缀和的前缀和求解, 这种思路比较复杂, 推荐看官方题解

官方题解

day161 2024-08-05

2053. Kth Distinct String in an Array

A distinct string is a string that is present only once in an array.

Given an array of strings arr, and an integer k, return the kth distinct string present in arr. If there are fewer than k distinct strings, return an empty string “”.

Note that the strings are considered in the order in which they appear in the array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0805UlLbJTx7tG9M.png

题解

本题是简单题, 先遍历数组并使用map保存字符串及其对应的出现次数, 再遍历数组找到只出现过一次的第k个字符串即可, 如没有这样的字符串返回空字符串.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func kthDistinct(arr []string, k int) string {
    maps := map[string]int{}
    for _, str := range arr{
        maps[str] = maps[str] + 1
    }
    count := 0
    for _, str := range arr{
        if maps[str] == 1{
            count++
        }
        if count == k{
            return str
        }
    }
    return ""
}

6. Zigzag Conversion

The string “PAYPALISHIRING” is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)

P A H N A P L S I I G Y I R And then read line by line: “PAHNAPLSIIGYIR”

Write the code that will take a string and make this conversion given a number of rows:

string convert(string s, int numRows);

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0805OnbsL4hII6Vy.png

题解

本题可以直接模拟z形排列的过程, 用一个二维数组来保存z形排列过程中每一行的字符串, 最后依次将每行字符串拼接起来即可. 模拟排列过程可以用两个变量, 一个控制排列的方向(从上到下或者从下到上), 一个控制当前的行数. 用strings.Builder可以快速拼接字符串.

代码

 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
func convert(s string, numRows int) string {
    if numRows == 1{
        return s
    }
    zig := make([]strings.Builder, numRows)
    direction := 0
    row := 0
    for _, char := range s{
        zig[row].WriteRune(char)
        if direction == 0{
            row++
        }
        if direction == 1{
            row--
        }
        if row == numRows-1{
            direction = 1
        }
        if row == 0{
            direction = 0
        }
    }
    var result strings.Builder
    for _, builder := range zig{
        result.WriteString(builder.String())
    }
    return result.String()
}

day162 2024-08-06

3016. Minimum Number of Pushes to Type Word II

You are given a string word containing lowercase English letters.

Telephone keypads have keys mapped with distinct collections of lowercase English letters, which can be used to form words by pushing them. For example, the key 2 is mapped with [“a”,“b”,“c”], we need to push the key one time to type “a”, two times to type “b”, and three times to type “c” .

It is allowed to remap the keys numbered 2 to 9 to distinct collections of letters. The keys can be remapped to any amount of letters, but each letter must be mapped to exactly one key. You need to find the minimum number of times the keys will be pushed to type the string word.

Return the minimum number of pushes needed to type word after remapping the keys.

An example mapping of letters to keys on a telephone keypad is given below. Note that 1, *, #, and 0 do not map to any letters.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0806QKsLD0fydkoK.png

题解

对于这种问题关键是把问题核心抽象出来, 本题中任意数量的字母可以映射在任意的电话按键上, 则采用类似哈夫曼编码的思想, 将出现频率最高的使其编码长度最短就能使总长度最短. 对应在本题中就是尽量让出现频率高的字母映射到按键的前面位置. 这样总的按键次数就会变少. 可用的按键总共有8个, 则先统计字符串中所有字母出现的频率并从大到小排序, 随后8个为一组进行映射, 如将频率前8的字母映射到8个按键的第一位上, 以此类推. 设组数为k, 将当前组中字母出现的次数与k相乘, 并将这些相加, 得到最终结果. 题目只要求最终的按键次数总和, 因此可以直接用一个数组, 下标表示26个小写字母, 对应的项表示出现频数, 将该数组排序后, 按照之前的思路每8个一组与1,2,3,4相乘即得最终结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func minimumPushes(word string) int {
    chars := make([]int, 26)
    for i,_ := range word{
        chars[word[i] - 'a']++
    }
    sort.Ints(chars)
    result := 0
    for i,_ := range chars{
        result += chars[25-i] * (i/8+1)
    }
    return result
}

day163 2024-08-07

273. Integer to English words

Convert a non-negative integer num to its English words representation.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0807FLw6W1m2ZZzo.png

题解

本题虽然是一道难题, 但思路也是比较清晰的, 首先理解英文的计数是以千为一组进行计数的, 如千(10^3), 百万(10^6), 十亿(10^9). 则通过一个函数单独处理每个三位一组的数据中的三位数最终解析的英文, 再补充上该三位数对应的单位即可. 注意处理0这种特殊情况

代码

 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
86
87
88
89
90
91
func numberToWords(num int) string {
    if num == 0{
        return "Zero"
    }
    data := map[int]string{0: "Zero",
        1: "One",
        2: "Two",
        3: "Three",
        4: "Four",
        5: "Five",
        6: "Six",
        7: "Seven",
        8: "Eight",
        9: "Nine",
        10: "Ten",
        11: "Eleven",
        12: "Twelve",
        13: "Thirteen",
        14: "Fourteen",
        15: "Fifteen",
        16: "Sixteen",
        17: "Seventeen",
        18: "Eighteen",
        19: "Nineteen",
        20: "Twenty",
        30: "Thirty",
        40: "Forty",
        50: "Fifty",
        60: "Sixty",
        70: "Seventy",
        80: "Eighty",
        90: "Ninety",
        100: "Hundred",
        1000: "Thousand",
        1000000: "Million",
        1000000000: "Billion",
        }
    var result strings.Builder
    if num/1000000000 > 0{
        result.WriteString(comma(num/1000000000,data))
        result.WriteString(" ")
        result.WriteString("Billion")
        result.WriteString(" ")
    }
    if (num%1000000000)/1000000 > 0{
        result.WriteString(comma((num%1000000000)/1000000,data))
        result.WriteString(" ")
        result.WriteString("Million")
        result.WriteString(" ")
    }
    if (num%1000000)/1000 > 0{
        result.WriteString(comma((num%1000000)/1000,data))
        result.WriteString(" ")
        result.WriteString("Thousand")
        result.WriteString(" ")
    }
    if (num%1000) > 0{
        result.WriteString(comma(num%1000,data))
    }
    resultstring := result.String()
    if resultstring[len(resultstring)-1] == ' '{
        resultstring = resultstring[:len(resultstring)-1]
    }
    return resultstring
}

func comma(input int,data map[int]string)string{
    var result strings.Builder
    if input / 100 > 0{
        result.WriteString(data[input/100])
        result.WriteString(" ")
        result.WriteString("Hundred")

    }
    if input % 100 < 20 && (input % 100) != 0{
        if input / 100 > 0{
            result.WriteString(" ")
        }
        result.WriteString(data[input % 100])
    }else if (input % 100) != 0{
        if input / 100 > 0{
            result.WriteString(" ")
        }
        result.WriteString(data[input%100-input%10])
        if input % 10 != 0{
            result.WriteString(" ")
            result.WriteString(data[input%10])
        }
    }
    return result.String()
}

day164 2024-08-08

885. Spiral Matrix III

You start at the cell (rStart, cStart) of an rows x cols grid facing east. The northwest corner is at the first row and column in the grid, and the southeast corner is at the last row and column.

You will walk in a clockwise spiral shape to visit every position in this grid. Whenever you move outside the grid’s boundary, we continue our walk outside the grid (but may return to the grid boundary later.). Eventually, we reach all rows * cols spaces of the grid.

Return an array of coordinates representing the positions of the grid in the order you visited them.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0808KR49y4uewtPI.png

题解

本题按照题目要求进行顺时针遍历, 对于超出边界的位置只需不将其输出到结果数组中即能完成题目的要求. 进行顺时针遍历的过程为每走完两个方向就将前进的步数加1, 如一开始的几次移动分别为右1, 下1, 左2, 上2, 右3, 下3… 因此按照这个规律遍历整个矩阵并将不超出边界的位置坐标放入结果数组中. 移动过程中使用一个变量表示方向, 并记录已经走过的在矩阵内部的位置的个数, 直到走过的个数与矩阵内元素的个数相同结束遍历.

代码

 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
func spiralMatrixIII(rows int, cols int, rStart int, cStart int) [][]int {
    position := make([]int, 2)
    position[0] = rStart
    position[1] = cStart
    count := 0
    sum := rows*cols
    step := 1
    result := [][]int{}
    result = append(result, []int{rStart,cStart})
    // 0,1,2,3 --- right down left up
    direction := 0
    for count < sum-1{
        for i:=0;i<step;i++{
            if direction == 0{
                position[1]++
            }else if direction == 1{
                position[0]++
            }else if direction == 2{
                position[1]--
            }else if direction == 3{
                position[0]--
            }
            if position[0] >= 0 && position[0] < rows && position[1] >= 0 && position[1] < cols{
                saveposition := []int{position[0], position[1]}
                result = append(result, saveposition)
                count++
            }
        }
        direction = (direction+1) % 4
        if direction == 2 || direction == 0{
            step++
        }
    }
    return result
}

day165 2024-08-09

840. Magic Squares In Grid

A 3 x 3 magic square is a 3 x 3 grid filled with distinct numbers from 1 to 9 such that each row, column, and both diagonals all have the same sum.

Given a row x col grid of integers, how many 3 x 3 contiguous magic square subgrids are there?

Note: while a magic square can only contain numbers from 1 to 9, grid may contain numbers up to 15.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0809LUp4RVEAKUlN.png

题解

本题要求找到所有满足行列及斜的和都相同且只由1-9组成的的3*3矩阵的个数, 则将1-9填入3*3矩阵中且满足如上要求必须每行每列的和均为1-9的和的1/3即15, 因此只需从头开始依次判断矩阵中的每个3*3矩阵是否满足每行每列及斜行和都为15即可, 任意条件不满足即可继续判断下一个矩阵是否满足条件. 因为矩阵的行列数都不超过10, 因此需要判断的次数并不多. 这里判断全部行列是否满足条件只需将3*3矩阵遍历一遍即可, 用一个sums数组保存列和, 在按行遍历的同时将每列的和可以同时求出(sums[0],[1],[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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func numMagicSquaresInside(grid [][]int) int {
    rows := len(grid)
    cols := len(grid[0])
    if rows<3 || cols<3{
        return 0
    }
    result := 0
    for rowi := 0;rowi<rows-2;rowi++{
        for coli := 0;coli<cols-2;coli++{
            if judge(rowi, coli, grid){
                result++
            }
        }
    }
    return result
}

func judge(rowi int, coli int, grid [][]int)bool{
    visited := make([]bool, 9)
    sums := make([]int, 5)
    for startrow := rowi;startrow<rowi+3;startrow++{
        rowsum := 0
        for startcol := coli;startcol<coli+3;startcol++{
            rowsum += grid[startrow][startcol]
            sums[startcol-coli] += grid[startrow][startcol]
            if grid[startrow][startcol] >=1 && grid[startrow][startcol] <= 9{
                visited[grid[startrow][startcol]-1] = true
            }
        }
        if rowsum != 15{
            return false
        }
    }
    sums[3] = grid[rowi][coli]+grid[rowi+1][coli+1]+grid[rowi+2][coli+2]
    sums[4] = grid[rowi][coli+2]+grid[rowi+1][coli+1]+grid[rowi+2][coli]
    for _,sum := range sums{
        if sum != 15{
            return false
        }
    }
    for _,visit := range visited{
        if !visit{
            return false
        }
    }
    return true
}

day166 2024-08-10

959. Regions Cut By Slashes

An n x n grid is composed of 1 x 1 squares where each 1 x 1 square consists of a ‘/’, ‘', or blank space ’ ‘. These characters divide the square into contiguous regions.

Given the grid grid represented as a string array, return the number of regions.

Note that backslash characters are escaped, so a ‘' is represented as ‘\’.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/081082pd6GdnNDxA.png

题解

像这样将一些小的区域连接在一起构成一个大区域, 最终计数组合成的大区域有几个的问题常常使用并查集解决, 让每个大区域派出一个代表元, 最终统计代表元的个数即可知道大区域的个数. 问题在于, 如何合并, 合并什么. 我们考虑’/’,和’'这两种情况, 可以发现如果将方块分割成四个小三角形, 右边的三角形都要和右侧方块的左边三角形合并在一起, 下面的三角形要和下面方块的上三角形合并在一起. 那么我们可以将所有的方块都分割成四个小三角形, 再根据题目中提供的斜线情况来合并这些小三角形(相当于给斜线做减法).

代码

 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
func regionsBySlashes(grid []string) int {
	rmap := make(map[int]int, 0)
	ll := len(grid)
	s := NewSet(4*ll*ll)

	for i := 0; i < ll; i++ {

		for j := 0; j < ll; j++ {
			zero := i * 4 * ll + 4 * j
			switch grid[i][j] {
			case '\\':
				s.Union(zero+1, zero+2)
				s.Union(zero+0, zero+3)
			case '/':
				s.Union(zero+0, zero+1)
				s.Union(zero+2, zero+3)
			case ' ':
				s.Union(zero+0, zero+1)
				s.Union(zero+0, zero+2)
				s.Union(zero+0, zero+3)
			}

			if j + 1 != ll {
				s.Union(zero+2, zero+4)
			}
			if i + 1 != ll {
				s.Union(zero+3, zero+4*ll+1)
			}
		}
	}

	for i := 0; i < 4*ll*ll; i++ {
		rmap[s.Find(i)] = 0
	}

	return len(rmap)
}

type Set struct {
	s []int
}

func NewSet(n int) *Set {
	s := make([]int, n)
	for i := 0; i < len(s); i++ {
		s[i] = i
	}
	return &Set {
		s,
	}
}

func (s *Set)Union(a,b int) {
	if s.Find(a) == s.Find(b) {
		return
	} else {
		s.s[s.Find(b)] = s.s[s.Find(a)]
	}
}

func (s *Set) Find(x int) int {
	if s.s[x] == x {
		return x
	} else {
		return s.Find(s.s[x])
	}
}

day167 2024-08-11

1568. Minimum Number of Days to Disconnect Island

You are given an m x n binary grid grid where 1 represents land and 0 represents water. An island is a maximal 4-directionally (horizontal or vertical) connected group of 1’s.

The grid is said to be connected if we have exactly one island, otherwise is said disconnected.

In one day, we are allowed to change any single land cell (1) into a water cell (0).

Return the minimum number of days to disconnect the grid.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0811KjAjKyWx5Ah1.png

题解

本题是一道难题, 但整体思路还是比较清晰, 对于任意一个岛屿矩阵, 如果有岛屿之间不相连, 则直接返回0, 如果只有一个岛屿且存在一个点使得去掉这个点后这一个岛屿会被分割为两个岛屿, 则返回1, 如果不存在这样的点则直接返回2. 对于任意形状的岛屿, 将其中最多两个土地变为水就能使一个岛屿分割为两个(考虑111,111,111. 只需将任意一个角分割出来即可, 即分割后为101,110,111, 此时已经将原来的一个岛屿分割为两个). 判断是否只有一个岛屿是之前已经解决过的问题, 使用dfs即可解决. 但判断是否存在一个点使得去掉这个点后一个岛屿会被分割为两个则是一个难解决的问题, 直观上可能会想到使用并查集来解决, 这种方案是可行的, 只是时间复杂度会比较高, 因为要遍历每一个土地来判断删掉这个土地后并查集是否会变成两个. 这里要引入图论中的一些概念, 这样的点在无向图中被称为割点. 要求割点就要引入一个求强连通分量的算法Tarjan算法. 有趣的是, 这个算法的发明者也是并查集的作者Robert E. Tarjan. 该作者在图算法上有着极为杰出的贡献. 对此算法的讲解推荐下面的youtube视频

tarjan算法

代码

 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
func minDays(grid [][]int) int {

    m := len(grid)
    n := len(grid[0])
    cntOne := 0

    dirs := [][]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
    g := make(map[int][]int, m*n)
    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if grid[i][j] == 1 {
                cntOne++
                for _, d := range dirs {
                    nx, ny := i+d[0], j+d[1]
                    if nx < 0 || nx >= m || ny < 0 || ny >= n {
                        continue
                    }
                    if grid[nx][ny] == 1 {
                        g[i*n+j] = append(g[i*n+j], nx*n+ny)
                    }
                }
            }
        }
    }
    //特殊处理
    if cntOne == 1 {
        return 1
    }
    if len(g) == 0 {
        return 0
    }

    isCut := make([]bool, m*n)
    dfn := make([]int, m*n)
    low := make([]int, m*n)

    dfsClock := 0
    cnt := 0
    var tarjan func(int, int) int
    tarjan = func(v, fa int) int {
        dfsClock++
        dfn[v] = dfsClock
        low[v] = dfsClock
        childCnt := 0
        for _, w := range g[v] {
            if dfn[w] == 0 {
                childCnt++
                lowW := tarjan(w, v)
                low[v] = min(low[v], lowW)
                if lowW >= dfn[v] {
                    isCut[v] = true
                    cnt++
                }
            } else {
                low[v] = min(low[v], dfn[w])
            }
        }
        if fa == -1 && childCnt == 1 {
            if isCut[v] {
                cnt--
            }
            isCut[v] = false
        }
        return low[v]
    }

    cntR := 0
    for i := range grid {
        for j, v := range grid[i] {
            if v == 1 && dfn[i*n+j] == 0 {
                if cntR > 0 {
                    return 0
                }
                cntR++
                tarjan(i*n+j, -1)
            }
        }
    }
    if cnt > 0 {
        return 1
    }
    return 2
}

总结

图算法往往比较复杂, 在理解抽象的图算法基础上还需要我们将问题抽象出来, 对应到具体的图的问题中, 如是无向图还是有向图, 求解的是图中的什么特殊点或者特殊边. 这些都需要长期的积累.

洛谷–割点

day168 2024-08-12

703. Kth Largest Element in a Stream

Design a class to find the kth largest element in a stream. Note that it is the kth largest element in the sorted order, not the kth distinct element.

Implement KthLargest class:

KthLargest(int k, int[] nums) Initializes the object with the integer k and the stream of integers nums. int add(int val) Appends the integer val to the stream and returns the element representing the kth largest element in the stream.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0812bV1KZSBD8bVl.png

题解

题目很好理解, 即不断向数组中添加数字但始终返回当前数组中所有数中第K大的数字(可以重复,这里的第K大是排序后的第K个). 这里可以使用一个最小堆, 因为我们只需要第K大的数字, 其余数字并不关心, 因此只需保证最大堆中永远只有k个数字则此时堆顶数字即为当前stream中全部数字中第K大的数字, 能够使用这种方法的另一个原因在于这里的stream只有添加操作而没有删除操作. 如果有删除操作的话, 因为只保留了全部stream中到目前为止第K大及以下的数字, 如果删掉堆顶, 则无法确定之前被舍弃的数字中哪个应该重新成为堆顶(如1,2,3,4 若堆只保留三个数字, 则当到4时堆内为4,3,2三个数, 堆顶为2如果有删除操作将3删掉, 则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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import (
	"container/heap"
)

type Kheap []int

func (kheap Kheap) Len() int{
    return len(kheap)
}

func (kheap Kheap) Less(i ,j int) bool{
    return kheap[i] < kheap[j]
}

func (kheap Kheap) Swap(i,j int){
    kheap[i], kheap[j] = kheap[j], kheap[i]
}

func (kheap *Kheap) Push(num interface{}){
    *kheap = append(*kheap, num.(int))
}

func (kheap *Kheap) Pop()(num interface{}){
    n := len(*kheap)
    x := (*kheap)[n-1]
    *kheap = (*kheap)[0:n-1]
    return x
}

type KthLargest struct {
    k int
    nums Kheap
}


func Constructor(k int, nums []int) KthLargest {
    kheap := Kheap{}
    if len(nums) <= k{
        for _, num := range nums{
            kheap = append(kheap, num)
        }
        heap.Init(&kheap)
    }else{
        for i:=0;i<k;i++{
            kheap = append(kheap, nums[i])
        }
        heap.Init(&kheap)
        for i:=k;i<len(nums);i++{
            if nums[i] > kheap[0]{
                heap.Pop(&kheap)
                heap.Push(&kheap, nums[i])
            }
        }
    }
    return KthLargest{k, kheap}
}


func (this *KthLargest) Add(val int) int {
    if len(this.nums) < this.k{
        heap.Push(&(this.nums), val)
        return this.nums[0]
    }else{
        if val > this.nums[0]{
            heap.Pop(&(this.nums))
            heap.Push(&(this.nums), val)
            return this.nums[0]
        }
    }
    return this.nums[0]
}


/**
 * Your KthLargest object will be instantiated and called as such:
 * obj := Constructor(k, nums);
 * param_1 := obj.Add(val);
 */

day169 2024-08-13

40. Combination Sum II

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target.

Each number in candidates may only be used once in the combination.

Note: The solution set must not contain duplicate combinations.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0813KLpTnkmkLkAv.png

题解

这种题目第一反应是需要遍历所有的数字组合来找到符合条件的数字, 但显然可以根据各种条件进行剪枝, 那么总体思路就是在遍历所有组合的基础上进行剪枝操作. 首先想清楚遍历所有组合可以使用回溯算法. 其次是可以从哪些方面进行剪枝. 目标是找到所有和等于目标数字的数字组合, 则显然当当前数字和已经大于目标数字时, 就不需要再去和后面的数字进行组合了, 这是最基本的剪枝. 再次考虑题目要求, 要求找到所有的组合且组合不能重复, 那么这里我们可以对相同的数字进行剪枝, 因为如果有多个相同数字, 在数组已有数字相同的情况下(前缀一样)选取任意一个得到的后续的组合都是相同的. 举例来说, 对于目标和为3的情况, 如果我们有1,2,2,2四个数字, 则选取1和任意一个2对应的组合都是相同的. 这里的重要前提是已有数字相同, 即选择的层级在同一层, 仍然举例来说, 如果目标和为8, 且有1,2,2,2,3,5这几个数字. 则当已有数字为1时, 选取三个2中的任意一个2后, 再与2后面的数字组合, 得到的数组都是一样的, 但如果已有数字是1,2 则此时选取后面两个2中的任意一个最终得到的组合都是一样的. 可能有人会在此处困惑, 这里在1时从三个2中选一个得到的不也是1,2吗, 和在已有数字是1,2的情况有什么区别呢, 这里的关键在回溯的层级, 如果选择1位于第一层, 则接下来回溯需要我们先选择第一个2继续向下搜索, 结束后再选择第二个2继续搜索, 最后选择第三个2继续搜索, 此时这三个2都位于第二层. 而已有1,2的情况相当于已经选择了1和第一个2后的继续向下搜索的过程, 此时继续选择第二个2或者第三个2, 这两个2位于第三层. 我画了一个简单的示意图, 可以很好的理解这个问题(不同颜色表示不同的2, 将整个搜索树展开可以清楚的看到位于不同层级的2, 这些2在不同层级的作用不同). 由此可知, 对于这个问题, 位于同一层的相同数字起到的作用是一样的. 可以对其进行剪枝.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0813INopp8IMG_1272B111D280-1.jpeg

代码

 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
func combinationSum2(candidates []int, target int) [][]int {
    sort.Ints(candidates) // Sort the array to manage duplicates
    result := [][]int{}
    current := []int{}

    var backtrack func(target, start int)
    backtrack = func(target, start int) {
        if target == 0 { // Base case: if we hit the target, add the current combination to the result
            result = append(result, append([]int{}, current...))
            return
        }
        for i := start; i < len(candidates); i++ {
            if i > start && candidates[i] == candidates[i-1] {
                continue // 跳过同层级的相同数字
            }
            if candidates[i] > target {
                break // 单个数字已经大于目标直接退出
            }
            current = append(current, candidates[i])
            backtrack(target-candidates[i], i+1)
            current = current[:len(current)-1]
        }
    }

    backtrack(target, 0)
    return result
}

总结

看评论区有人提到这个问题与子集和问题相同, 子集和是个经典的np完全问题. 因此这个问题只能遍历所有组合找到所有符合的答案, 剪枝不过是局部优化, 在最坏情况下可能所有组合都满足条件, 此时相当于不存在剪枝.

day170 2024-08-14

719. Find K-th Smallest Pair Distance

The distance of a pair of integers a and b is defined as the absolute difference between a and b.

Given an integer array nums and an integer k, return the kth smallest distance among all the pairs nums[i] and nums[j] where 0 <= i < j < nums.length.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0814nDv1GexjGquG.png

题解

本题是一道难题, 因为要找的是数对差中第k小的数对, 因此重要的是数字与数字的差, 暴力方法即为将所有的数对差都求出来后排序即可直接得到第k位的差值. 但显然可以对此进行剪枝, 这时可以想到如果数组有序的话, 当数字a和数字b的差已经大于目标值时, 就没必要继续计算a和大于b的数字的差值. 问题在于最终要求的就是这里的目标值, 在没有目标值的情况下, 如何确定目标值, 这个问题就变成了一个查找问题. 在数组排序后, 我们可以知道整个数组中两数差的最大值(数组第一个数字和最后一个数字的差), 有了最大值和最小值(0), 就可以使用二分法来假设目标值, 假设好目标值后计算小于等于目标值的差值个数, 如果大于等于k个则更新右边界, 否则更新左边界. 这里要注意为什么等于k的时候还要继续更新右边界, 因为通过二分法假设目标值可能会出现目标值并不存在于数组的差值中但仍然满足第k个这个条件, 如差值为1,2,3,5. 而假设当前的假设目标值为4, 则小于4和小于5的差值都为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
func smallestDistancePair(nums []int, k int) int {
    var enough = func(x int) bool {
        var i, j, count int
        j++
        for i < len(nums) {
            for j < len(nums) && nums[j] - nums[i] <= x {
                j++
            }
            count += j - i - 1
            i++
        }
        return count >= k
    }

    sort.Ints(nums)
    var lo, hi int
    hi = nums[len(nums) - 1] - nums[0]

    for lo < hi {
        cur := lo + (hi - lo) / 2
        if !enough(cur) {
            lo = cur + 1
        } else {
            hi = cur
        }

    }
    return lo
}

总结

像二分法这种为人熟知的方法一定要深刻理解其背后的思想和目标场景, 才能在复杂的背景下准确应用.

day171 2024-08-15

860. Lemonade Change

At a lemonade stand, each lemonade costs $5. Customers are standing in a queue to buy from you and order one at a time (in the order specified by bills). Each customer will only buy one lemonade and pay with either a $5, $10, or $20 bill. You must provide the correct change to each customer so that the net transaction is that the customer pays $5.

Note that you do not have any change in hand at first.

Given an integer array bills where bills[i] is the bill the ith customer pays, return true if you can provide every customer with the correct change, or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0815blTU4df5vcg6.png

题解

本题是简单题, 比较直接的做法可以通过贪心来解决该题, 每次找钱时都先尝试找较大的金额, 再找较小的金额. 但实际上因为题目中只有三种额度的钱, 则可以直接枚举所有情况(一共三种), 即对10直接找一张5, 对20找一张10一张5或者三张5. 对20要先枚举找一张10的情况, 因为找10一定要和5搭配, 而找三张5则不受10的影响. 这种直接枚举速度比较快.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func lemonadeChange(bills []int) bool {
    money := make([]int, 2)
    for _,bill := range bills{
        if bill == 5{
            money[0]++
        }else if bill == 10{
            money[1]++
            money[0]--
        }else{
            if money[1] > 0{
                money[1]--
                money[0]--
            }else{
                money[0] -= 3
            }
        }
        if money[0] < 0 || money[1] < 0{
            return false
        }
    }
    return true
}

day172 2024-08-16

624. Maximum Distance in Arrays

You are given m arrays, where each array is sorted in ascending order.

You can pick up two integers from two different arrays (each array picks one) and calculate the distance. We define the distance between two integers a and b to be their absolute difference |a - b|.

Return the maximum distance.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0816Eud0miJaDw23.png

题解

考虑到本题中各个数组已经从小到大排好序, 则直接使用每个数组的首位数字(最小数字)分别与其余数组的末位数字(最大数字)做差并取绝对值, 这些值中的最大值即为所求的值. 来考虑只有两个数组的情况, 如果数组1的最小值为a,最大值为b, 数组2的最小值为c, 最大值为d. 若a<b<c<d则最大差值为d-a, 若a<c<b<d, 则最大差值为d-a, 若a<c<d<b, 则最大差值为|d-a|和|b-c|二者中的较大值, 则无论哪种情况, 只要求出数组的最小值和其他数组最大值的差最终均能得到正确答案. 但这种解法需要将每个数组都与其他数组比较一遍, 此时可以想到因为数组都是排好序的, 则设定一个全局最小值, 在遍历各个数组的过程中用各个数组的最小值来更新全局最小值, 同理也可以得到一个全局最大值, 要先将全局最小值与当前数组的最大值和当前数组最小值与全局最大值做差后更新全局最大差值, 再用当前数组的值更新全局最值, 因为题目中要求必须从不同的数组中取两个数字, 则这样可以避免全局最小和全局最大值位于同一个数组当中. 如[[1,4],[0,5]]如果在取得全局最小值和全局最大值后再做差, 则因为0和5在同一个数组中, 并不满足题目条件. 而若先求出差再更新最小值和最大值, 则会得到正确答案.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func maxDistance(arrays [][]int) int {
    smallest := arrays[0][0]
    biggest := arrays[0][len(arrays[0])-1]
    result := 0
    for _, array := range arrays[1:]{
        n := len(array)
        result = max(abs(biggest-array[0]), result, abs(array[n-1]-smallest))
        smallest = min(array[0], smallest)
        biggest = max(array[n-1], biggest)
    }
    return result
}

func abs(input int)int{
    if input < 0{
        return -input
    }
    return input
}

day173 2024-08-17

1937. Maximum Number of Points with Cost

You are given an m x n integer matrix points (0-indexed). Starting with 0 points, you want to maximize the number of points you can get from the matrix.

To gain points, you must pick one cell in each row. Picking the cell at coordinates (r, c) will add points[r][c] to your score.

However, you will lose points if you pick a cell too far from the cell that you picked in the previous row. For every two adjacent rows r and r + 1 (where 0 <= r < m - 1), picking cells at coordinates (r, c1) and (r + 1, c2) will subtract abs(c1 - c2) from your score.

Return the maximum number of points you can achieve.

abs(x) is defined as:

x for x >= 0. -x for x < 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0817XUKrbszCuxge.png

题解

本题每一行中各个位置能得到的最大点数仅与上一行有关, 因此只要解决两行矩阵第二行如何根据第一行求得每个位置能得到的最大点数这一子问题即可得解. 最直接的想法当然是暴力, 对第二行每个数字都遍历第一行的全部数字, 每个数字都通过点数-距离得到其对于第二行该数字的价值. 最后取价值最大的数加在该数字上, 这种方式如果每行数字个数为n个, 需要n^2的复杂度. 则若共有m行, 整体需要m*n^2的复杂度. 因此我们需要思考一些方式使得每行只需遍历常数次即能让下一行每个位置找到上一行中的最大值. 这个问题的思路在某些地方与"接雨水"这到经典题有相似之处. 虽然我们在从左向右遍历的时候对于任意位置不能得到全局最大值, 但在遍历的过程中其实已经获得了当前下标左侧的全部值的信息, 因此我们可以记录到某一下标的左侧数组中的全部数字到这一下标的最大价值, 只需定义一个变量leftmax, 并在遍历过程中不断将leftmax-1和当前下标的数字比较并更新leftmax. 举例来说, 如1,3,2,6,4这几个数字, 遍历1时leftmax为1, 遍历3时将leftmax-1(1-1)和3比较得到新的leftmax为3, 遍历2时将leftmax-1(3-1)和2比较得到新的leftmax为2. 将每个位置的leftmax都保存下来, 这样相当于将求每个位置的最大价值这一问题拆为两个子问题, 即求解每个位置左侧的最大价值和每个位置右侧的最大价值. 最后再将这两个问题的解比较即得最终解. 这样的好处在于这两个子问题都是可以"并行求解"的, 即对行r遍历一遍行r-1即可求出所有位置的一个子问题的解, 求得两个子问题的解也只需两次. 而原问题则需要每个位置都遍历一遍行r-1, 在遍历的过程中虽然访问了行r-1的全部数字, 但大部分信息都被抛弃了, 在下一个位置又要重新获取这些信息. 这是非常低效的, 在之前题解中多次强调, 能越充分的利用信息就有越高的求解效率.

因此本题求解过程为对行r, 遍历两次行r-1获得全部位置的leftmax和rightmax, 最后遍历一遍行r并更新每个位置的最大价值即可.

代码

 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
func maxPoints(points [][]int) int64 {
    rows := len(points)
    if rows == 1{
        return int64(maxSlice(points[0]))
    }
    cols := len(points[0])
    leftmax := make([]int, cols)
    rightmax := make([]int, cols)
    for row:=1;row<rows;row++{
        leftmaxnow := points[row-1][0]
        for index,num := range points[row-1]{
            leftmax[index] = max(leftmaxnow-1, num)
            leftmaxnow = leftmax[index]
        }
        rightmaxnow := points[row-1][cols-1]
        for index:=cols-1;index>=0;index--{
            rightmax[index] = max(rightmaxnow-1, points[row-1][index])
            rightmaxnow = rightmax[index]
        }
        for col:=0;col<cols;col++{
            points[row][col] += max(leftmax[col],rightmax[col])
        }

    }
    return int64(maxSlice(points[rows-1]))
}

func maxSlice(numbers []int) int {
    maxNum := numbers[0]
    for _, num := range numbers[1:] {
        if num > maxNum {
            maxNum = num
        }
    }
    return maxNum
}

day173 2024-08-18

264. Ugly Number II

An ugly number is a positive integer whose prime factors are limited to 2, 3, and 5.

Given an integer n, return the nth ugly number.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0818t4Rgq13Am2NL.png

题解

本题通过暴力遍历每个数并判断是不是丑数显然比较低效, 最好的办法是直接生成丑数, 问题在于如何生成丑数呢, 回想类似的题目可以直接通过之前的具有相似性质的数字生成后面的数字, 那么丑数也可能具有这种特点, 可以通过数学归纳法证明. a) 设第n+1个丑数为U。根据丑数的定义,U可以表示为:

U = 2^a _ 3^b _ 5^c,其中a、b、c都是非负整数。

b) 我们可以将U表示为以下三种形式之一:

U = 2 _ (2^(a-1) _ 3^b _ 5^c) 当a > 0时 U = 3 _ (2^a _ 3^(b-1) _ 5^c) 当b > 0时 U = 5 _ (2^a _ 3^b _ 5^(c-1)) 当c > 0时 c) 注意到括号中的表达式(2^(a-1) _ 3^b _ 5^c)、(2^a _ 3^(b-1) _ 5^c)和(2^a _ 3^b * 5^(c-1))都是更小的丑数,因为它们的指数和比U小。

d) 根据归纳假设,这些更小的丑数已经在我们生成的n个丑数中。

e) 因此,U必定可以通过将某个已知的丑数乘以2、3或5得到。

则由此, 生成丑数可以通过之前的丑数分别乘以2,3,5得到, 为了保证得到的丑数是有序的, 则可以用三个指针分别表示2,3,5三个数当前已经乘过的丑数是哪个. 每次取三个指针位置的数乘以对应的被乘数中最小的.如初始丑数都为1, 设指针分别为i2,i3,i5, 则先将2乘1得到2, 同时指针i2指向1表示下标为0的数字(1)已经乘过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
func nthUglyNumber(n int) int {
    ugly := make([]int, n)
    ugly[0] = 1

    i2, i3, i5 := 0, 0, 0

    for i := 1; i < n; i++ {
        next2 := ugly[i2] * 2
        next3 := ugly[i3] * 3
        next5 := ugly[i5] * 5

        nextUgly := min(next2, min(next3, next5))
        ugly[i] = nextUgly

        if nextUgly == next2 {
            i2++
        }
        if nextUgly == next3 {
            i3++
        }
        if nextUgly == next5 {
            i5++
        }
    }

    return ugly[n-1]
}

day174 2024-08-19

650. 2 Keys Keyboard

There is only one character ‘A’ on the screen of a notepad. You can perform one of two operations on this notepad for each step:

Copy All: You can copy all the characters present on the screen (a partial copy is not allowed). Paste: You can paste the characters which are copied last time. Given an integer n, return the minimum number of operations to get the character ‘A’ exactly n times on the screen.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0819quqWn30l1biR.png

题解

本题题目有点幽默, 一个经典的程序员梗, 程序员键盘上只需要两个键就够了, 一个复制, 一个粘贴. 本题正是一个这样的键盘.

回归题目本身, 第一步可以发现如果n能被一个正整数a整除, 则可以通过将n/a个数的’A’粘贴a-1次得到n个’A’, 加上复制操作总共需要的操作次数为a次. 则可以发现只要将n分解成几个因子相乘, 将这几个因子相加就是一个得到n的操作总数. 接下来的问题就是如何分解能让这个操作总数最小, 这里很容易想到将n分解为质因数, 因为质因数是比较特殊的因数, 这是我们的猜测, 可以通过简单的证明说明分解成质因数一定不大于分解成合数因数的操作次数, 只需证明如果a,b均大于1则a+b(质因数的操作总数)<=a*b(合数因数的操作总数).

首先,我们可以考虑 ab - (a+b),如果能证明这个差值非负,那么就证明了 ab ≥ a+b。

a*b - (a+b) = ab - a - b = ab - a - b + 1 - 1 = (a-1)(b-1) - 1

因为a和b都是大于1的正整数,所以 a-1 ≥ 1 且 b-1 ≥ 1

因此,(a-1)(b-1) ≥ 1

所以,(a-1)(b-1) - 1 ≥ 0

这就证明了 ab - (a+b) ≥ 0,即 ab ≥ a+b

由此我们得到只要将n分解成质因数相乘再将这些因子相加就能得到最少的操作总数. 如何将n分解成质因数相乘呢, 只需从2开始不断用n试除质因子, 当无法整除时将质因子加一(不用担心可以被合数整除, 如如果一个数能被6整除, 则从2递增到3时一定已经将这个数除尽了), 直到因子大于等于n的平方根即可. 每次分解出一个质因子都将其加和到操作总数上即得最终的答案.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func minSteps(n int) int {
    if n == 1{
        return 0
    }
    d := 2
    result := 0
    for n > 1 {
		for n%d == 0 {
			result += d
			n /= d
		}
		d++
		if d*d > n {
			if n > 1 {
				result += n
			}
			break
		}
	}
    return result

}

day 175 2024-08-20

1140. Stone Game II

Alice and Bob continue their games with piles of stones. There are a number of piles arranged in a row, and each pile has a positive integer number of stones piles[i]. The objective of the game is to end with the most stones.

Alice and Bob take turns, with Alice starting first. Initially, M = 1.

On each player’s turn, that player can take all the stones in the first X remaining piles, where 1 <= X <= 2M. Then, we set M = max(M, X).

The game continues until all the stones have been taken.

Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0820rbyn5xa0NtDv.png

题解

本题思考对于Alice取石子的场景, 如何知道Alice最终最多能取到多少石子, 对于最简单的场景, 如当前Alice只能取一堆或者两堆石子, 我们只需知道当Alice取一堆石子后后续最多能取到多少石子, 并与当前一堆石子加和, 即可知道如果选择只取一堆石子Allice最终最多能得到多少石子, 取两堆石子同理. 则问题变为当Alice已经取完一次石子后, 后续Alice最多能取到多少石子, 对于Alice来说, 目标为取得最多的石子, 而对于Bob来说, 目标则是让Alice取到最少的石子. 则可以设定一个flag表示当前是Alice的回合还是Bob的回合, 如果是Alice的回合, 则尝试所有可行的取石子方法并加上取石子后剩余的石子堆中Alice能取到的最大石子数再取一个最大值. 如果是Bob的回合, 则也尝试所有可行的取石子方法并返回Alice能取石子的最小值. 如石子总共有3堆, Alice先取可以取一堆或者两堆, 则Alice取一堆加上剩余两堆Alice还能取得的最大值. 剩余两堆中Bob同样可以取一堆或者取两堆, 如果Bob选择取一堆则Alice还能取到第三堆, 如果Bob选择取两堆, 则Alice只能取到0, 对Bob来说, 当然是最小化Alice能取的石子个数即让Alice取0, 此时剩余两堆中Alice能取得的最大值就是0. 用状态来表示可以表示为取0加上A(2,1,0). 其中2表示下一个人从第2堆开始取, 1表示上一个人取完后M的值, 即下一个人可以取的堆上限的1/2, 0表示下一个取的人该是Bob, 1表示该是Alice. 将状态记忆化, 遇上重复的状态不需要再次递归计算.

这类问题的关键是把大问题抽象成一个统一的结构, 分解成小问题解决, 在本题中即为当Alice可取的堆数从1到2M时, 只需考虑用Alice取的堆数加上剩余的堆中Alice能取得的最大值即可. 至于剩余的堆中Alice能取得最大值怎么计算, 因为此问题的结构和前一个问题完全相同, 则我们一定可以通过不断递归将问题规模不断变小最终到达一个终止状态得到解决, 因此不用过于考虑计算细节, 只需确定好终止状态, 其余部分通过结构相同问题递归处理就能得到最终答案.

代码

 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
func stoneGameII(piles []int) int {
    memo := make(map[string]int)
    length :=  len(piles)
    var dfs func(i, M int, flag bool) int
    dfs = func(i, M int, flag bool) int{
        if i == length {
            return 0
        }

        key := fmt.Sprintf("%d,%d,%v", i, M, flag)
        if val, exists := memo[key]; exists {
            return val
        }

        cnt := 0
        var mx int
        if flag {
          mx = 0
         } else {
          mx = math.MaxInt32
         }

        for j := i; j < min(length, i+2*M); j++ {
            cnt += piles[j]
            val := dfs(j+1, max(M, j-i+1), !flag)
            if flag {
                val += cnt
                mx = max(mx, val)
            } else {
                mx = min(mx, val)
            }
    }
        memo[key] = mx
        return mx
    }
    return dfs(0, 1, true)
}

day 176 2024-08-21

664. Strange Printer

There is a strange printer with the following two special properties:

The printer can only print a sequence of the same character each time. At each turn, the printer can print new characters starting from and ending at any place and will cover the original existing characters. Given a string s, return the minimum number of turns the printer needed to print it.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/08213P3Mjb62YhEb.png

题解

本题是一道难题, 难在如何把问题转换为相同结构的子问题, 本题打印字符串过程的区间是非常重要的, 根据题目可以发现, 如果字符串两端是相同的, 如abba, 则在打印一串a的时候可以同时打印出两端的两个a. 则abba需要打印的次数和abb需要打印的次数相同, abb打印的次数由abb的两个子问题决定, 分别是ab和b,a和bb两个问题的打印次数的最小值决定. 由此可知, 本题可将一个区间内打印次数的问题转换为区间内子区间的打印次数的问题, 区间两端值相同时直接等于子区间的次数, 不同则需遍历所有可能取最小值. 由此不断减小问题规模直到区间长度为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

func strangePrinter(s string) (result int) {
    newstr := string(s[0])

    for i:= 1 ;i < len(s); i++ {
        if s[i] != s[i-1] {
            newstr += string(s[i])
        }
    }

    m := len(newstr)

    dp := make([][]int,m)
    for i:=range dp{
        dp[i] = make([]int,m)
    }

    for i:=0;i<m;i++{
        for j:=i;j>=0;j--{
            if i == j {
                dp[i][j] = 1
                continue
            }else{
                dp[j][i] = math.MaxInt32
            }
            if newstr[i] == newstr[j] {
                dp[j][i] = dp[j][i-1]
            }else{
                for k:=j;k<i;k++{
                    dp[j][i] = min(dp[j][i],dp[j][k]+dp[k+1][i])
                }
            }
        }
    }

    result = dp[0][m-1]

    return
}

总结

一般长度为100这样的问题很可能最终会是n^3的复杂度, 因此只要能得到解题思路就不用过于考虑效率的问题, 注意递归过程通过记忆化优化即可.

day177 2024-08-22

476. Number Complement

The complement of an integer is the integer you get when you flip all the 0’s to 1’s and all the 1’s to 0’s in its binary representation.

For example, The integer 5 is “101” in binary and its complement is “010” which is the integer 2. Given an integer num, return its complement.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0822XymK4w573pW1.png

题解

本题求数字的类似于"补码"的数字, 题目本身比较简单, 只需不断将数字模2后转换并右移, 将转换后的数字不断左移并加上转换得到的各个位, 最终即可得到最后被转换的数字.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func findComplement(num int) int {
    result := 0
    bit := 0
    count := 0
    for num > 0{
        bit = num % 2
        num = num >> 1
        bit = bit ^ 1
        result += bit << count
        count++
    }
    return result
}

day178 2024-08-23

592. Fraction Addition and Subtraction

Given a string expression representing an expression of fraction addition and subtraction, return the calculation result in string format.

The final result should be an irreducible fraction. If your final result is an integer, change it to the format of a fraction that has a denominator 1. So in this case, 2 should be converted to 2/1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0823PZUrYHXGbUme.png

题解

本题关键在于寻找公倍数和最大公约数, 因为题目中分母的大小都在1到10之间, 所以只需找到一个公倍数(不要求最小)将两个分数的分母统一起来方便做加减法. 而需要最大公约数则是因为运算得到的分数要化成最简式. 找最大公约数有一个经典的欧几里得算法(辗转相除法). 题目思路为遇到运算符即处理运算符随后处理运算符后的分数. 首个分数即时是负的也可以通过0减去该分数的方式与后面的处理统一起来. 为了简化计算直接将所有分母都转换成1-10的公倍数2520. 这样只处理分子即可.

代码

 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
func fractionAddition(expression string) string {
    if expression[0] != '-' {
        expression = "+" + expression
    }
    sign := 1
    i, n := 0, len(expression)
    nume := 0
    nume1, deno1 := 0, 0
    LCM := 2520

    for i < n {
        if expression[i] == '+' {
            sign = 1
        } else {
            sign = -1
        }
        i++

        // 处理分子
        nume1 = int(expression[i] - '0')
        i++
        if i < n && expression[i] != '/' {
            nume1 = nume1*10 + int(expression[i]-'0')
            i++
        }

        // 处理分母
        i++ // 跳过 '/'
        deno1 = int(expression[i] - '0')
        i++
        if i < n && expression[i] != '+' && expression[i] != '-' {
            deno1 = deno1*10 + int(expression[i]-'0')
            i++
        }

        nume1 = LCM / deno1 * sign * nume1
        nume += nume1
    }

    var result strings.Builder
    if nume == 0 {
        return "0/1"
    } else if nume < 0 {
        nume = -nume
        result.WriteString("-")
    }
    GCD := getGCD(nume, LCM)
    result.WriteString(strconv.Itoa(nume / GCD))
    result.WriteString("/")
    result.WriteString(strconv.Itoa(LCM / GCD))
    return result.String()
}

// 求最大公约数(欧几里得算法)
func getGCD(a, b int) int {
    for b != 0 {
        a, b = b, a%b
    }
    return a
}

day179 2024-08-24

564. Find the Closest Palindrome

Given a string n representing an integer, return the closest integer (not including itself), which is a palindrome. If there is a tie, return the smaller one.

The closest is defined as the absolute difference minimized between two integers.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0824i7QPcWT0L30M.png

题解

本题考虑能得到离一个数最近的回文数的所有情况, 假设数字根据长度中间位置有一个"对称轴". 则在对称轴两侧的数字完全按逆序排列即得到回文数, 从原始数字得到回文数可以直接复制左半边的数字并逆序, 可以将左半边数字加一并逆序, 也可以将左半边数字减一并逆序. 只需要比较这几种情况得到的回文数哪个和原数字的差最小即可. 但除此以外还要考虑如999这样的数字, 距离其最近的回文数为1001, 同样对于1000来说, 其对应的回文数为999, 因此需要将这两种情况也考虑在内. 因此对于任意数字, 列举这五种情况下得到的回文数并选择与原数字差最小的即可. 注意对于本身已经是回文数的不能选择自身, 即差必须大于0.

代码

 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
func nearestPalindromic(n string) string {
    num, _ := strconv.Atoi(n)

    length := len(n)
    candidates := []int{}

    // 情况1:999 -> 1001
    candidates = append(candidates, int(math.Pow10(length)) + 1)

    // 情况2:1000 -> 999
    candidates = append(candidates, int(math.Pow10(length-1)) - 1)

    // 获取左半部分
    leftHalf := n[:(length+1)/2]
    leftNum, _ := strconv.Atoi(leftHalf)

    // 情况3:直接镜像
    candidates = append(candidates, createPalindrome(leftNum, length%2 == 0))

    // 情况4:左半部分+1
    candidates = append(candidates, createPalindrome(leftNum+1, length%2 == 0))

    // 情况5:左半部分-1
    candidates = append(candidates, createPalindrome(leftNum-1, length%2 == 0))

    closest := candidates[0]
    minDiff := abs(num - closest)


    for _, candidate := range candidates[1:] {
        diff := abs(num - candidate)
        if (diff < minDiff && diff > 0) || (diff == minDiff && candidate < closest) {
            closest = candidate
            minDiff = diff
        }
    }

    return strconv.Itoa(closest)
}

func createPalindrome(num int, even bool) int {
    palindrome := num
    if !even {
        num /= 10
    }
    for num > 0 {
        palindrome = palindrome*10 + num%10
        num /= 10
    }
    return palindrome
}

func abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

day180 2024-08-25

145. Binary Tree Postorder Traversal

Given the root of a binary tree, return the postorder traversal of its nodes’ values.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0825UT0EkQ59RcnE.png

题解

本题为基础题, 考查最简单的二叉树遍历方法, 使用递归遍历二叉树, 先遍历左子节点, 再遍历右子节点, 最后将根节点的值加入到结果数组当中.

代码

 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.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func postorderTraversal(root *TreeNode) []int {
    if root == nil{
        return []int{}
    }
    result := []int{}
    var traversal func(*TreeNode)
    traversal = func(fa *TreeNode){
        if fa.Left != nil{
            traversal(fa.Left)
        }
        if fa.Right != nil{
            traversal(fa.Right)
        }
        result = append(result, fa.Val)
    }
    traversal(root)
    return result
}

day181 2024-08-26

590. N-ary Tree Postorder Traversal

Given the root of an n-ary tree, return the postorder traversal of its nodes’ values.

Nary-Tree input serialization is represented in their level order traversal. Each group of children is separated by the null value (See examples)

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0826V88pNkYMVEH1.png

题解

本题和昨天的二叉树遍历思路上一致, 只需要将二叉树遍历的左右子节点改为遍历父节点的孩子节点数组即可.

代码

 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
/**
 * Definition for a Node.
 * type Node struct {
 *     Val int
 *     Children []*Node
 * }
 */

func postorder(root *Node) []int {
    if root == nil{
        return []int{}
    }
    result := []int{}
    var traversal func(*Node)
    traversal = func(fa *Node){
        if len(fa.Children) > 0{
            for _, child := range fa.Children{
                traversal(child)
            }
        }
        result = append(result, fa.Val)
    }
    traversal(root)
    return result
}

day182 2024-08-27

1514. Path with Maximum Probability

You are given an undirected weighted graph of n nodes (0-indexed), represented by an edge list where edges[i] = [a, b] is an undirected edge connecting the nodes a and b with a probability of success of traversing that edge succProb[i].

Given two nodes start and end, find the path with the maximum probability of success to go from start to end and return its success probability.

If there is no path from start to end, return 0. Your answer will be accepted if it differs from the correct answer by at most 1e-5.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0827fiB1rHFThy9y.png

题解

本题寻找从一个点到另外一个点的最大可能性, 其实与寻找从一个点到另外一个点的最小距离异曲同工, 而这个图是一个无向图且边的权重均为正, 这能让我们想到dijistra算法. 只是需要对dijistra算法进行一些改动, 将计算两点之间的最短距离改为计算两点之间路径的总的可能性, 将每次从队列中取出距离原点距离最近的点改为取出距离原点可能性最大的点, 其余部分不变.

实现dijistra算法在寻找当前队列中到原点距离最短的节点时,如果使用数组则需遍历整个数组, 但使用优先级队列则可以直接得到有最短距离的节点(本题中为最大可能性)

代码

 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
type Edge struct {
  to         int
  probability float64
}

type Graph [][]Edge

func buildGraph(n int, edges [][]int, succProb []float64) Graph {
  graph := make(Graph, n)
  for i, edge := range edges {
    from, to := edge[0], edge[1]
    prob := succProb[i]
    graph[from] = append(graph[from], Edge{to: to, probability: prob})
    graph[to] = append(graph[to], Edge{to: from, probability: prob})
  }
  return graph
}

type Item struct {
  node       int
  probability float64
  index      int
}

type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].probability > pq[j].probability }
func (pq PriorityQueue) Swap(i, j int) {
  pq[i], pq[j] = pq[j], pq[i]
  pq[i].index = i
  pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
  n := len(*pq)
  item := x.(*Item)
  item.index = n
  *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
  old := *pq
  n := len(old)
  item := old[n-1]
  old[n-1] = nil
  item.index = -1
  *pq = old[0 : n-1]
  return item
}

func maxProbability(n int, edges [][]int, succProb []float64, start_node int, end_node int) float64 {
  graph := buildGraph(n, edges, succProb)

  probs := make([]float64, n)
  probs[start_node] = 1.0

  pq := make(PriorityQueue, 0)
  heap.Init(&pq)
  heap.Push(&pq, &Item{node: start_node, probability: 1.0})

  for pq.Len() > 0 {
    item := heap.Pop(&pq).(*Item)
    node := item.node
    prob := item.probability

    if node == end_node {
      return prob
    }

    if prob < probs[node] {
      continue
    }

    for _, edge := range graph[node] {
      newProb := prob * edge.probability
      if newProb > probs[edge.to] {
        probs[edge.to] = newProb
        heap.Push(&pq, &Item{node: edge.to, probability: newProb})
      }
    }
  }

  return 0.0
}

day183 2024-08-28

1905. Count Sub Islands

You are given two m x n binary matrices grid1 and grid2 containing only 0’s (representing water) and 1’s (representing land). An island is a group of 1’s connected 4-directionally (horizontal or vertical). Any cells outside of the grid are considered water cells.

An island in grid2 is considered a sub-island if there is an island in grid1 that contains all the cells that make up this island in grid2.

Return the number of islands in grid2 that are considered sub-islands.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0828Lc8ouMrz7EDc.png

题解

本题在数grid2岛屿个数的基础上增加判断岛屿中所有陆地块是否都包含在grid1中即可, 如果有任意的陆地块不包含在grid1的陆地中则这个岛屿不算sub island. 判断岛屿中的陆地个数则已经之前多次处理过, 使用dfs即可, 每次都向四个方向进行dfs, dfs过程中判断当前陆地块是否包含在grid1的陆地块中, 不包含则直接返回, 包含继续dfs直到四周均无法继续dfs为止.

需要注意的是, 在dfs的过程中不能因为grid2中的岛屿中某块陆地已经不在grid1中就直接返回, 因为需要将整块岛屿都遍历完, 如果不将整块岛屿都遍历完并置0的话会出现一块岛屿被分割遍历多次的情况. 在返回的过程中, 如果用true和false表示遍历的岛屿中的陆地在不在grid1中, 则需要设置一个变量并和各个方向dfs的返回结果做并, 而不是直接返回四个方向dfs的并(如dfs1&&dfs2&&dfs3&&dfs4), 因为并运算的短路效应, 如果前面的dfs有为false的情况则后面的dfs不会执行, 而我们需要四个dfs都被执行. 故需要设置单独的变量来保存四个dfs的结果.

代码

 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
func countSubIslands(grid1 [][]int, grid2 [][]int) int {
    rows := len(grid2)
    cols := len(grid2[0])
    var dfs func([][]int, [][]int,int,int)bool
    dfs = func(g1 [][]int, g2[][]int, row int, col int)bool{
        if row < 0 || row > rows-1 || col < 0 || col > cols-1 || g2[row][col]==0{
            return true
        }

        cond := true
        if g1[row][col] == 0{
            cond = false
        }
        g2[row][col] = 0
        cond = dfs(g1, g2, row-1, col) && cond
        cond = dfs(g1, g2, row+1, col) && cond
        cond = dfs(g1, g2, row, col-1) && cond
        cond = dfs(g1, g2, row, col+1) && cond
        return cond
    }

    results := 0
    for i, rowcontent := range grid2{
        for j, _ := range rowcontent{
            if grid2[i][j] == 1{
                if dfs(grid1, grid2, i, j){
                    results++
                }
            }
        }
    }
    return results
}

day184 2024-08-29

947. Most Stones Removed with Same Row or Column

On a 2D plane, we place n stones at some integer coordinate points. Each coordinate point may have at most one stone.

A stone can be removed if it shares either the same row or the same column as another stone that has not been removed.

Given an array stones of length n where stones[i] = [xi, yi] represents the location of the ith stone, return the largest possible number of stones that can be removed.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0829acaVmeQ5MUz7.png

题解

本题通过观察可以发现只要彼此之间有关联(共行或共列)的一系列坐标, 最后仅保留一个, 其余均可通过某种顺序被全部删除. 如(0,0), (0,2), (2,0), (2,2), 四个点之前每个都和另外一个有关联, 则最终四个点仅保留一个. 也就意味着, 如果a与b有关联, 则a b可视为在同一个集合中, 若c和a有关联, 则c b a可视为在同一个集合中, 以此类推, 直到没有新的数字加入这个集合, 这里可以使用并查集, 最后只需用全部坐标点的个数减掉集合的个数即得到可以被删除点的最多个数.

问题在于, 如何构造并查集, 由题目可知, 处在同一行或者同一列的点均可以视为在同一个集合中, 则可以将同一行的点进行union合并,同一列的也合并, 但点是二维的, 而行或列单独是一维的, 将点按照行列合并并不方便, 因此需要想办法将维度降低, 考虑行列的个数最多均为10^4, 则最简单的方法就是将列数加上10^4+1, 从而将其和行数放在同一个轴上, 这样就将每个点的二维行列转换成了一维的行来表示, 列放在全部行的后面来表示. 则基于此对每个点的列转化和和行进行合并操作即可构造需要的并查集, 最后用点的个数减去并查集的集合个数即得需要删除的点数.

这里很有意思的一个思路在于维度压缩, 因为题目中点的坐标有范围, 则x,y两个轴不会被完全使用, 仅仅使用了x,y两个轴上的一个片段, 则可以将y轴的这个片段投射到x轴的某个原本片段之外的区域上, 这样就将原本在两个轴上的问题转换成了一个轴上的问题, 方便解决.

代码

 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
func removeStones(stones [][]int) int {
	fa := make([]int,20005)
	for i := range fa{
		fa[i] = i
	}

	var find func(int) int
	find = func(i int) int {
		if i!=fa[i]{
			fa[i] = find(fa[i])
		}
		return fa[i]
	}

	for _,stone := range stones{
		u := find(stone[0])
		v := find(stone[1]+10001)
		fa[u] = v
	}

	mp := make(map[int]int)

	for _,stone := range stones{
		mp[find(stone[0])]++
	}
	return len(stones)-len(mp)
}

day185 2024-08-30

2699. Modify Graph Edge Weights

You are given an undirected weighted connected graph containing n nodes labeled from 0 to n - 1, and an integer array edges where edges[i] = [ai, bi, wi] indicates that there is an edge between nodes ai and bi with weight wi.

Some edges have a weight of -1 (wi = -1), while others have a positive weight (wi > 0).

Your task is to modify all edges with a weight of -1 by assigning them positive integer values in the range [1, 2 * 109] so that the shortest distance between the nodes source and destination becomes equal to an integer target. If there are multiple modifications that make the shortest distance between source and destination equal to target, any of them will be considered correct.

Return an array containing all edges (even unmodified ones) in any order if it is possible to make the shortest distance from source to destination equal to target, or an empty array if it’s impossible.

Note: You are not allowed to modify the weights of edges with initial positive weights.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/08302Qfrs1K0b16t.png

题解

本题首先注意到目标是要求最短路, 而本题中边权重除了-1外均为正值, 而负值也只有-1一个. 则可以先忽略所有边权重为-1的边, 对其他边可以使用dijistra算法(使用优先级队列优化算法效率). 得到的结果有四种情况, 比target大, 比target小, 和target相等, 以及从源点到目标点之间没有可行路径. 若和target相等, 则将其余-1边均置为一个任意大的数, 不影响最终结果即可. 考虑到target最大为10^9, 可以置为10^9+1. 若比target小, 则不可能通过修改-1边的值得到一条最短路径和target相等(因为当前最短路径已经比target小, 若得到新的最短路只能比当前最短路更小, 只会距离target越来越远), 这种情况返回空集即可. 若比target大或没有可行路径, 则需考虑如何处理-1边. 修改-1边相当于将之前忽略的边加入到图中, 加入到图后要考虑两个问题, 加入忽略的边后是否能得到一条新的从源点到目标点的路径, 以及这条路径的权重和与target的大小关系. 在处理-1边时需要先解决第一个问题, 能否得到新的路径, -1权重被修改后的范围为1-2*10^9, 因此我们将边-1的权重先修改为1, 再执行dijistra算法, 看是否能得到从源点到目标点的小于等于target的路径. 不能则证明这条-1边权重修改对目标没有影响, 继续修改下一条-1边权重为1, 再次执行dijistra算法直到dijistra算法的结果小于等于target为止. 此时若小于target则将此时被修改权重的-1边权重调整一下, 增加target和当前路径权重和的差值即得到了等于target的路径. 剩余的-1边一样赋予一个足够大的不影响结果的值即可.

注意此处需要每次修改一条-1边的权重就执行一次dijistra, 因为每一条边能产生的影响都是独立的, 无法预知任意一条-1边对结果的影响, 修改-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
 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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
var INF int

type Edge struct {
    to, weight int
}

type PQItem struct {
    node, dist int
}

type PriorityQueue []*PQItem

func (pq PriorityQueue) Len() int           { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].dist < pq[j].dist }
func (pq PriorityQueue) Swap(i, j int)      { pq[i], pq[j] = pq[j], pq[i] }

func (pq *PriorityQueue) Push(x interface{}) {
    item := x.(*PQItem)
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    *pq = old[0 : n-1]
    return item
}

func runDijkstra(graph [][]Edge, source, destination int) int {
    n := len(graph)
    minDistance := make([]int, n)
    for i := range minDistance {
        minDistance[i] = INF
    }
    minDistance[source] = 0

    pq := make(PriorityQueue, 0)
    heap.Init(&pq)
    heap.Push(&pq, &PQItem{node: source, dist: 0})

    for pq.Len() > 0 {
        current := heap.Pop(&pq).(*PQItem)
        if current.node == destination {
            return current.dist
        }
        if current.dist > minDistance[current.node] {
            continue
        }
        for _, edge := range graph[current.node] {
            newDist := current.dist + edge.weight
            if newDist < minDistance[edge.to] {
                minDistance[edge.to] = newDist
                heap.Push(&pq, &PQItem{node: edge.to, dist: newDist})
            }
        }
    }
    return INF
}

func modifiedGraphEdges(n int, edges [][]int, source int, destination int, target int) [][]int {
    INF = target+1
    graph := make([][]Edge, n)
    for i := range edges {
        if edges[i][2] != -1 {
            graph[edges[i][0]] = append(graph[edges[i][0]], Edge{to: edges[i][1], weight: edges[i][2]})
            graph[edges[i][1]] = append(graph[edges[i][1]], Edge{to: edges[i][0], weight: edges[i][2]})
        }
    }

    currentShortestDistance := runDijkstra(graph, source, destination)
    if currentShortestDistance < target {
        return [][]int{}
    }

    matchesTarget := currentShortestDistance == target

    for i := range edges {
        if edges[i][2] == -1 {
            if matchesTarget {
                edges[i][2] = INF
            } else {
                edges[i][2] = 1
            }
            graph[edges[i][0]] = append(graph[edges[i][0]], Edge{to: edges[i][1], weight: edges[i][2]})
            graph[edges[i][1]] = append(graph[edges[i][1]], Edge{to: edges[i][0], weight: edges[i][2]})

            if !matchesTarget {
                newDistance := runDijkstra(graph, source, destination)
                if newDistance <= target {
                    edges[i][2] += target - newDistance
                    graph[edges[i][0]][len(graph[edges[i][0]])-1].weight = edges[i][2]
                    graph[edges[i][1]][len(graph[edges[i][1]])-1].weight = edges[i][2]
                    matchesTarget = true
                }
            }
        }
    }

    if matchesTarget {
        return edges
    }
    return [][]int{}
}

day186 2024-08-31

1. Two Sum

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0831COEbBIu5rdYe.png

题解

leetcode每日一题系统貌似出问题了(英文版leetcode), 今天的每日一题重复了前几天的1514题, 看了看那天的题目不知什么时候被换成了这道两数和问题, 作为leetcode第一题其地位不亚于abandon. 本题是一道简单题, 只需先扫描一遍数组将数字作为key将其在原数组中的下标作为value存放在一个哈希表中. 再扫描数组, 查询target和当前数字的差值是否存在于哈希表中, 如果存在则返回当前数字和差值作为key的value组成的数组, 注意处理一些特殊情况, 如当差值和数字自身相同时, 因为数组中可能出现重复数字, 如果有多个重复数字, 则返回这个数字的两个不同下标, 如果不重复, 因为不能返回两个相同的下标作为结果, 则跳过这个数字继续向下处理.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func twoSum(nums []int, target int) []int {
    find := map[int][]int{}
    for i,num := range nums{
        find[num] = append(find[num], i)
    }
    for _,num := range nums{
        if len(find[target-num])>0{
            if num != target-num{
                return []int{find[num][0], find[target-num][0]}
            }else if num == target-num && len(find[num]) > 1{
                return []int{find[num][0], find[num][1]}
            }
        }
    }
    return []int{}
}

day187 2024-09-01

2022. Convert 1D Array Into 2D Array

You are given a 0-indexed 1-dimensional (1D) integer array original, and two integers, m and n. You are tasked with creating a 2-dimensional (2D) array with m rows and n columns using all the elements from original.

The elements from indices 0 to n - 1 (inclusive) of original should form the first row of the constructed 2D array, the elements from indices n to 2 * n - 1 (inclusive) should form the second row of the constructed 2D array, and so on.

Return an m x n 2D array constructed according to the above procedure, or an empty 2D array if it is impossible.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09011WbuX5ihPxx0.png

题解

本题是一道简单题, 先判断能否转换成要求的形状的二维数组, 再构造这个二维数组即可, 判断也很直接, 获取一维数组的长度和要求的二维数组的行列数的乘积比较, 不相等就不能转换. 本题如果出现在面试肯定是道送分题.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func construct2DArray(original []int, m int, n int) [][]int {
    if len(original)!=m*n{
        return [][]int{}
    }
    result := [][]int{}
    for i:=0;i<m;i++{
        row := []int{}
        for j:=0;j<n;j++{
            row = append(row, original[i*n+j])
        }
        result = append(result, row)
    }
    return result
}

day188 2024-09-02

1894. Find the Student that Will Replace the Chalk

There are n students in a class numbered from 0 to n - 1. The teacher will give each student a problem starting with the student number 0, then the student number 1, and so on until the teacher reaches the student number n - 1. After that, the teacher will restart the process, starting with the student number 0 again.

You are given a 0-indexed integer array chalk and an integer k. There are initially k pieces of chalk. When the student number i is given a problem to solve, they will use chalk[i] pieces of chalk to solve that problem. However, if the current number of chalk pieces is strictly less than chalk[i], then the student number i will be asked to replace the chalk.

Return the index of the student that will replace the chalk pieces.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09025Z393o8xY4f7.png

题解

本题是一道中等难度题, 但解题思路很直观, 对于这种可能需要经过多轮最后返回剩下粉笔不足那个同学的编号的问题, 可以直接跳过中间大家都能拿到足够粉笔的轮次, 即将几个同学需要的粉笔个数打包看成一个整体, 从总数中先去掉这个整体的倍数直到剩下的粉笔不够拿完一整个轮次, 再去判断具体的哪个同学粉笔不足. 对于此类只有最后一轮次有用中间的都不影响的情况, 直接通过取余保留最后一轮再判断最终结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func chalkReplacer(chalk []int, k int) int {
    chalksum := 0
    for _, num := range chalk{
        chalksum += num
    }
    remain := k % chalksum
    for i, num := range chalk{
        remain -= num
        if remain < 0{
            return i
        }
    }
    return k
}

day189 2024-09-03

1945. Sum of Digits of String After Convert

You are given a string s consisting of lowercase English letters, and an integer k.

First, convert s into an integer by replacing each letter with its position in the alphabet (i.e., replace ‘a’ with 1, ‘b’ with 2, …, ‘z’ with 26). Then, transform the integer by replacing it with the sum of its digits. Repeat the transform operation k times in total.

For example, if s = “zbax” and k = 2, then the resulting integer would be 8 by the following operations:

Convert: “zbax” ➝ “(26)(2)(1)(24)” ➝ “262124” ➝ 262124 Transform #1: 262124 ➝ 2 + 6 + 2 + 1 + 2 + 4 ➝ 17 Transform #2: 17 ➝ 1 + 7 ➝ 8 Return the resulting integer after performing the operations described above. https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09033rbokxnELzJO.png

题解

本题是一道简单题, 只需按照题目所述过程对字符串进行转换, 在转换字符串过程中可以直接将各个字母得到的数字进行按位加和, 无需将字符串转换为整个数字后再进行处理, 因为转换成数字后也是对数字进行按位加和避免溢出可以在处理字符串的过程中直接处理. 再不断对得到的新数字重复按位加和, 此处只要加和到和小于10即可停止直接返回结果, 因为只有一位数字的情况下怎么加和都是这位数字.

代码

 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
func getLucky(s string, k int) int {
    rawnum := 0
    convertbyte := 0
    for i:=0;i<len(s);i++{
        convertbyte = int(s[i]-'a'+1)
        if convertbyte >= 10{
            rawnum += convertbyte/10+convertbyte%10
        }else{
            rawnum += convertbyte
        }
    }
    result := rawnum
    for i:=0;i<k-1;i++{
        result = 0
        for rawnum > 0{
            result += rawnum%10
            rawnum = rawnum/10
        }
        if result < 10{
            break
        }
        rawnum = result
    }
    return result
}

day190 2024-09-04

874. Walking Robot Simulation

A robot on an infinite XY-plane starts at point (0, 0) facing north. The robot can receive a sequence of these three possible types of commands:

-2: Turn left 90 degrees. -1: Turn right 90 degrees. 1 <= k <= 9: Move forward k units, one unit at a time. Some of the grid squares are obstacles. The ith obstacle is at grid point obstacles[i] = (xi, yi). If the robot runs into an obstacle, then it will instead stay in its current location and move on to the next command.

Return the maximum Euclidean distance that the robot ever gets from the origin squared (i.e. if the distance is 5, return 25).

Note:

North means +Y direction. East means +X direction. South means -Y direction. West means -X direction. There can be obstacle in [0,0].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0904tWTRM0Is4AwD.png

题解

本题按照题目中的要求遍历数组并对机器人的行动进行模拟, 每当一次行动结束后计算其到原点的欧氏距离, 并更新最大值.在执行行动过程中如何判断是否碰到障碍物, 对于一个前进k步的行动, 每次前进1步都判断一下当前坐标是否碰到障碍物. 在go中可以使用map作为哈希表, 将障碍物的坐标, 一个固定的长度为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
28
29
30
31
32
33
34
35
36
func robotSim(commands []int, obstacles [][]int) int {
    dx := []int{0, 1, 0, -1}  // 北、东、南、西
    dy := []int{1, 0, -1, 0}
    x, y := 0, 0
    direction := 0
    maxDistSquared := 0

    // 创建障碍物集合
    obstacleSet := make(map[[2]int]bool)
    for _, obstacle := range obstacles {
        obstacleSet[[2]int{obstacle[0], obstacle[1]}] = true
    }

    for _, cmd := range commands {
        if cmd == -2 {
            direction = (direction - 1 + 4) % 4
        } else if cmd == -1 {
            direction = (direction + 1) % 4
        } else {
            for step := 0; step < cmd; step++ {
                nextX, nextY := x + dx[direction], y + dy[direction]
                if obstacleSet[[2]int{nextX, nextY}] {
                    break
                }
                x, y = nextX, nextY

            }
            distSquared := x*x + y*y
            if distSquared > maxDistSquared {
                maxDistSquared = distSquared
            }
        }
    }

    return maxDistSquared
}

总结

本题除了用固定长度的数组作为哈希表的键值, 也可以将x,y坐标通过某种方式哈希后映射到一个唯一的哈希值, 从而将这个哈希值作为键值. 比较简单的哈希方法为将y乘以一个倍数再与x相加, 只要让得到的和不重复(哈希不碰撞)即可. 倍数可以取x,y坐标的最大值. 核心在于让得到的哈希值唯一且对于同样的输入能得到相同的哈希值.

day191 2024-09-05

2028. Find Missing Observations

You have observations of n + m 6-sided dice rolls with each face numbered from 1 to 6. n of the observations went missing, and you only have the observations of m rolls. Fortunately, you have also calculated the average value of the n + m rolls.

You are given an integer array rolls of length m where rolls[i] is the value of the ith observation. You are also given the two integers mean and n.

Return an array of length n containing the missing observations such that the average value of the n + m rolls is exactly mean. If there are multiple valid answers, return any of them. If no such array exists, return an empty array.

The average value of a set of k numbers is the sum of the numbers divided by k.

Note that mean is an integer, so the sum of the n + m rolls should be divisible by n + m.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0905t879DaTShf7B.png

题解

本题首先判断什么情况下不存在可行解, 用平均数计算数字和再减去已知的数字后的余数如果比6n大或比n小, 则无可行解, 6n表示未知的n个骰子都是6, n表示未知的n个骰子都是1. 若有可行解, 则先将这n个骰子均赋值为1, 随后使用贪心赋值, 直到将余数用完. 如3个骰子, 先赋值1,1,1. 若余数为7, 则再赋值6,1,1, 再赋值6,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
func missingRolls(rolls []int, mean int, n int) []int {
    rollsum := 0
    for _,roll := range rolls{
        rollsum += roll
    }
    sum := mean *(len(rolls)+n)
    sum -= rollsum
    if sum > 6*n || sum < n{
        return []int{}
    }
    remain := make([]int, n)
    for i:=0;i<n;i++{
        remain[i] = 1
    }
    sum -= n
    index := 0
    for sum > 5{
        remain[index] += 5
        sum -= 5
        index++
    }
    remain[index] += sum
    return remain
}

day192 2024-09-06

3217. Delete Nodes From Linked List Present in Array

You are given an array of integers nums and the head of a linked list. Return the head of the modified linked list after removing all nodes from the linked list that have a value that exists in nums.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0906o1q2BrIlssFL.png

题解

本题先保存nums中都有哪些数字, 本题的关键在于如何快速判断某个数是否在nums中, 考虑到nums范围到10^5. 因此可以直接用10^5的布尔数组, 下标表示对应的数字, true表示存在, false表示不存在. 当然用哈希表也是可以的. 随后用双指针法对链表中的节点进行遍历并删掉包含在nums中的节点即可.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func modifiedList(nums []int, head *ListNode) *ListNode {
    var right, left *ListNode
    numbool := make([]bool, 100001)
    for _,num := range nums{
        numbool[num] = true
    }
    left = nil
    right = head
    for right != nil{
        if numbool[right.Val]{
            if left == nil{
                head = right.Next
            }else{
                left.Next = right.Next
            }
        }else{
            left = right
        }
        right = right.Next
    }
    return head
}

day193 2024-09-07

1367. Linked List in Binary Tree

Given a binary tree root and a linked list with head as the first node.

Return True if all the elements in the linked list starting from the head correspond to some downward path connected in the binary tree otherwise return False.

In this context downward path means a path that starts at some node and goes downwards.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0907UocXVtnbLaMZ.png

题解

本题首先要遍历二叉树找到节点值和链表的head值相同的节点, 再尝试判断是否有一个向下的路径, 路径上各个节点的值和链表完全一致. 判断是否有路径满足时可以使用dfs, 如果孩子节点值满足链表中下一个节点的值继续遍历直到遍历完整个链表后返回true, 否则返回false.

总体思路为, 遍历二叉树(我使用了bfs), 遍历到节点的值和链表的head值相同则调用函数使用dfs判断是否有路径满足链表, 满足则直接终止遍历. 不满足继续遍历二叉树.

判断是否有路径满足链表的函数设计是在一般的dfs函数上增加了当前节点和链表当前节点值是否相同的判断, 不同直接返回false. 若相同且链表已经遍历到末尾则返回true(递归函数的终止状态). 设计递归函数一定记得设计终止状态. 终止状态清晰了那么中间部分只需完成重复的局部子问题(本题中是遍历节点的左右节点).

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func isSubPath(head *ListNode, root *TreeNode) bool {
    bfs := []*TreeNode{}
    bfs = append(bfs, root)
    result := false
    for len(bfs) > 0{
        for _,node := range bfs{
            if node.Val == head.Val{
                if findPath(head, node){
                    result = true
                    bfs = []*TreeNode{}
                    break
                }
            }
            if node.Left != nil{
                bfs = append(bfs, node.Left)
            }
            if node.Right != nil{
                bfs = append(bfs, node.Right)
            }
            bfs = bfs[1:]
        }
    }
    return result
}

func findPath(head *ListNode, root *TreeNode)bool{
    if head.Val != root.Val{
        return false
    }else if head.Next == nil{
        return true
    }
    result := false
    if root.Left != nil{
        result = result || findPath(head.Next, root.Left)
    }
    if root.Right != nil{
        result = result || findPath(head.Next, root.Right)
    }
    return result
}

day194 2024-09-08

725. Split Linked List in Parts

Given the head of a singly linked list and an integer k, split the linked list into k consecutive linked list parts.

The length of each part should be as equal as possible: no two parts should have a size differing by more than one. This may lead to some parts being null.

The parts should be in the order of occurrence in the input list, and parts occurring earlier should always have a size greater than or equal to parts occurring later.

Return an array of the k parts.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0908vAgxRg5Bk0yS.png

题解

本题就采用最直接的思路, 先遍历一遍数组, 统计所有节点的个数, 再计算得到分割成k部分后每部分的节点个数. 若总节点个数为n, 则n/k得到的是分割后每部分的个数, n%k得到的是分割成k个后剩余的个数(不足k), 则可将这剩余的n%k分配到n%k个部分中, 每部分一个, 即满足题目中的大小差不超过1这一条件.

本题还可以采用快慢指针, 只不过要根据题目设置k个不同的快慢指针, 实现起来似乎更麻烦.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func splitListToParts(head *ListNode, k int) []*ListNode {
    num := 0
    current := head
    for current != nil{
        num++
        current = current.Next
    }
    current = head
    ave := num / k
    remain := num % k
    count := ave
    if remain > 0{
        count++
        remain--
    }
    result := []*ListNode{head}
    for current != nil{
        count--
        if count == 0{
            temp := current
            current = current.Next
            temp.Next = nil
            result = append(result, current)
            count += ave
            if remain > 0{
                count++
                remain--
            }
        }else{
            current = current.Next
        }
    }
    if num < k{
        for i:=0;i<(k-num-1);i++{
            result = append(result, nil)
        }
    }else{
        result = result[0:k]
    }
    return result

}

day195 2024-09-09

2326. Spiral Matrix IV

You are given two integers m and n, which represent the dimensions of a matrix.

You are also given the head of a linked list of integers.

Generate an m x n matrix that contains the integers in the linked list presented in spiral order (clockwise), starting from the top-left of the matrix. If there are remaining empty spaces, fill them with -1.

Return the generated matrix.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0909bHxqUbda73OX.png

题解

本题按照题目要求将链表中的数字依次顺时针序填入指定大小的数组中即可. 如何实现顺时针填入, 为了代码的简洁性, 可以用方向数组来表示向上下左右四个方向前进时时行列坐标的变化, 用一个变量指示方向, 这样就可以将四个方向前进的代码统一起来, 行列分别与前进的方向数组当前方向的坐标相加即表示前进. 注意起点位于0,0的左面. 分别记录当前行和列应该前进的步数, 按行移动一次则将行步数减一, 按列则将列步数减一. 直到遍历矩阵所有格子则结束填充.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func spiralMatrix(m int, n int, head *ListNode) [][]int {
    matrix := [][]int{}
    for i:=0;i<m;i++{
        row := make([]int, n)
        matrix = append(matrix, row)
    }
    current := head
    steps := []int{n,m-1}
    sum := n*m
    x,y := 0,-1
    direction := [][]int{{0,1},{1,0},{0,-1},{-1,0}}
    dir := 0
    for sum > 0{
        for i:=0;i<steps[dir%2];i++{
            x += direction[dir][0]
            y += direction[dir][1]
            if current != nil{
                matrix[x][y] = current.Val
                current = current.Next
            }else{
                matrix[x][y] = -1
            }
        }
        sum -= steps[dir%2]
        steps[dir%2]--
        dir = (dir+1)%4
    }
    return matrix
}

day196 2024-09-10

2807. Insert Greatest Common Divisors in Linked List

Given the head of a linked list head, in which each node contains an integer value.

Between every pair of adjacent nodes, insert a new node with a value equal to the greatest common divisor of them.

Return the linked list after insertion.

The greatest common divisor of two numbers is the largest positive integer that evenly divides both numbers.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0910SKsaBc1wbvu5.png

题解

本题了解求两个数的最大公因数的算法(欧几里得算法)就很好解题. 实现欧几里得算法可以使用递归方法也可以使用迭代, 本题中我使用的是迭代. 再用两个指针分别指向链表中的前一个节点和后一个节点, 计算得到二者的最大公因数后在两个节点之间插入一个新节点. 直到链表末尾.

代码

 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
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func insertGreatestCommonDivisors(head *ListNode) *ListNode {
    if head.Next == nil{
        return head
    }
    left := head
    right := head.Next
    for right != nil{
        newnode := ListNode{gcd(left.Val, right.Val), right}
        left.Next = &newnode
        left = right
        right = right.Next
    }
    return head
}

func gcd(x, y int)int{
    big, small, remain := 0,0,0
    if x > y{
        big = x
        small = y
    }else{
        big = y
        small = x
    }
    remain = big % small
    for remain > 0{
        big = small
        small = remain
        remain = big % small
    }
    return small
}

day197 2024-09-11

2220. Minimum Bit Flips to Convert Number

A bit flip of a number x is choosing a bit in the binary representation of x and flipping it from either 0 to 1 or 1 to 0.

For example, for x = 7, the binary representation is 111 and we may choose any bit (including any leading zeros not shown) and flip it. We can flip the first bit from the right to get 110, flip the second bit from the right to get 101, flip the fifth bit from the right (a leading zero) to get 10111, etc. Given two integers start and goal, return the minimum number of bit flips to convert start to goal.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0911Bp83tVuclXiK.png

题解

本题是一道简单题, 对两个数进行异或操作会使得两个数二进制位上数字相同的位变为0, 数字不同的位变为1, 则进行异或后统计有多少个1即可, 本题大概是让人了解一下异或操作.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func minBitFlips(start int, goal int) int {
	// XOR the two numbers to get the differing bits
	diff := start ^ goal
	count := 0

	// Count the number of 1's in the XOR result
	for diff > 0 {
		count += diff & 1
		diff >>= 1
	}

	return count
}

day198 2024-09-12

1684. Count the Number of Consistent Strings

You are given a string allowed consisting of distinct characters and an array of strings words. A string is consistent if all characters in the string appear in the string allowed.

Return the number of consistent strings in the array words.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0912Wvb3dm8bZMGM.png

题解

本题是一道简单题, allowed中的字母均为不同字母, 则可用长度为26的布尔数组保存allowed中哪个字母出现过, 再遍历words数组, 对每个字符串进行遍历, 一旦出现过未在allowed中出现的字母则直接跳过, 遍历下一个字符串, 对符合要求的字符串进行计数.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func countConsistentStrings(allowed string, words []string) int {
    allow := make([]bool, 26)
    for i,_ := range allowed{
        allow[allowed[i]-'a'] = true
    }
    flag := true
    result := 0
    for _,word := range words{
        flag = true
        for i,_ := range word{
            if !allow[word[i]-'a']{
                flag = false
                break
            }
        }
        if flag{
            result++
        }
    }
    return result
}

day199 2024-09-13

1310. XOR Queries of a Subarray

You are given an array arr of positive integers. You are also given the array queries where queries[i] = [lefti, righti].

For each query i compute the XOR of elements from lefti to righti (that is, arr[lefti] XOR arr[lefti + 1] XOR … XOR arr[righti] ).

Return an array answer where answer[i] is the answer to the ith query.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0913HP8rsSdDtlxy.png

题解

本题可使用"前缀和". 考虑到每次计算query都是从query的lefti异或到righti. 之前提到过精准计算某一段的值可以用以该段段尾为结尾的前缀和减去该段段首为结尾的前缀和. 在这里同样如此, 要用到异或的特性, a^b^c的值再与a异或相当于b^c. 则可先计算出arr数组的全部元素的前缀"异或和"(即求到当前下标的子数组的全部异或后的结果). 在计算query时用到段尾下标的前缀"异或和"与到段首下标的前缀"异或和"异或即得中间部分的异或和. 注意求[0,1]的异或和是求0位和1位异或的结果, 是包含0位的, 而求前缀和时相当于从下标"-1"开始计算, 需要在前缀和数组前面补充一个值为0的前缀和表示"-1"的值, 这样求[0,1]的前缀和时就是用1的前缀和减去"-1"的前缀和

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func xorQueries(arr []int, queries [][]int) []int {
    prefix := []int{0}
    prexor := 0
    for _, num := range arr{
        prexor = prexor ^ num
        prefix = append(prefix, prexor)
    }
    result := []int{}
    for _,query := range queries{
        result = append(result, prefix[query[0]] ^ prefix[query[1]+1])
    }
    return result
}

day200 2024-09-14

2419. Longest Subarray With Maximum Bitwise AND

You are given an integer array nums of size n.

Consider a non-empty subarray from nums that has the maximum possible bitwise AND.

In other words, let k be the maximum value of the bitwise AND of any subarray of nums. Then, only subarrays with a bitwise AND equal to k should be considered. Return the length of the longest such subarray.

The bitwise AND of an array is the bitwise AND of all the numbers in it.

A subarray is a contiguous sequence of elements within an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0914zXkuivxqDnnG.png

题解

本题也是一道和位运算有关的问题, 乍一看这个问题似乎和昨天的问题有几分相似, 看到题面可能会想到只能计算出所有子数组的按位与, 同时记录当前子数组按位并的最大值, 以及最大值对应子数组的长度. 最终得到结果.

但是位运算一定有些自身的特点可以帮助我们减少需要遍历的子数组的个数, 我们考虑任意两个数进行按位与, 可以发现任意一个数的某一位为0则运算的结果中该位即为0, 则可以肯定, 对于两个不同的数, 进行按位与得到的结果肯定比两个数中较大的那个小(二进制0的个数最少也要和小的数一样多, 如1和3与得到1). 则由此我们只需在遍历数组时记下当前最大值, 并记下当前该最大值连续出现时出现的次数的最大值, 最终即得到结果.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
    int longestSubarray(vector<int>& nums) {
        int max = 0;
        int longest = 1;
        int current = 0;
        for (auto iter : nums){
            if (iter > max){
                max = iter;
                longest = current = 1;
            }else if (iter == max){
                current++;
                if (current > longest){
                    longest = current;
                }
            }else{
                current = 0;
            }
        }
        return longest;
    }
};

day201 2024-09-15

1371. Find the Longest Substring Containing Vowels in Even Counts

Given the string s, return the size of the longest substring containing each vowel an even number of times. That is, ‘a’, ’e’, ‘i’, ‘o’, and ‘u’ must appear an even number of times.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/091589L5p3UZ9skI.png

题解

本题第一步容易想到可以使用"前缀和"记录到每个下标的前缀子字符串中各个元音字母个数的奇偶性. 找最长的包含偶数元音字母的子字符串即寻找两个距离最远的下标, 且以这两个下标结尾的前缀子字符串中各个元音字母个数的奇偶性相同. 此时可以想到可以用数组来保存每个元音字母个数的奇偶性, 这样就需要长度为5的数组, 可以是整型数组也可以是布尔数组(仅有奇或者偶两种状态). 只是在比较任意两个下标对应的前缀子字符串元音奇偶性是否相同时需要遍历这个数组.

有没有更快的方法可以避免遍历数组呢? 可以想到对于奇偶这种二值状态, 仅需要一个二进制位就能表示, 而任意一个整型都包含32个二进制位, 所以用一个整数表示五个元音字母的奇偶状态是完全没问题的. 这里是充分利用每个二进制位能包含的信息. 有一个经典问题, 即1000瓶水里有一瓶是有毒的, 需要多少只老鼠才能试出那瓶有毒的毒药(可怜的鼠鼠)也是相同的思想.

有了这个想法后, 我们可以用一个整数来表示到某个下标的前缀子字符串中元音字母的奇偶性, 只要两个下标处对应的整数相同, 则二者之间的子字符串就满足题目要求. 考虑五个元音字母用二进制表示对应的整数最大为31. 可以直接构造一个长度32的二维数组, 记录每个整数对应的开始下标和当前的最长长度, 如此即可在一遍遍历字符串的同时不断更新该数组, 得到每个整数对应的最长长度, 最后遍历这个二维数组, 找出最长长度中的最长长度即得结果.

代码

 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 findTheLongestSubstring(string s) {
        int rows = 32;
        int cols = 2;
        int currentmask = 0;
        int index = 0;
        vector<vector<int>> array(rows, vector<int>(cols, -1));
        int max_length = 0;

        for (char character : s) {
            switch (character) {
                case 'a': currentmask ^= 1; break;
                case 'e': currentmask ^= 2; break;
                case 'i': currentmask ^= 4; break;
                case 'o': currentmask ^= 8; break;
                case 'u': currentmask ^= 16; break;
            }

            if (currentmask != 0) {
                if (array[currentmask][0] == -1) {
                    array[currentmask][0] = index;
                } else {
                    array[currentmask][1] = index - array[currentmask][0];
                    max_length = max(max_length, array[currentmask][1]);
                }
            } else {
                max_length = max(max_length, index + 1);
            }

            index++;
        }

        return max_length;
    }
};

day202 2024-09-16

539. Minimum Time Difference

Given a list of 24-hour clock time points in “HH:MM” format, return the minimum minutes difference between any two time-points in the list.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0916FyhMh6BMnOnh.png

题解

本题要求列表中任意两个时间之间的最短时间差, 注意仅要求求出时间差, 不要求给出是具体哪两个时间对应的时间差, 也不要求给出时间的具体位置. 则通过给时间排序, 再遍历排好序的时间数组找到相邻有序时间之间差值的最小值. 排序可以先将时间全部转换为从0点开始的分钟数再给分钟数排序, 这样排序时只需对整数排序, 也方便计算时间差值. 在排序时从0点开始排序, 24点为最大值, 则在计算时间之间的差值时最后还要计算一下最后一个时间和第一个时间反向的差值(排序是顺时针的, 这里相当于计算下逆时针的时间差, 因为时间相当于一个圆形)与最小差值比较并更新. 注意以上针对的均为所有时间都不相同的情况, 若存在两个时间相同, 直接返回0. c++中的set 标准库表示的有序集合是内部自动有序且不含重复元素的容器, 因此可以利用这个数据结构来保存给出的时间转换后的分钟数.

代码

 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
class Solution {
public:
    int findMinDifference(vector<string>& timePoints) {
        set<int> minutesSet;
        for (const auto& timePoint : timePoints){
            int hours = stoi(timePoint.substr(0, 2));
            int minute = stoi(timePoint.substr(3, 2));
            int minutes = hours*60 + minute;
            if (minutesSet.contains(minutes)){
                return 0;
            }
            minutesSet.insert(minutes);
        }
        int last = 0;
        int mintime = 1441;
        set<int>::iterator first = minutesSet.begin();
        set<int>::iterator it = minutesSet.begin();
        last = *it;
        it++;
        while (it != minutesSet.end()){
            mintime = min(mintime, *it-last);
            last = *it;
            it++;
        }
        mintime = min(mintime, *first+1440-last);
        return mintime;
    }
};

day203 2024-09-17

884. Uncommon Words from Two Sentences

A sentence is a string of single-space separated words where each word consists only of lowercase letters.

A word is uncommon if it appears exactly once in one of the sentences, and does not appear in the other sentence.

Given two sentences s1 and s2, return a list of all the uncommon words. You may return the answer in any order.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0917Wg5ECkbPuB5F.png

题解

本题读懂题目可以发现, 实际上只是要求记下所有在所有字符串中均只出现了一次的单词, 最终返回这些单词. 则先将字符串按空格分割, 再使用map记录所有单词出现的次数, 将单词作为key, 出现次数作为value. 最终遍历map并将只出现一次的单词放入数组中返回.

代码

 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:
    vector<string> uncommonFromSentences(string s1, string s2) {
        map<string, int> word;
        cutSentence(s1, word);
        cutSentence(s2, word);
        vector<string> res;
        for (map<string, int>::iterator it = word.begin();it != word.end();it++){
            if (it->second == 1){
                res.push_back(it->first);
            }
        }
        return res;
    }

    void cutSentence(string input, map<string, int>& words){
        istringstream ss(input);
        string word;
        while(ss>>word) {
            if (words.find(word) == words.end()) {
            words[word] = 1;
        } else {
            words[word]++;
        }
        }
    }
};

day204 2024-09-18

179. Largest Number

Given a list of non-negative integers nums, arrange them such that they form the largest number and return it.

Since the result may be very large, so you need to return a string instead of an integer.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09188ZOJ155HaaYk.png

题解

本题将数字当作字符串, 按照字符序从大到小排序再将排好序后的字符串连接起来即得到答案. 在这里定义的数字字符串的大小顺序要满足以下条件:

  1. 逐字符比较, 字符大的字符串序大.
  2. 两个不同长度的字符串, 短字符串与长字符串逐字符比较后全部相同, 而长字符串还有剩余字符, 则比较将两个字符串按两种方式(短前长后和长前短后)连接后得到的新字符串的字符串序.

注意处理全为0的特殊情况, 若最终得到的字符串起始为0, 直接返回单个0组成的字符串即可.

代码

 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
bool customCompare(const string& a, const string& b) {
    size_t i = 0;
    while (i < a.length() && i < b.length()) {
        if (a[i] != b[i]) {
            return a[i] > b[i];  // 逐字符比较,字符大的字符串序大
        }
        i++;
    }
    if (a.length() == b.length()){
        return false;
    }
    // 如果短字符串是长字符串的前缀,则比较将两个字符串两种方式连接后的字符串大小
    return customCompare(a+b, b+a);
}

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        vector<string> numstr;
        for (auto num : nums){
            numstr.push_back(to_string(num));
        }
        sort(numstr.begin(), numstr.end(), customCompare);
        string result;
        for (auto str : numstr){
            result += str;
        }
        if (result[0] == '0'){
            return "0";
        }
        return result;
    }

};

day205 2024-09-19

241. Different Ways to Add Parentheses

Given a string expression of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. You may return the answer in any order.

The test cases are generated such that the output values fit in a 32-bit integer and the number of different results does not exceed 10^4.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0919QR2gc7OoJQwJ.png

题解

本题算是一道比较典型的递归加记忆化问题, 首先确定终止条件, 当某个子表达式中仅包含数字时, 则直接返回这个数字. 当是一个表达式时, 则分别递归计算运算符两侧的表达式可能得到的结果并根据运算符对两侧结果进行运算得到当前表达式所有可能得到的运算结果. 感觉和学习编译原理时经典的递归下降解析表达式的值有些相似.

举例来说, 对"2*3-4*5" 这样的式子, 我们只需将其看作不同表达式和运算符的组合, 分别计算这些表达式的值就能得出最终结果, 而计算表达式值的过程都是结构相似的问题, 因此可以不断分解成子问题. 这里就可以分解成2,*, 3-4*5. 即表达式2, 乘号以及后面的表达式. 也可以分解成2*3, -, 4*5. 这个分解可以用简单的字符串遍历来完成, 遍历整个表达式串, 遇到运算符则对运算符两侧的表达式进行递归求值, 继续遍历下一个运算符, 直到结尾.

显然这个过程中会有一些子表达式被重复计算, 因此可以想办法保存递归过程中算得的中间结果, 可以用一个二维数组表示从下标i开始到下标j的表达式的可能值. 即matrix[i][j]记录了i-j的表达式的所有可能运算结果. 二维数组可以使用一维数组表示, 即用i*length(表达式长度)+j来表示matrix[i][j]的值. 这样就可以用一个vector<vector>来表示记忆化的数组.

对于递归的问题, 思路一定要清楚, 只需找到递归的终止条件, 并写出解决最小的子问题的过程, 剩下的就是将问题分解, 分别使用递归求解分解后的问题再将结果整合起来进行处理, 至于中间递归的过程不要试图去想的太详细, 只要在宏观上能实现 分解问题->解决子问题->合并解决子问题的结果. 就能得到正确答案.

代码

 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
class Solution {
public:
    const int Big = 100000;
    vector<int> diffWaysToCompute(string expression) {
        int leng = expression.length();
        vector<vector<int>> memo(leng*leng);
        vector<int> result = subCompute(expression, 0, leng-1, memo, leng);
        return result;
    }

private:
    vector<int> subCompute(const string& expression, int begin, int end, vector<vector<int>>& memo, int leng) {
        if (memo[begin*leng+end].size() > 0) {
            return memo[begin*leng+end];
        }

        vector<int> result;
        bool hasOperator = false;

        for (int i = begin; i <= end; i++) {
            if (expression[i] == '+' || expression[i] == '-' || expression[i] == '*') {
                hasOperator = true;
                vector<int> left = subCompute(expression, begin, i-1, memo, leng);
                vector<int> right = subCompute(expression, i+1, end, memo, leng);

                for (int l : left) {
                    for (int r : right) {
                        if (expression[i] == '+') {
                            result.push_back(l + r);
                        } else if (expression[i] == '-') {
                            result.push_back(l - r);
                        } else if (expression[i] == '*') {
                            result.push_back(l * r);
                        }
                    }
                }
            }
        }

        if (!hasOperator) {
            result.push_back(stoi(expression.substr(begin, end-begin+1)));
        }

        memo[begin*leng+end] = result;
        return result;
    }
};

day206 2024-09-20

214. Shortest Palindrome

You are given a string s. You can convert s to a palindrome by adding characters in front of it.

Return the shortest palindrome you can find by performing this transformation.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/092021GmdHWmTgR4.png

题解

本题是一道难题,但整体思路是比较简洁的,我们只能在字符串的前面添加字符来构造回文串,则以字符串开头作为起始的回文子字符串是不需要构造的,需要添加的只是回文子字符串后面的部分。(如aabaac,则aabaa是不需要做任何变动的,只需添加c),如果在字符串中间位置有一个回文串对于构造整个回文串影响不大(如acbcd,显然要构造回文串必须将cbcd反转一遍)。则解题思路为先找到以字符串开头为起始的原始字符串中的最长回文子串,再将该子串后面的字符串反转添加到字符串前面即得到目标字符串。

接下来要解决的问题是,如何得到以开头作为起始的最长回文子串有多长。如果我们将字符串逆序, 问题就变成了能找到的包含原始字符串开头和包含逆转后的字符串末尾的两个字符串中相同的子字符串最长有多长(如aabaac逆转后为caabaa则前一个字符串开头的aa和后一个字符串结尾的aa完全相同,则最长为2)。这是一个模式匹配的问题,如果熟悉kmp算法的话,就会发现这和kmp算法中next数组的含义非常相似,如果把原始字符串和逆转后的字符串连接在一起,就变成了求这个连接后的字符串的最长公共前后缀的长度问题,若字符串长度为n,则此问题即为求next[n-1]。

kmp算法的next的求法网上已经有很多讲解,以下面的情况为例,

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0920KwtiVwZYVDyC.jpg

如果pk和pj相同,则将长度加1,但当pk和pj不同时,为什么要去找pnext[k]。实际上这里pk和pj不同时,我们的目标是找到pk之前的某个公共前后缀,这个更小的公共前后缀中前缀的下一位恰为pj,则由于这是一个公共前后缀,这个公共前后缀是之前的pk对应的公共前后缀的一部分,则这个更小的公共前缀也会和pj前面的一个更小的公共后缀对应起来。这时之前的公共前缀后面恰好有一个pj就和后面pj前面的公共后缀再加上pj对应起来构成了一个新的公共前后缀。我们就知道了新的公共前后缀的长度。

本题还可以使用滚动哈希来解决。即Rabin-Karp算法,这里不再讲解该算法,可参考下面的资料

Rabin-Karp算法

代码

 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 {
public:
    string shortestPalindrome(string s) {
        string rev_s = s;
        reverse(rev_s.begin(), rev_s.end());
        string temp = s + "#" + rev_s;

        int n = temp.length();
        vector<int> lps(n, 0);

        for (int i = 1, len = 0; i < n;) {
            if (temp[i] == temp[len]) {
                lps[i++] = ++len;
            } else if (len) {
                len = lps[len - 1];
            } else {
                lps[i++] = 0;
            }
        }
        // 最长回文前缀的长度
        int longest_palindrome_prefix = lps[n - 1];

        // 需要反转并添加到前面的子串
        string to_reverse = s.substr(longest_palindrome_prefix);
        reverse(to_reverse.begin(), to_reverse.end());

        // 拼接并返回结果
        return to_reverse + s;
    }
};

day207 2024-09-21

386. Lexicographical Numbers

Given an integer n, return all the numbers in the range [1, n] sorted in lexicographical order.

You must write an algorithm that runs in O(n) time and uses O(1) extra space.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09219ROMuzHfztdy.png

题解

本题要求将从1-n的数字按照字典序排列,要明确字典序的含义,即将数字视为字符串,从头开始遍历字符串,对每一位的字符,字符小(ascii码小,或者理解为一般意义上的字符顺序如a-z,0-9)的即在前面,短字符串和长字符串的前缀相同,则短字符串在前面。

在遍历这样的数字过程中相当于构造了一棵树,树的每一层都是0-9,如果还可以继续产生下一层则先产生下一层并遍历。对每一层的操作都是相同的因此可以使用递归解决。这里的层就是将原来的数字乘10后对个位进行遍历,看当前产生的新数字乘10是否小于n,小于n则可以产生新的一层。在每一层的操作为:遍历0-9,遍历每个数字时与原始数字相加构成新数字,判断新数字是否小于n,小于n则将新数字乘10判断是否小于n,小于n则递归调用层处理函数,将新数字乘10作为参数传递当作下一层的原始数字。

相当于固定0-9的顺序产生一棵生成树,对这棵多叉树进行后序遍历。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
    vector<int> lexicalOrder(int n) {
        vector<int> result;
        for(int i=1;i<=9 && i<=n;i++){
            result.push_back(i);
            if (i*10 <= n){
                layer(i*10, result, n);
            }
        }
        return result;
    }
    void layer(int raw, vector<int>& result, int target){
        for (int i=0; i<=9 && raw+i<=target; i++){
            result.push_back(raw+i);
            if ((raw+i)*10 <= target){
                layer((raw+i)*10, result, target);
            }
        }
    }
};

day208 2024-09-22

440. K-th Smallest in Lexicographical Order

Given two integers n and k, return the kth lexicographically smallest integer in the range [1, n].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0922rJ3xhbTWPf0e.png

题解

本题是一道难题。

我们有昨天题目的基础,可能会觉得只需在昨天题目解法的基础上返回数组中的第k个数。实际操作中可以发现会出现MLE和TLE的问题。显然不是这么简单扩展一下就能解决的问题,先考虑MLE的问题,考虑到最终我们只需要返回第k大的数字,则没必要用result数组来保存所有数字,只需在遍历到第k大的数字时将结果赋给变量并退出递归函数,这样只需要一个变量保存最终结果,避免了n过大时MLE的问题。再考虑TLE的问题,因为本题中n和k的取值范围都非常大,我们需要想一些方法来对字典序的数字的生成树进行计数,通过计数直接定位到每个位上的数字是什么。

我们需根据树的节点个数不断一层层判断当前层应当选择哪个节点直到最低层,被选择过的层之上的节点个数应从目标中减去,因为这些节点都是有意义的,表示比较短的数字(如两层的情况,根节点表示1这个数字,这个数字本身也是序列中的一部分)。并将选择的节点对应的数字连接即得最终的结果。(也可理解为根据子树的节点个数选择需要遍历的节点,记录遍历路径即得最终结果,算是一种启发式遍历)。

在每一层中,根据已经遍历过的节点前缀计算0节点对应的子树节点个数,将节点个数与当前总节点个数加和并与目标k比较,小于等于k则继续计算下一个相邻节点对应的子树节点数,如此反复直到找到目标k所属的节点子树。则该节点的值为下一位的值,继续遍历该节点对应的子树的下一层按照上面的方式寻找下一层中目标k对应的节点。注意每棵树的节点个数是受到n限制的,即该树的所有路径得到的数字均需小于等于n。

如何对某个已知前缀的子树中的节点个数进行计数呢?先判断当前前缀pre1是否小于等于n,再比较(pre1+1)(pre2)和n的大小,如果pre2比n小,说明该层是满的,此时应该给计数加上当前层的节点个数,即从根节点开始1,10…10^k(k表示层数)。给pre1和pre2均乘10进入树的下一层再继续判断下一层是否是满的,这里可以发现由于一开始pre2=pre1+1,则实际上若某一层的节点是满的,该层的节点个数就是pre2-pre1(和10^k相等)。如果节点不是满的,那么该层节点个数是n-pre1(注意这个过程中pre1一直是变化的,不断乘10)。举例来说,假如当前前缀为12,n为140,则以该前缀作为根节点的子树节点个数为13-12+(13*10-12*10)=11。假如当前前缀为12,n为125则节点个数为(13-12)+(125-12*10),因为125<13*10=130。再结合下面的图理解。

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0922mnOFiWIMG_E0C343C9BCC8-1.jpeg

为什么采用昨天的解法会超时而这种解法就快得多,在这种解法中,通过不断计数子树的节点数来选择某一层对应的节点相当于一位一位的确定最终数字的某一位,相当于一种“十分法”。“二分法”是通过目标值和当前中间值的相对大小确定属于左右哪个区间,这里的“十分法”类似,通过比较目标k和当前10个区间的前n个区间的节点总数大相对大小确定目标k属于哪个区间。再继续将这个小区间作为整体再次10分。直到找到目标数字。

代码

 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:
    int findKthNumber(int n, int k) {
        int curr = 1;
        k--;

        while (k > 0) {
            long long steps = countSteps(n, curr, curr + 1);
            if (steps <= k) {
                k -= steps;
                curr++;
            } else {
                k--;
                curr *= 10;
            }
        }

        return curr;
    }

private:
    long long countSteps(long long n, long long n1, long long n2) {
        long long steps = 0;
        while (n1 <= n) {
            steps += min(n + 1, n2) - n1;
            n1 *= 10;
            n2 *= 10;
        }
        return steps;
    }
};

day209 2024-09-23

2707. Extra Characters in a String

You are given a 0-indexed string s and a dictionary of words dictionary. You have to break s into one or more non-overlapping substrings such that each substring is present in dictionary. There may be some extra characters in s which are not present in any of the substrings.

Return the minimum number of extra characters left over if you break up s optimally.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0923oan93ei5Pq70.png

题解

本题考虑字符串任何一个位置之前的子字符串中的最小extra字符个数可以如何求得,则如果把该位置i自己当作一个extra字符,那么其之前的最小extra字符个数即为extra[i-1]+1。如果位置i对应的字符是某个字典中某个字符串的末尾,设这个字符串长度为k,则extra[i]=extra[i-k]。注意i可能是多个字符串的末尾,则我们取这些这些字符串计算得到的所有extra[i]和extra[i-1]+1中的最小值。

我们可以从后向前遍历字符串s,按照上面的解法通过不断递归求得当前位置的extra[i]。这是大多数题解中的做法,但一般而言我们先想到的还是从前向后遍历字符串,如何在从前向后遍历时求得各个位置的extra[i]呢。我们考虑任意位置i,假设该位置的extra[i]是已知的。则若我们找到所有以位置i字符开头的包含在字典中的字符串,设该字符串末尾的位置为j,则extra[j]=min(extra[i],extra[j])。那么可以找到所有这样的字符串并更新对应位置的extra。当遍历到j位置时,再将j位置的字符当作extra字符,比较extra[j-1]+1和extra[j]的大小并取较小值。这样假如存在位置i和位置k,以这两个位置开头的字符串都存在于字典中且均以位置j结尾,则当遍历完i和k时,j位置的extra[j]的值就会是min(extra[i],extra[k])。以此类推可知最终extra[j]一定能取到最小值(表示j之前的子字符串中的最少extra字符个数)。

这里我们寻找所有以位置i的字符开头的在字典中的字符串时,如果有多个字符串均符合,则短的字符串必定为长的字符串的前缀串(因为字符串s是固定的,判断字符串是否在字典中是不断遍历s得到的)。判断某个字符串是否在一个字符串集合中,字典树Trie是常用的快速判断方法,而在本题中使用trie的优势在于相同前缀的字符串无需再从头开始比较字符,直接沿着之前前缀串的最后一个节点继续向下查找即可,大大加快了查找速度。

代码

 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
86
87
88
89

const int ALPHABET_SIZE = 26;

struct TrieNode {
    TrieNode* children[ALPHABET_SIZE];
    bool isEndOfWord;

    TrieNode() : isEndOfWord(false) {
        for(int i = 0; i < ALPHABET_SIZE; ++i){
            children[i] = nullptr;
        }
    }
};


class Trie {
public:
    TrieNode* root;

    Trie() { root = new TrieNode(); }

    void insert(const string &word) {
        TrieNode* node = root;
        for(char c : word){
            int index = c - 'a';
            if(index < 0 || index >= ALPHABET_SIZE){
                continue;
            }
            if(node->children[index] == nullptr){
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
        }
        node->isEndOfWord = true;
    }

    ~Trie() {
        function<void(TrieNode*)> deleteTrie = [&](TrieNode* node) {
            if(node == nullptr) return;
            for(int i = 0; i < ALPHABET_SIZE; ++i){
                if(node->children[i] != nullptr){
                    deleteTrie(node->children[i]);
                }
            }
            delete node;
        };
        deleteTrie(root);
    }
};

class Solution {
public:
    int minExtraChar(string s, vector<string>& dictionary) {
        int n = s.size();

        Trie trie;
        for(const string &word : dictionary){
            trie.insert(word);
        }

        vector<int> dp(n + 1, INT32_MAX);
        dp[0] = 0;

        for(int i = 0; i < n; ++i){

            if(dp[i] + 1 < dp[i+1]){
                dp[i+1] = dp[i] +1;
            }

            TrieNode* node = trie.root;
            int j = i;
            while(j < n){
                int index = s[j] - 'a';
                if(index < 0 || index >= ALPHABET_SIZE || node->children[index] == nullptr){
                    break;
                }
                node = node->children[index];
                ++j;
                if(node->isEndOfWord){
                    if(dp[i] < dp[j]){
                        dp[j] = dp[i];
                    }
                }
            }
        }

        return dp[n];
    }
};

day210 2024-09-24

3043. Find the Length of the Longest Common Prefix

You are given two arrays with positive integers arr1 and arr2.

A prefix of a positive integer is an integer formed by one or more of its digits, starting from its leftmost digit. For example, 123 is a prefix of the integer 12345, while 234 is not.

A common prefix of two integers a and b is an integer c, such that c is a prefix of both a and b. For example, 5655359 and 56554 have a common prefix 565 while 1223 and 43456 do not have a common prefix.

You need to find the length of the longest common prefix between all pairs of integers (x, y) such that x belongs to arr1 and y belongs to arr2.

Return the length of the longest common prefix among all pairs. If no common prefix exists among them, return 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0924WdYUOX86qpiT.png

题解

题目中的数组要求求最长公共前缀,考虑到每个数组中的不同数字之间也可能存在公共前缀,为了避免对字符串进行重复遍历,可以使用字典树。字典树充分利用了数字每一位的信息,非常适用于类似本题目的求前缀等场景下,还有一道经典题目是求数组中任意两个数字的最大异或和,也可以使用字典树求解,利用异或运算异或两次后会回到数字本身的特性,用数字的二进制形式构造字典树,充分利用每个二进制位的信息,通过贪心尽量将每一位置为1,最终找到最大异或和,可参考P4551 最长异或路径

本题在对两个数组构建好两棵字典树后,对字典树同时进行dfs,依次选取每一层中相同的字符对应的子节点进行遍历,直到在某一层无法找到共同字符为止。用一个变量保存遍历到的树的最大深度即为最长公共前缀的长度。

代码

 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
#pragma GCC optimize("O3", "unroll-loops")

const int TREESIZE = 10;

struct TrieNode{
    TrieNode *children[TREESIZE];
    bool isEndOfWord;

    TrieNode(): isEndOfWord(false){
        for (int i = 0;i<TREESIZE;++i){
            children[i] = nullptr;
        }
    }
};

class Trie{
    public:
    TrieNode *root;
    Trie(){root = new TrieNode();}

    void insert(const string& word){
        TrieNode* node = root;
        for(char c : word){
            if (node->children[c-'0'] == nullptr){
                node->children[c-'0'] = new TrieNode();
            }
            node = node->children[c-'0'];
        }
        node->isEndOfWord = true;
    }

    ~Trie() {
        function<void(TrieNode*)> deleteTrie = [&](TrieNode* node) {
            if(node == nullptr) return;
            for(int i = 0; i < TREESIZE; ++i){
                if(node->children[i] != nullptr){
                    deleteTrie(node->children[i]);
                }
            }
            delete node;
        };
        deleteTrie(root);
    };

};

class Solution {
public:
    int longestCommonPrefix(vector<int>& arr1, vector<int>& arr2) {
        ios::sync_with_stdio(false);
        Trie *trie1 = new Trie();
        for (int num : arr1){
            string numstr = to_string(num);
            trie1->insert(numstr);
        }
        Trie *trie2 = new Trie();
        for (int num : arr2){
            string numstr = to_string(num);
            trie2->insert(numstr);
        }
        int result = 0;
        dfs(trie1->root, trie2->root, 0, result);
        return result;

    }

    void dfs(TrieNode* root1,TrieNode* root2, int depth, int& result){
        result = max(result, depth);
        for(int i=0;i<TREESIZE;i++){
            if (root1->children[i] != nullptr && root2->children[i] != nullptr){
                cout<< i;
                dfs(root1->children[i],root2->children[i],depth+1,result);
            }
        }
    }
};

day211 2024-09-25

2416. Sum of Prefix Scores of strings

You are given an array words of size n consisting of non-empty strings.

We define the score of a string word as the number of strings words[i] such that word is a prefix of words[i].

For example, if words = [“a”, “ab”, “abc”, “cab”], then the score of “ab” is 2, since “ab” is a prefix of both “ab” and “abc”. Return an array answer of size n where answer[i] is the sum of scores of every non-empty prefix of words[i].

Note that a string is considered as a prefix of itself.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0925UvJwkizQjyZA.png

题解

本题是一道难题,但像这种要多次计算字符串前缀的问题我们已经熟悉了,为了避免重复遍历字符串可以使用字典树trie。在这个问题上思考一下为什么用字典树更好,如果我们用map将某个前缀作为key,其出现的次数作为value。这样对于有包含关系的前缀就会产生大量重复,同时没能充分利用前缀自身自带的字符的先后关系(字符串ab,a是在b前面且与b紧密相邻的,用字典树则将ab相邻和a在b前面这两种字符关系都表示了出来)。

熟悉字典树后,本题只需对每个字符串构建字典树,对字典树的节点做一些修改,节点中加入表示被访问到次数的count变量,无需记录节点是否为单词的结尾。构建好后对每个字符串,访问字典树,并将路径上所有节点的count相加即得最终的sum。

代码

 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
const int ALPHABET_SIZE = 26;

struct TrieNode {
    TrieNode* children[ALPHABET_SIZE];
    int count;

    TrieNode() : count(0) {
        for(int i = 0; i < ALPHABET_SIZE; ++i){
            children[i] = nullptr;
        }
    }
};


class Trie {
public:
    TrieNode* root;

    Trie() { root = new TrieNode(); }

    void insert(const string &word) {
        TrieNode* node = root;
        for(char c : word){
            int index = c - 'a';
            if(index < 0 || index >= ALPHABET_SIZE){
                continue;
            }
            if(node->children[index] == nullptr){
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
            node->count++;
        }
    }

    int count(const string &word){
        int ret = 0;
        TrieNode* node = root;
        for(char c : word){
            int index = c - 'a';
            node = node->children[index];
            ret += node->count;
        }
        return ret;
    }

    ~Trie() {
        function<void(TrieNode*)> deleteTrie = [&](TrieNode* node) {
            if(node == nullptr) return;
            for(int i = 0; i < ALPHABET_SIZE; ++i){
                if(node->children[i] != nullptr){
                    deleteTrie(node->children[i]);
                }
            }
            delete node;
        };
        deleteTrie(root);
    }
};


class Solution {
public:
    vector<int> sumPrefixScores(vector<string>& words) {
        Trie *trie = new Trie();
        for (string word : words){
            trie->insert(word);
        }
        vector<int> result;
        for (string word : words){
            result.push_back(trie->count(word));
        }
        return result;
    }
};

day212 2024-09-26

729. My Calendar I

You are implementing a program to use as your calendar. We can add a new event if adding the event will not cause a double booking.

A double booking happens when two events have some non-empty intersection (i.e., some moment is common to both events.).

The event can be represented as a pair of integers start and end that represents a booking on the half-open interval [start, end), the range of real numbers x such that start <= x < end.

Implement the MyCalendar class:

MyCalendar() Initializes the calendar object. boolean book(int start, int end) Returns true if the event can be added to the calendar successfully without causing a double booking. Otherwise, return false and do not add the event to the calendar.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/09266fkystmmNuG2.png

题解

本题涉及到区间问题,可以使用线段树。在熟悉线段树结构的情况下本题非常容易解决,只需查询book中出现的区间在线段树中查询得到的区间和是否为0即可知道该区间是否被覆盖过,如果为0说明区间完全没有被覆盖则可以预订,并更新对应区间的值,这里可以给区间所有值加一(在线段树中这个操作并不会真的将全部节点都加1,而是会存在懒操作,只要在覆盖这个区间的节点内加入标记记录了存在加1操作)。非0则区间可能被部分覆盖,即有冲突,则不能预订。

关键的部分在线段树的实现,这里我们使用动态开点的线段树并使用懒标记。

代码

 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
class MyCalendar {
private:
    struct Node {
        Node* left;
        Node* right;
        int val;
        int lazy;
        Node() : left(nullptr), right(nullptr), val(0), lazy(0) {}
    };

    Node* root;
    const int MAX_RANGE = 1e9;

    void pushDown(Node* node, int start, int end) {
        if (!node->left) node->left = new Node();
        if (!node->right) node->right = new Node();
        if (node->lazy) {
            int mid = start + (end - start) / 2;
            node->left->val += node->lazy;
            node->left->lazy += node->lazy;
            node->right->val += node->lazy;
            node->right->lazy += node->lazy;
            node->lazy = 0;
        }
    }

    int query(Node* node, int start, int end, int l, int r) {
        if (l <= start && end <= r) return node->val;
        pushDown(node, start, end);
        int mid = start + (end - start) / 2;
        int res = 0;
        if (l <= mid) res += query(node->left, start, mid, l, r);
        if (r > mid) res += query(node->right, mid + 1, end, l, r);
        return res;
    }

    void update(Node* node, int start, int end, int l, int r, int val) {
        if (l <= start && end <= r) {
            node->val += val;
            node->lazy += val;
            return;
        }
        pushDown(node, start, end);
        int mid = start + (end - start) / 2;
        if (l <= mid) update(node->left, start, mid, l, r, val);
        if (r > mid) update(node->right, mid + 1, end, l, r, val);
        node->val = max(node->left->val, node->right->val);
    }

public:
    MyCalendar() {
        root = new Node();
    }

    bool book(int start, int end) {
        if (query(root, 0, MAX_RANGE, start, end - 1) > 0) {
            return false;
        }
        update(root, 0, MAX_RANGE, start, end - 1, 1);
        return true;
    }
};

总结

本题也可以使用简单的二分法求解,将book中的每个区间加入到一个有序set中。对当前book区间在set中通过二分快速查找到第一个已经存在的区间的结束时间大于该区间的开始和结束时间(其余情况或者和该区间有重合,或者位于该区间前,不会影响该区间的插入)。再判断找到的这个区间的开始时间和当前book区间的结束时间的大小,如果小于则有覆盖,大于等于则无覆盖,说明当前book区间可以预订,将其插入到这个有序set中。代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MyCalendar {
    struct Node {
        int start, end;
        Node(int start, int end) : start(start), end(end) {}
        bool operator<(const Node& other) const { return other.start >= end; }
    };
    set<Node> s;

public:
    MyCalendar() {}

    bool book(int start, int end) {
        auto it = s.lower_bound({start, end});
        if (it != s.end() && it->start < end)
            return false;
        s.insert({start, end});
        return true;
    }
};

day213 2024-09-27

731. My Calendar II

You are implementing a program to use as your calendar. We can add a new event if adding the event will not cause a triple booking.

A triple booking happens when three events have some non-empty intersection (i.e., some moment is common to all the three events.).

The event can be represented as a pair of integers start and end that represents a booking on the half-open interval [start, end), the range of real numbers x such that start <= x < end.

Implement the MyCalendarTwo class:

MyCalendarTwo() Initializes the calendar object. boolean book(int start, int end) Returns true if the event can be added to the calendar successfully without causing a triple booking. Otherwise, return false and do not add the event to the calendar.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0927rcG0qD6TNhbk.png

题解

本题仍然可以使用线段树求解,我们思考线段树的作用是什么,线段树使用了额外的空间保存了不断二分的区间的状态,从而对于有些区间问题,我们只需要知道这个区间拥有的某些性质即足以解决问题,没必要了解区间内的全部细节。这种区间对应了整体思想,将某些部分当作一个整体,用整体性质去求解。经典的线段树保存的是区间内全部数字的和,对于本题,我们只需修改区间保存的状态就可复用线段树解题。

本题中我们需要知道的是某个时间被访问过几次,则可以记录每个时间被访问的次数,对于区间我们可以记录该区间内这个访问次数的最大值。因为某个时间区间被预订后,该区间的所有时间的访问次数都要加1,因此仍然可以先将这个加1的状态保留在顶层的节点,在需要时再向下探索并将状态传递下去。

则我们需要构造线段树,其query查询的是某个区间的最大值,update将区间所有节点均加1。遍历book数组,对于每个预订,判断对应区间的最大值是否等于2,等于2则不能预定,小于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
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
class MyCalendarTwo {
private:
    struct Node {
        Node* left;
        Node* right;
        int val;
        int lazy;
        Node() : left(nullptr), right(nullptr), val(0), lazy(0) {}
    };

    Node* root;
    const int MAX_RANGE = 1e9;

    void pushDown(Node* node, int start, int end) {
        if (!node->left) node->left = new Node();
        if (!node->right) node->right = new Node();
        if (node->lazy) {
            int mid = start + (end - start) / 2;
            node->left->val += node->lazy;
            node->left->lazy += node->lazy;
            node->right->val += node->lazy;
            node->right->lazy += node->lazy;
            node->lazy = 0;
        }
    }

    int query(Node* node, int start, int end, int l, int r) {
        if (l <= start && end <= r) return node->val;
        pushDown(node, start, end);
        int mid = start + (end - start) / 2;
        int res = 0;
        if (l <= mid) res = max(res,query(node->left, start, mid, l, r));
        if (r > mid) res = max(res,query(node->right, mid + 1, end, l, r));
        return res;
    }

    void update(Node* node, int start, int end, int l, int r, int val) {
        if (l <= start && end <= r) {
            node->val += val;
            node->lazy += val;
            return;
        }
        pushDown(node, start, end);
        int mid = start + (end - start) / 2;
        if (l <= mid) update(node->left, start, mid, l, r, val);
        if (r > mid) update(node->right, mid + 1, end, l, r, val);
        node->val = max(node->left->val, node->right->val);
    }

public:
    MyCalendarTwo() {
        root = new Node();
    }

    bool book(int start, int end) {
        if (query(root, 0, MAX_RANGE, start, end - 1) >=2 ) {
            return false;
        }
        update(root, 0, MAX_RANGE, start, end - 1, 1);
        return true;

    }
};

/**
 * Your MyCalendarTwo object will be instantiated and called as such:
 * MyCalendarTwo* obj = new MyCalendarTwo();
 * bool param_1 = obj->book(start,end);
 */

总结

当然本题仍然可以通过暴力或者二分求解,下面给出暴力的示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyCalendarTwo {
    vector<pair<int,int>> b;
    vector<pair<int,int>>db;
public:
    MyCalendarTwo() {

    }

    bool book(int start, int end) {

        for(pair<int,int> x: db){
            if(start<x.second && end>x.first) return false;
        }
        for(pair<int,int> x : b){
            if(start<x.second && end>x.first){
                db.push_back({max(start,x.first),min(end,x.second)});
            }
        }
        b.push_back({start,end});
        return true;

    }
};

day214 2024-09-28

641. Design Circular Deque

Design your implementation of the circular double-ended queue (deque).

Implement the MyCircularDeque class:

MyCircularDeque(int k) Initializes the deque with a maximum size of k. boolean insertFront() Adds an item at the front of Deque. Returns true if the operation is successful, or false otherwise. boolean insertLast() Adds an item at the rear of Deque. Returns true if the operation is successful, or false otherwise. boolean deleteFront() Deletes an item from the front of Deque. Returns true if the operation is successful, or false otherwise. boolean deleteLast() Deletes an item from the rear of Deque. Returns true if the operation is successful, or false otherwise. int getFront() Returns the front item from the Deque. Returns -1 if the deque is empty. int getRear() Returns the last item from Deque. Returns -1 if the deque is empty. boolean isEmpty() Returns true if the deque is empty, or false otherwise. boolean isFull() Returns true if the deque is full, or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0928P8VO6toCeM2T.png

题解

本题是中等难度题,但题目本身逻辑比较简单,要实现双端循环链表,只需记录下链表的首部和尾部位置信息,链表的当前大小和最大容量按要求实现即可。可以用数组也可以用链表,本题中我使用数组实现。

代码

 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

class MyCircularDeque {
private:
    vector<int> data;
    int front;
    int rear;
    int size;
    int capacity;

public:
    MyCircularDeque(int k) : data(k), front(0), rear(0), size(0), capacity(k) {}

    bool insertFront(int value) {
        if (isFull()) return false;
        data[front] = value;
        if(size == 0){
            rear = (front+1)%capacity;
        }
        front = (front - 1 + capacity) % capacity;
        size++;
        return true;
    }

    bool insertLast(int value) {
        if (isFull()) return false;
        data[rear] = value;
        if (size == 0){
            front = (rear - 1 + capacity) % capacity;
        }
        rear = (rear + 1) % capacity;
        size++;
        return true;
    }

    bool deleteFront() {
        if (isEmpty()) return false;
        front = (front + 1) % capacity;
        size--;
        return true;
    }

    bool deleteLast() {
        if (isEmpty()) return false;
        rear = (rear - 1 + capacity) % capacity;
        size--;
        return true;
    }

    int getFront() {
        if (isEmpty()) return -1;
        return data[(front+1)%capacity];
    }

    int getRear() {
        if (isEmpty()) return -1;
        return data[(rear - 1 + capacity) % capacity];
    }

    bool isEmpty() {
        return size == 0;
    }

    bool isFull() {
        return size == capacity;
    }
};

day215 2024-09-29

432. All O`one Data Structure

Design a data structure to store the strings’ count with the ability to return the strings with minimum and maximum counts.

Implement the AllOne class:

AllOne() Initializes the object of the data structure. inc(String key) Increments the count of the string key by 1. If key does not exist in the data structure, insert it with count 1. dec(String key) Decrements the count of the string key by 1. If the count of key is 0 after the decrement, remove it from the data structure. It is guaranteed that key exists in the data structure before the decrement. getMaxKey() Returns one of the keys with the maximal count. If no element exists, return an empty string “”. getMinKey() Returns one of the keys with the minimum count. If no element exists, return an empty string “”. Note that each function must run in O(1) average time complexity.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0929ecisQ42xBcws.png

题解

本题是一道难题,本题要快速获得出现频率最大的key和出现频率最小的key,就要将频率和对应的key记录下来。一方面我们要记录每个key和其对应的频率方便进行增减操作,即尽快完成inc和dec操作,另一方面我们也要记录不同频率和其对应的keys以便快速查找最大最小频率对应的key。记录每个key和其对应的频率可以通过map(可以使用unordered map,因为顺序不重要)实现,记录频率和其对应的key也可以通过map实现,区别在于因为一个频率可能对应多个字符串,则map需要将频率作为键,将一个字符串set作为值方便快速插入和删除该频率对应的字符串中的某个字符串。由于map默认是有序的,则找到最大值和最小值只需返回map的开头和结尾set中任一字符串即可。

在增加某个key时,如果该key不存在,则插入这个key并将频率设置为1,同时查询频率map,若频率map中不存在1这个key,则插入1作为key,并在对应的set中插入这个字符串。存在则直接插入字符串。若增加的key存在,则在频率map中找到以对应频率为key的set,删掉这个字符串,并找频率+1对应的set,将这个字符串插入。减少某个key类似。获取最大最小key直接找到频率map的begin和rbegin,即有序map的开头和结尾返回set中任一字符串即可。

注意如果将map和set的查询,删除,插入操作都视为均摊时间复杂度为O(1)的话,则本题我们实现的四个函数的时间复杂度可以视为O(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
class AllOne {
private:
    unordered_map<string, int> strmaps;
    map<int, set<string>> fre;
public:
    AllOne() {
    }

    void inc(string key) {
        // 如果 key 不存在,插入并设置频率为 1
        if (strmaps.find(key) == strmaps.end()) {
            strmaps[key] = 1;
            fre[1].insert(key);
        } else {
            int count = strmaps[key];
            strmaps[key]++;
            fre[count].erase(key);
            if (fre[count].empty()) {
                fre.erase(count);
            }
            fre[count + 1].insert(key);
        }
    }

    void dec(string key) {
        int count = strmaps[key];
        strmaps[key]--;
        fre[count].erase(key);

        if (fre[count].empty()) {
            fre.erase(count);
        }

        if (strmaps[key] == 0) {
            strmaps.erase(key);
        } else {
            fre[count - 1].insert(key);
        }
    }

    string getMaxKey() {
        if (fre.empty()) {
            return "";
        }
        return *(fre.rbegin()->second.begin());
    }

    string getMinKey() {
        if (fre.empty()) {
            return "";
        }
        return *(fre.begin()->second.begin());
    }
};

day216 2024-09-30

1381. Design a Stack With Increment Operation

Design a stack that supports increment operations on its elements.

Implement the CustomStack class:

CustomStack(int maxSize) Initializes the object with maxSize which is the maximum number of elements in the stack. void push(int x) Adds x to the top of the stack if the stack has not reached the maxSize. int pop() Pops and returns the top of the stack or -1 if the stack is empty. void inc(int k, int val) Increments the bottom k elements of the stack by val. If there are less than k elements in the stack, increment all the elements in the stack.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0930hE2q4SQyqQL7.png

题解

本题根据题目要求要构建一个可以同时修改从栈底开始向上几个元素的栈,如果用一个数组来实现栈的话,向数组末尾插入就是向栈顶插入,修改从栈底开始的几个值即为从数组头部开始遍历数组,修改对应的值。非常符合题目的要求。用一个变量记录当前栈的大小,在push和pop时先判断栈是否已空或已满。

除了可以使用vector,也可以使用一般的数组,定义一个数组指针,在构造函数中按需动态分配数组内存。

代码

 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
class CustomStack {
private:
    int count;
    int maxLen;
    vector<int> stackvector;
public:
    CustomStack(int maxSize) {
        count = 0;
        maxLen = maxSize;
    }

    void push(int x) {
        if (count < maxLen){
            stackvector.push_back(x);
            count++;
        }
    }

    int pop() {
        if (count>0){
            int result = stackvector[count-1];
            stackvector.pop_back();
            count--;
            return result;
        }
        return -1;
    }

    void increment(int k, int val) {
        for(int i=0;i<k&&i<count&&i<maxLen;i++){
            stackvector[i] += val;
        }
    }
};

/**
 * Your CustomStack object will be instantiated and called as such:
 * CustomStack* obj = new CustomStack(maxSize);
 * obj->push(x);
 * int param_2 = obj->pop();
 * obj->increment(k,val);
 */

day217 2024-10-01

1497. Check If Array Pairs Are Divisible by k

Given an array of integers arr of even length n and an integer k.

We want to divide the array into exactly n / 2 pairs such that the sum of each pair is divisible by k.

Return true If you can find a way to do that or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1001e0GB8YrcDSmd.png

题解

本题要将全部数字分为两两一组,并要求每组中的两个数组和可以被k整除。对于整除的问题,一般可以通过取模将余数相同的数字筛选出来,这些数字可视为拥有相同的“性质”,无需关心其原本的数字大小。想实现组合的两个数字可以被k整除,只需这两个数字对k取模后的余数和可以被k整除。则统计各个余数对应的数字个数,再按照余数和为k将对应的组组合(如k=5,则余数为1和余数为4的数字个数相同,则这两个组的数字可以两两配对使得其被k整除),看两个组的数字个数是否相同,全部相同且余数为0的组的个数为偶数,则能够组合成功,否则不能。

注意本题中数字可能为负数,则要考虑负数取模的情况。在c++和其他一些语言中,对负数取模,计算方式是先绝对值取模再根据原数字的符号添加符号。如-1%5=-1。为了让负数取模的结果也为正数,即数学中的取模方式,可以用(num%k+k)%k使得正负数均得到正数的模。

代码

 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 {
public:
    bool canArrange(vector<int>& arr, int k) {
        vector <vector<int>> remain(k);
        for (int num : arr){
            remain[(num%k+k)%k].push_back(num);
        }
        if (remain[0].size() % 2 == 1){
            return false;
        }
        if (k%2 == 1){
            for (int i=1; i<=k/2;i++){
                if (remain[i].size() != remain[k-i].size()){
                    return false;
                }
            }
        }else{
            for (int i=1; i<k/2;i++){
                if (remain[i].size() != remain[k-i].size()){
                    return false;
                }
            }
            if (remain[k/2].size()%2 == 1){
                return false;
            }
        }

        return true;
    }
};

day218 2024-10-02

1331. Rank Transform of an Array

Given an array of integers arr, replace each element with its rank.

The rank represents how large the element is. The rank has the following rules:

Rank is an integer starting from 1. The larger the element, the larger the rank. If two elements are equal, their rank must be the same. Rank should be as small as possible.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1002FuIQoMmox8I2.png

题解

本题rank就是数字在数组中所有数字中的位次,复制数组构造pair并排序用pair类型将数组中的数字和其对应的下标组合起来,对pair的数字排序后遍历有序数组,若数字和前一个数字相同则rank不变否则rank加1,将rank填入result数组pair中数字对应的下标位置。

代码

 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
class Solution {
public:
    vector<int> arrayRankTransform(vector<int>& arr) {
        vector<pair<int,int>> sorted_rank(arr.size());
        vector<int> result(arr.size());
        for(int i = 0; i < arr.size(); ++i) {
            sorted_rank[i] = {arr[i], i};
        }
        sort(sorted_rank.begin(), sorted_rank.end(), [](pair<int,int> p, pair<int, int> q){
           return p.first < q.first;
        });

        int rank_num = 0;
        for(int i = 0; i < result.size(); ++i) {
            int value = sorted_rank[i].first;
            int index = sorted_rank[i].second;
            if(i == 0 || sorted_rank[i-1].first != value) {
                ++rank_num;
            }
            result[index] = rank_num;

        }

        return result;
    }
};

day219 2024-10-03

1590. Make Sum Divisible by P

Given an array of positive integers nums, remove the smallest subarray (possibly empty) such that the sum of the remaining elements is divisible by p. It is not allowed to remove the whole array.

Return the length of the smallest subarray that you need to remove, or -1 if it’s impossible.

A subarray is defined as a contiguous block of elements in the array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1003OjeSw7Bf5SvI.png

题解

涉及此类和整除相关的题目,取余是基本步骤,取余的目的在于找到同余,余数相同的可视为具有某种相同的性质,数论中同余就是一种等价关系。

本题中对整个数组求和并取余后,余数即为应该删掉的子数组数字和对p取余的值,设为k。如何求得所有和对p取余余数为k的子数组呢。可以利用前缀和,如果到下标i的前缀数组和对p取余为m,到下标i+n的前缀数组和对p取余为(m+k)%p,则i到i+n之间的子数组对p取余得到的就应该是k。则我们可以将已经遍历过的前缀数组数组和取余的余数作为key,该余数对应的已遍历的最后一个下标作为value保存起来。这样遍历数组,对任一下标i我们知道其前缀和余数为m,再去遍历余数对应的下标map,找到(m-k)%p这个余数对应的下标。计算当前下标到找到的下标的距离,与保存的最小长度比较,取较小。再用当前下标更新余数对应的map。这样求和后遍历一遍数组即可得到结果。

代码

 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 Solution {
public:
    int minSubarray(vector<int> &nums, int p) {
        int n = nums.size();
        long totalrem = accumulate(nums.begin(), nums.end(), 0l) % p;
        if(totalrem == 0) {
            return 0;
        }
        unordered_map<int, int> presums;
        presums[0] = -1;
        long sum = 0;
        int minlen = INT_MAX;
        for(int i = 0; i < n; i++) {
            sum += nums[i];
            long rem = sum % p;
            long target = (((rem - totalrem) % p) + p) % p;
            if(presums.contains(target)) {
                minlen = min(i - presums[target], minlen);
            }
            presums[rem] = i;
        }
        return (minlen == INT_MAX or minlen == n) ? -1 : minlen;
    }
};

day220 2024-10-04

2491. Divide Players Into Teams of Equal Skill

You are given a positive integer array skill of even length n where skill[i] denotes the skill of the ith player. Divide the players into n / 2 teams of size 2 such that the total skill of each team is equal.

The chemistry of a team is equal to the product of the skills of the players on that team.

Return the sum of the chemistry of all the teams, or return -1 if there is no way to divide the players into teams such that the total skill of each team is equal.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1004CPuvIeBvG5B7.png

题解

本题要先计算出数组和再求出平均分配给每个team的skill为多少,记为k。统计所有skill值的个数放入map,从小到大遍历skill值并判断k-skill的个数是否与skill相同,相同则计算skill和k-skill的乘积与个数相乘,加和到最终结果中。继续遍历,不同则直接返回-1。

注意到本题中skill的值最大为1000,故统计频率可以直接用数组替代map,开辟一个长度为1001的整数数组,下标表示skill,值表示出现频率即可。

代码

 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
class Solution {
public:
    long long dividePlayers(vector<int>& skill) {
        int n = skill.size();
        long long sum = 0;
        vector<int> freq(1001, 0);

        // 计算总和并统计频率
        for (int sk : skill) {
            sum += sk;
            freq[sk]++;
        }

        // 检查是否可以平均分配
        if (sum % (n / 2) != 0) {
            return -1;
        }

        int targetSum = sum / (n / 2);
        long long chemistry = 0;

        for (int i = 1; i <= targetSum/2; i++) {
            if (freq[i] == 0){
                continue;
            }
            int complement = targetSum - i;
            if (complement < i) break;  // 避免重复检查

            if (i == complement) {
                if (freq[i] % 2 != 0) {
                    return -1;
                }
                chemistry += (long long)i * complement * (freq[i] / 2);
            } else {
                if (freq[i] != freq[complement]) {
                    return -1;
                }
                chemistry += (long long)i * complement * freq[i];
            }
        }

        return chemistry;
    }
};

day221 2024-10-05

567. Permutation in String

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise.

In other words, return true if one of s1’s permutations is the substring of s2.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1005o0z59CxviRXY.png

题解

本题先统计s1字符串中各个字母的个数。由于要寻找s2中某个子字符串是s1中字母的组合,则使用一个s1长度的滑动窗口在s2字符串上滑动并计数窗口内各个字母的个数,滑动过程中不断将窗口右侧的字母个数加一判断是否与s1匹配,左侧的减一判断是否与s1匹配。用一个变量记录当前窗口中已经匹配的字母的个数,每次滑动后判断匹配的字母个数是否达到26个,达到26个则直接返回true。

代码

 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
class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        if (s1.size() > s2.size()) {
            return false;
        }

        vector<int> count(26, 0);
        vector<int> window(26, 0);

        for (char c : s1) {
            count[c - 'a']++;
        }

        for (int i = 0; i < s1.size(); i++) {
            window[s2[i] - 'a']++;
        }

        int matched = 0;
        for (int i = 0; i < 26; i++) {
            if (window[i] == count[i]) {
                matched++;
            }
        }

        if (matched == 26) return true;

        // 滑动窗口
        for (int right = s1.size(); right < s2.size(); right++) {
            int left = right - s1.size();

            // 添加右边的新字符
            int r = s2[right] - 'a';
            window[r]++;
            if (window[r] == count[r]) matched++;
            else if (window[r] == count[r] + 1) matched--;

            // 移除左边的旧字符
            int l = s2[left] - 'a';
            window[l]--;
            if (window[l] == count[l]) matched++;
            else if (window[l] == count[l] - 1) matched--;

            if (matched == 26) return true;
        }

        return false;
    }
};

day222 2024-10-06

1813. Sentence Similarity III

You are given two strings sentence1 and sentence2, each representing a sentence composed of words. A sentence is a list of words that are separated by a single space with no leading or trailing spaces. Each word consists of only uppercase and lowercase English characters.

Two sentences s1 and s2 are considered similar if it is possible to insert an arbitrary sentence (possibly empty) inside one of these sentences such that the two sentences become equal. Note that the inserted sentence must be separated from existing words by spaces.

For example,

s1 = “Hello Jane” and s2 = “Hello my name is Jane” can be made equal by inserting “my name is” between “Hello” and “Jane” in s1. s1 = “Frog cool” and s2 = “Frogs are cool” are not similar, since although there is a sentence “s are” inserted into s1, it is not separated from “Frog” by a space. Given two sentences sentence1 and sentence2, return true if sentence1 and sentence2 are similar. Otherwise, return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1006vIkjQL7Rbg8x.png

题解

本题只能插入一个句子,则要在插入一个句子后将短句变为长句,短句拆分后的前后两部分应分别位于长句的首尾部分(可以拆为空句,这样就是短句完全是长句的开头部分或者结尾部分),则遍历两个句子,匹配开头部分,不匹配时继续向后遍历长句直到可以继续匹配,此时匹配的是短句的后半部分,注意可能存在短句的后半部分的的某部分被重复匹配的情况,即假设短句为aaab,长句可能为aaaaab,因此需要一个指针记录匹配短句后半部分时的起始位置,另外一个指针则进行正常的长短句匹配。

代码

 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
class Solution {
public:
    bool areSentencesSimilar(string sentence1, string sentence2) {
        vector<string> s1;
        vector<string> s2;
        istringstream iss1(sentence1);
        istringstream iss2(sentence2);
        string word;
        while(iss1 >> word){
            s1.push_back(word);
        }
        while(iss2 >> word){
            s2.push_back(word);
        }
        if (s1.size() > s2.size()){
            swap(s1,s2);
        }
        int start = 0;
        for (;start<s1.size();start++){
            if(s1[start] != s2[start]){
                break;
            }
        }
        int tail = start;
        if (tail == s1.size()){
            return true;
        }
        int s2ptr = start;
        // cout << tail << " "<<s2ptr<<" "<<s1.size() << s2.size();
        for(;s2ptr<s2.size();s2ptr++){
            if (tail >= s1.size()){
                tail = start;
            }
            if(s2[s2ptr] != s1[tail]){
                tail = start;
                if(s2[s2ptr] == s1[tail]){
                    tail++;
                }
            }else{
                tail++;
            }
        }
        if (tail == s1.size()){
            return true;
        }
        return false;
    }
};

总结

使用KMP算法应该还可以对字符串匹配的过程做进一步的优化,把每个单词视为kmp中的字母,即可使用同样的方案加速匹配,在这里就不继续改进了。

本题还可采用将短句视为一个双端队列,遍历短句时同时将短句头部和长句头部,短句尾部和长句尾部进行匹配,任意一端匹配成功则弹出一个单词,两端都不匹配的时候结束遍历,判断短句是否已经全部弹出即可得到结果,空则说明可以匹配上,非空则不能匹配上。

 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
class Solution {
public:
    bool areSentencesSimilar(string sentence1, string sentence2) {
        deque<string> v1, v2;
        stringstream ss(sentence1);
        string s;
        while(getline(ss,s,' ')){
            v1.push_back(s);
        }
        stringstream ss2(sentence2);
        while(getline(ss2,s,' ')){
            v2.push_back(s);
        }
        if(v1.size() > v2.size()) swap(v1,v2);
        while(v1.size()){
            if(v1.front() == v2.front()){
                v1.pop_front();
                v2.pop_front();
            } else if(v1.back() == v2.back()){
                v1.pop_back();
                v2.pop_back();
            } else {
                break;
            }
        }
        return v1.empty();
    }
};

day223 2024-10-07

2696. Minimum String Length After Removing substrings

You are given a string s consisting only of uppercase English letters.

You can apply some operations to this string where, in one operation, you can remove any occurrence of one of the substrings “AB” or “CD” from s.

Return the minimum possible length of the resulting string that you can obtain.

Note that the string concatenates after removing the substring and could produce new “AB” or “CD” substrings.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1007RamtD7kRdtwY.png

题解

本题和之前做过的一些删除子字符串的题目有些相似,做这个题的时候突然想到这种删除的过程有点像玩过的经典同色消除游戏“祖玛”。黄色的球前后都是绿色的球,消掉黄色的球后绿色的球聚在一起可以再次被消掉,这个过程需要保存“前面存在某种颜色的球”这一状态,同时只有某种颜色的球被消掉后才会暴露出更前面的某种颜色的球。这样一想栈结构已经浮出脑海了,本题使用栈保存这两个子字符串的前置状态(这里是字符串AB中的A和CD中的C)。如果继续遍历时的字符能与栈顶字母匹配则将栈顶字母出栈。一旦不匹配就清空栈,重新入栈。

代码

 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 {
public:
    int minLength(string s) {
        stack<char> pairs;
        int delelen = 0;
        for (char c : s){
            if (c == 'A' || c == 'C'){
                pairs.push(c);
                continue;
            }else if (c == 'D'){
                if (!pairs.empty() && pairs.top() == 'C'){
                    pairs.pop();
                    delelen += 2;
                    continue;
                }
            }else if(c == 'B'){
                if (!pairs.empty() && pairs.top() == 'A'){
                    pairs.pop();
                    delelen += 2;
                    continue;
                }
            }
            if (!pairs.empty()){
                stack<char> emptyStack;
                swap(emptyStack,pairs);
            }
        }
        return s.size()-delelen;
    }
};

day224 2024-10-08

1963. Minimum Number of Swaps to Make the String Balanced

You are given a 0-indexed string s of even length n. The string consists of exactly n / 2 opening brackets ‘[’ and n / 2 closing brackets ‘]’.

A string is called balanced if and only if:

It is the empty string, or It can be written as AB, where both A and B are balanced strings, or It can be written as [C], where C is a balanced string. You may swap the brackets at any two indices any number of times.

Return the minimum number of swaps to make s balanced.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1008vifTrvQbe7RI.png

题解

本题也是一道括号匹配问题,题面为通过调换顺序让字符串中所有中括号都是匹配的。本题明确说明左括号和右括号的数量相同,即最终一定可以全部匹配。括号匹配问题离不开栈结构,对于左括号直接入栈,遇到右括号且同时栈顶为左括号则匹配成功,将栈顶左括号出栈。若栈中没有可供匹配的左括号,遇到右括号则将右括号当作左括号入栈同时将“调换”变量+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
class Solution {
public:
    int minSwaps(string s) {
        int result = 0;
        int length = s.size();
        int stacklen = 0;
        for (int i=0;i<s.size();i++){
            if ((length-i-1) == result){
                break;
            }
            if (s[i] == '['){
                stacklen++;
            }else if (s[i] == ']'){
                if (stacklen > 0){
                    stacklen--;
                }else{
                    stacklen++;
                    result++;
                }
            }
        }
        return result;
    }
};

day225 2024-10-09

921. Minimum Add to Make Parentheses Valid

A parentheses string is valid if and only if:

It is the empty string, It can be written as AB (A concatenated with B), where A and B are valid strings, or It can be written as (A), where A is a valid string. You are given a parentheses string s. In one move, you can insert a parenthesis at any position of the string.

For example, if s = “()))”, you can insert an opening parenthesis to be “(()))” or a closing parenthesis to be “())))”. Return the minimum number of moves required to make s valid.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/10090ZydPlF1SeVw.png

题解

本题解法和昨天的题目类似,左括号可以放在栈中等待匹配,如果栈中为空且扫描到了右括号则必须要前面补充一个左括号与之匹配,此时要将添加括号的数量加1,最终将字符串全部扫描完成后,栈中剩余的左括号的数量即为无法匹配的左括号数量,需要添加相同数量的右括号来匹配,因此将添加括号数量加上该数量得到最终结果。同样,因为栈中保存的一直为左括号,因此无需真的构造一个栈并将左括号入栈,只需用一个变量标记当前栈中的数量即可。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
    int minAddToMakeValid(string s) {
        int num = 0;
        int result = 0;
        for (char c : s){
            if (c == '('){
                num++;
            }else if(c == ')'){
                if (num > 0){
                    num--;
                }else{
                    result++;
                }
            }
        }
        result += num;
        return result;
    }
};

day226 2024-10-10

962. Maximum Width Ramp

A ramp in an integer array nums is a pair (i, j) for which i < j and nums[i] <= nums[j]. The width of such a ramp is j - i.

Given an integer array nums, return the maximum width of a ramp in nums. If there is no ramp in nums, return 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1010CxODxExtAzxK.png

题解

本题先排序,排序的作用在于将乱序的数字整理为有序,有序的作用在于将数字之间的相对大小信息和位置关联起来,适用于当某个问题对于较大数求解得到结果后就无需对更小的数字求解时,像本题中要求nums[i]<=nums[j]则可以先找到比某个数字大的所有数字,再去比较数字下标之间的关系。在排序后,对于有序数组中下标k的之后的数字都大于k,只需比较k之后的数字在原始数组中的下标和k数字的原始下标之间的大小无需再考虑之前的数字。 如果每个数字都和其后面的数字比较下标需要n^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
28

class Solution {
public:
    int maxWidthRamp(vector<int>& nums) {
        int n = nums.size();
        vector<pair<int, int>> sorted(n);

        for (int i = 0; i < n; i++) {
            sorted[i] = {nums[i], i};
        }

        // 按值排序
        sort(sorted.begin(), sorted.end());

        int maxWidth = 0;
        int minIndex = n;  // 初始化为一个不可能的大索引

        // 遍历排序后的数组
        for (const auto& [value, index] : sorted) {
            // 更新最大宽度
            maxWidth = max(maxWidth, index - minIndex);
            // 更新最小索引
            minIndex = min(minIndex, index);
        }

        return maxWidth;
    }
};

总结

本题还可以使用单调栈,使用单调栈避免了排序,排序自身需要nlogn的时间复杂度,使用单调栈可以达到n的时间复杂度。单调栈需要正向遍历一遍数组,再反向遍历一遍数组。正向遍历数组时构造一个单调减的单调栈,仅保存单调减的数字和其对应的下标。在反向遍历时如果数字大于栈顶数字则弹出栈顶直到数字小于栈顶数字,弹出过程中不断计算栈顶下标和当前下标的差作为宽度。继续反向遍历重复上述过程。这样可以得到最大宽度的原因是对于反向遍历的下标k和k-1的两个数字。如果下标k的数字比栈顶数字x大。若k-1的数字也比x大,则k-1和x对应的下标s之间的距离k-1-s一定小于k-s。即如果反向遍历时两个数字对应的左侧的栈顶相同,不可能存在比最右端的数字到栈顶数字距离更大的距离存在。而单调栈从底到顶的下标是单调增的,因此如果弹出了栈顶,栈顶的下标位置只会向左移动,这样对于相同的右端,弹出栈顶只会使宽度增大。相当于第一次正向遍历保证了左端的单调性,第二次反向遍历则自带右端的单调性。这样遍历两遍数组即可得最终结果。

 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
class Solution {
public:
    int maxWidthRamp(vector<int>& nums) {
        int n = nums.size();
        stack<int> indicesStack;

        // Fill the stack with indices in increasing order of their values
        for (int i = 0; i < n; i++) {
            if (indicesStack.empty() || nums[indicesStack.top()] > nums[i]) {
                indicesStack.push(i);
            }
        }

        int maxWidth = 0;

        // Traverse the array from the end to the start
        for (int j = n - 1; j >= 0; j--) {
            while (!indicesStack.empty() &&
                   nums[indicesStack.top()] <= nums[j]) {
                maxWidth = max(maxWidth, j - indicesStack.top());
                // Pop the index since it's already processed
                indicesStack.pop();
            }
        }

        return maxWidth;
    }
};

day227 2024-10-11

1942. The Number of the Smallest Unoccupied Chair

There is a party where n friends numbered from 0 to n - 1 are attending. There is an infinite number of chairs in this party that are numbered from 0 to infinity. When a friend arrives at the party, they sit on the unoccupied chair with the smallest number.

For example, if chairs 0, 1, and 5 are occupied when a friend comes, they will sit on chair number 2. When a friend leaves the party, their chair becomes unoccupied at the moment they leave. If another friend arrives at that same moment, they can sit in that chair.

You are given a 0-indexed 2D integer array times where times[i] = [arrivali, leavingi], indicating the arrival and leaving times of the ith friend respectively, and an integer targetFriend. All arrival times are distinct.

Return the chair number that the friend numbered targetFriend will sit on.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1011YenBDfVA7f4P.png

题解

本题先根据arrival时间对times数组中的子数组进行排序,遍历有序的times数组,将当前空余的最小座位号的座位分配给当前朋友,同时向map中对应key为当前朋友离开时间的value数组插入分配给当前朋友的座位号。在遍历每一个新的arrival时间时,先从map中找到所有小于等于该时间的leaving对应的座位号数组。这些座位在该时间之前都处于空闲状态,因此将这些座位号加入到一个最小堆中。这样给新的朋友分配座位时只需从最小堆弹出顶部数字即为当前空余的最小座位号。这就解决了如何获得空余座位中最小座位号的问题。如果最小堆中没有任何座位号,那就将一个新的座位号分配给该朋友,同时更新当前的最大座位号。

代码

 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
class MinHeap {
private:
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;

public:
    void insert(int value) {
        pq.push(value);
    }

    int extractMin() {
        int minValue = pq.top();
        pq.pop();
        return minValue;
    }

    int peekMin() const {
        return pq.top();
    }

    bool isEmpty() const {
        return pq.empty();
    }

    int size() const {
        return pq.size();
    }
};

class Solution {
public:
    int smallestChair(vector<vector<int>>& times, int targetFriend) {
        int max_seat = 0;
        unordered_map<int, vector<int>> leave_map;
        MinHeap available_seats;

        // 为每个朋友添加索引,并按到达时间排序
        for (int i = 0; i < times.size(); i++) {
            times[i].push_back(i);
        }
        sort(times.begin(), times.end());

        int last_arrival = 0;
        for (const auto& time : times) {
            int arrival = time[0];
            int leaving = time[1];
            int friend_index = time[2];

            // 处理在上一个到达时间到当前到达时间之间离开的朋友
            for (int t = last_arrival + 1; t <= arrival; ++t) {
                if (leave_map.find(t) != leave_map.end()) {
                    for (int seat : leave_map[t]) {
                        available_seats.insert(seat);
                    }
                    leave_map.erase(t);
                }
            }

            int assigned_seat;
            if (!available_seats.isEmpty()) {
                assigned_seat = available_seats.extractMin();
            } else {
                assigned_seat = max_seat++;
            }

            // 如果是目标朋友,返回分配的座位
            if (friend_index == targetFriend) {
                return assigned_seat;
            }

            // 更新离开时间映射
            leave_map[leaving].push_back(assigned_seat);

            last_arrival = arrival;
        }

        return -1;
    }
};

day228 2024-10-12

2406. Divide Intervals Into Minimum Number of Groups

You are given a 2D integer array intervals where intervals[i] = [lefti, righti] represents the inclusive interval [lefti, righti].

You have to divide the intervals into one or more groups such that each interval is in exactly one group, and no two intervals that are in the same group intersect each other.

Return the minimum number of groups you need to make.

Two intervals intersect if there is at least one common number between them. For example, the intervals [1, 5] and [5, 8] intersect.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1012OJNNFVflIZH0.png

题解

本题先将intervals数组按left排序,排序的好处在于无序的数组遍历时信息过于丰富使得我们无法确定一些性质,而排序后信息含量减少更方便我们确定一些关系。直观来说,排序后left是有序的,因此我们不需要再考虑left和left之间的关系,只需考虑right以及left和right之间的关系。

其实信息量多还是少的定义是有一点反直觉的,比如一个乱序的句子和一个正常的通顺句子,哪个句子中的信息含量更高呢,其实是乱序的句子。因为乱序的句子其表达的意义有更多的可能性,这就意味着句子中每个单词都包含更丰富的信息。而通顺的句子其句子含义基本是可以确定的,去掉一两个字甚至也不影响我们理解句子的含义,这也意味着句子中有些字的包含的信息其实非常少。再比如文言文会比白话文难懂,正是因为文言文遣词造句更加精简,使得每个句子中包含的信息相对于白话文大大增加了,因此我们可能一下子很难理解这么多的信息,这就是“难懂”。

我之前一直默认有序包含了更多的“信息”,常常说排序是让我们有了更多可利用的信息,但从信息论的角度看,其实排序是让这个系统的信息更少,减少了干扰信息,从而使得我们可以更好的把握它的性质。这样看来我之前的说法其实是错误的,因此有必要在此纠正。

回到本题,在left有序后,只需考虑left和right之间的关系,遍历数组时将right保存起来,如果我们想将下一个interval和当前已经在某个组中的interval放在同一组中,则下一个interval的left应该大于当前某个组中最大的right。如果有多个组的最大right都符合条件,我们就应该使用贪心的思路,将该interval放入所有组中right最小的组内。由此,我们只需要知道当前所有组中最小的right是多少,与下一个interval的left比较,如果left>right,则放入该组并更新该组的right,否则创建一个新的组将这个interval单独放入一个组中。由于只需要最小的right,可以利用最小堆,将所有组的right都放入最小堆,每次返回堆顶即可

代码

 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
class MinHeap {
private:
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;

public:
    void insert(int value) {
        pq.push(value);
    }

    int extractMin() {
        int minValue = pq.top();
        pq.pop();
        return minValue;
    }

    int peekMin() const {
        return pq.top();
    }

    bool isEmpty() const {
        return pq.empty();
    }

    int size() const {
        return pq.size();
    }
};

class Solution {
public:
    int minGroups(vector<vector<int>>& intervals) {
        sort(intervals.begin(),intervals.end());
        int groups = 0;
        MinHeap minheap;
        minheap.insert(intervals[0][1]);
        groups++;
        for (int i=1;i<intervals.size();i++){
            int maxright = minheap.peekMin();
            if (intervals[i][0] > maxright){
                maxright = minheap.extractMin();
                minheap.insert(intervals[i][1]);
            }else{
                minheap.insert(intervals[i][1]);
                groups++;
            }
        }
        return groups;
    }
};

day229 2024-10-13

632. Smallest Range Covering Elements from K Lists

You have k lists of sorted integers in non-decreasing order. Find the smallest range that includes at least one number from each of the k lists.

We define the range [a, b] is smaller than range [c, d] if b - a < d - c or a < c if b - a == d - c.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/10139sb351OK8Rb2.png

题解

本题要找出能在每个数组中都包含至少一个数字的最小数字范围,则整体上可以先将所有数组内的数字打上标记标记其所在的数组,再将全部数组合并并排序,随后用滑动窗口,每当窗口内包含所有数组至少一个数字记录当前窗口的大小并与当前最小值比较,进行最小值的更新。

对上述思路中存在的问题进行一步步分析,打上标记可以构造一个新的结构体。排序可以将所有数组合并后使用快排,因为数组均为有序数组,也可以使用选择排序,每次遍历所有数组选择这些数组中最小的第一个数字。但直觉上我觉得快排可能更快一些。最后一个很重要的问题,如何快速判断窗口内是否满足所有数组都至少包含一个数字?可以使用带索引的最小堆(优先级队列)。用一个数组记录窗口中包含的每个数组中数字的个数,在最小堆中放入的是数字个数和对应的数组索引的组合,用数字个数来比较。

将(数字个数,数组索引)正常放入最小堆,但在弹出堆顶元素时,先根据所有判断这个元素是不是过时的,如果数字个数和数组中记录的数字个数相同则说明该个数有效,否则舍弃并继续弹出下一个数字。

使用滑动窗口时,若堆顶数字为0,说明窗口内缺少了某个数组的数字(不需要知道具体是哪个,知道缺了就够了),则扩大窗口,直到堆顶数字大于0。根据窗口左右两端数字更新范围最小值,缩小窗口,直到堆顶数字重新为0,再扩大窗口。直到遍历完所有数字。

代码

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class MinValueArray {
private:
    vector<int> arr;  // 存储实际的数组值
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    // 优先级队列存储 <值, 索引> 对,使用 greater 比较器使其成为最小堆

public:
    MinValueArray(int size) : arr(size, 0) {
        for (int i = 0; i < size; ++i) {
            pq.push({0, i});
        }
    }

    void update(int index, int value) {
        if (index < 0 || index >= arr.size()) {
            throw std::out_of_range("Index out of range");
        }
        arr[index] = value;
        pq.push({value, index});
    }

    void addValue(int index, int value) {
        if (index < 0 || index >= arr.size()) {
            throw std::out_of_range("Index out of range");
        }
        arr[index] += value;
        pq.push({arr[index], index});
    }

    int getMin() {
        while (!pq.empty()) {
            auto [value, index] = pq.top();
            if (value == arr[index]) {
                return value;  // 找到了当前的最小值
            }
            pq.pop();  // 这是一个过时的值,将其移除
        }
        throw std::runtime_error("Array is empty");
    }

};

struct Num {
    int num;
    int array;
};

class Solution {
public:
    vector<int> smallestRange(vector<vector<int>>& nums) {
        vector<Num*> bigarray;
        for (int i = 0; i < nums.size(); i++) {
            for (auto n : nums[i]) {
                Num* num = new Num;
                num->num = n;
                num->array = i;
                bigarray.push_back(num);
            }
        }
        sort(
            bigarray.begin(), bigarray.end(),
            [](const Num* a, const Num* b) -> bool { return a->num < b->num; });
        int left = 0;
        int right = 0;
        int result_left = 0;
        int result_right = 0;
        int minrange = 10e5+1;
        MinValueArray minheap(nums.size());
        while (right < bigarray.size()) {
            if (minheap.getMin() == 0) {
                // 如果最小值为0,说明还没有包含所有列表的元素
                minheap.addValue(bigarray[right]->array, 1);
                right++;
            } else {
                // 已经包含了所有列表的元素,尝试缩小范围
                while (minheap.getMin() > 0) {
                    int currentRange = bigarray[right - 1]->num - bigarray[left]->num;
                    if (currentRange < minrange) {
                        minrange = currentRange;
                        result_left = bigarray[left]->num;
                        result_right = bigarray[right - 1]->num;
                    }

                    // 移动左指针
                    minheap.addValue(bigarray[left]->array, -1);
                    left++;
                }
            }
        }

        // 处理最后一个窗口
        while (minheap.getMin() > 0) {
            int currentRange = bigarray[right - 1]->num - bigarray[left]->num;
            if (currentRange < minrange) {
                minrange = currentRange;
                result_left = bigarray[left]->num;
                result_right = bigarray[right - 1]->num;
            }
            minheap.addValue(bigarray[left]->array, -1);
            left++;
        }

        return {result_left, result_right};
    }
};

总结

我的解法有些复杂了,使用优先级队列其实没必要将子数组合并,直接从各个子数组中拿出第一个数放入优先级队列,记录当前队列中的最大值,弹出最小值来计算范围,并更新结果。弹出一个最小值后再从其所在的子数组头部继续放入一个值进入优先级队列,尝试更新最大值,计算新的范围,重复以上步骤直到某个子数组中所有数字都被使用完。这里主要利用了每个子数组都是有序的这一条件,这样每次取出数组头部数字即为当前数组中的最小值,也意味着与当前的数字范围最接近。

 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
class Solution {
public:
    vector<int> smallestRange(vector<vector<int>>& nums) {
        // Priority queue to store (value, list index, element index)
        priority_queue<pair<int, pair<int, int>>,
                       vector<pair<int, pair<int, int>>>, greater<>>
            pq;
        int maxVal = INT_MIN, rangeStart = 0, rangeEnd = INT_MAX;

        // Insert the first element from each list into the min-heap
        for (int i = 0; i < nums.size(); i++) {
            pq.push({nums[i][0], {i, 0}});
            maxVal = max(maxVal, nums[i][0]);
        }

        // Continue until we can't proceed further
        while (pq.size() == nums.size()) {
            auto [minVal, indices] = pq.top();
            pq.pop();
            int row = indices.first, col = indices.second;

            // Update the smallest range
            if (maxVal - minVal < rangeEnd - rangeStart) {
                rangeStart = minVal;
                rangeEnd = maxVal;
            }

            // If possible, add the next element from the same row to the heap
            if (col + 1 < nums[row].size()) {
                int nextVal = nums[row][col + 1];
                pq.push({nextVal, {row, col + 1}});
                maxVal = max(maxVal, nextVal);
            }
        }

        return {rangeStart, rangeEnd};
    }
};

day230 2024-10-14

2530. Maximal Score After Applying K Operations

You are given a 0-indexed integer array nums and an integer k. You have a starting score of 0.

In one operation:

choose an index i such that 0 <= i < nums.length, increase your score by nums[i], and replace nums[i] with ceil(nums[i] / 3). Return the maximum possible score you can attain after applying exactly k operations.

The ceiling function ceil(val) is the least integer greater than or equal to val.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1014Db9JNoNTFQDS.png

题解

本题可以使用贪心算法,每次都增加当前nums数组中的最大数字,增加后处理最大数字,再继续找到nums中的最大数字并增加。显然我们需要在一个不断变化的数组中一直返回其最大值。则最大堆符合要求,因此构造一个最大堆,将数组数字全部放进去,每次弹出堆顶数字并加到结果中,按照规则将该数字变换为1/3重新放入堆中。如此直到增加次数达到k。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
    long long maxKelements(vector<int>& nums, int k) {
        priority_queue<long long int> pq;
        for (auto num : nums){
            pq.push(num);
        }
        long long int result = 0;
        long long int temp;
        while(k>0){
            result += pq.top();
            temp = pq.top() / 3 + ((pq.top()%3)?1:0);
            pq.pop();
            pq.push(temp);
            k--;
        }
        return result;
    }
};

day231 2024-10-15

2938. Separate Black and White Balls

There are n balls on a table, each ball has a color black or white.

You are given a 0-indexed binary string s of length n, where 1 and 0 represent black and white balls, respectively.

In each step, you can choose two adjacent balls and swap them.

Return the minimum number of steps to group all the black balls to the right and all the white balls to the left.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/101581cgwXkBIJs6.png

题解

本题起初想到可以遍历一遍数组统计0的个数,再遍历数组将前面的1和后面的0交换直到到达0的个数的位置。但这样好像麻烦了一些,统计0的个数其实没有必要,直接使用双指针,分别从首尾开始遍历数组,尾指针遇到的0和首指针遇到的1交换,直到两个指针相遇即可。原因在于尾指针每次遇到0就将其和1交换,则尾指针指向位置之后的数组可以确保是全1的,同理首指针之前的数组可以确保是全0的。二者相遇时相遇位置之后的数组为全1,之前的为全0,就已经满足题目条件了。

遍历过程中,尾指针遇到0停下,移动首指针直到遇到1,计算二者距离并加和到结果中,继续移动尾指针直到头尾相遇结束。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
    long long minimumSteps(string s) {
        long long int head = 0;
        long long int tail = s.size()-1;
        long long int result = 0;
        while (tail > head){
            while(s[tail] != '0' && tail > head){
                tail--;
            }
            while(s[head] != '1' && head < tail){
                head++;
            }
            result += tail-head;
            tail--;
            head++;
        }
        return result;
    }
};

day232 2024-10-16

1405. Longest Happy String

A string s is called happy if it satisfies the following conditions:

s only contains the letters ‘a’, ‘b’, and ‘c’. s does not contain any of “aaa”, “bbb”, or “ccc” as a substring. s contains at most a occurrences of the letter ‘a’. s contains at most b occurrences of the letter ‘b’. s contains at most c occurrences of the letter ‘c’. Given three integers a, b, and c, return the longest possible happy string. If there are multiple longest happy strings, return any of them. If there is no such string, return the empty string “”.

A substring is a contiguous sequence of characters within a string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1016naxMGo18YVtN.png

题解

本题要求构造一个最长的happy字符串,可以使用给定数量的a,b,c来构造。唯一的限制是不能出现连续三个相同字符。则可以用贪心来解决。每次都给当前字符串添加上满足限制的当前剩余数量最多的字符,直到无法在满足限制的条件下添加任何字符或字符全部用光为止。

因为题中只有三个字符,则可以每次都给三个字符排序,排序后挨个遍历,找到满足条件的字符添加到字符串末尾。

当然用最大堆同样也是可以的,使用最大堆要先判断堆顶字符是否连续三个,若是连续三个,则临时弹出堆顶,用新的堆顶字符构造字符串,再将原来的堆顶恢复。若新的堆顶字符个数为0,则无法继续构造。

代码

 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 longestDiverseString(int a, int b, int c) {
        vector<pair<int, char>> chars = {{a, 'a'}, {b, 'b'}, {c, 'c'}};

        string result;
        while (true) {
            // 每次都重新排序三个字符
            sort(chars.begin(), chars.end(), greater<pair<int, char>>());

            bool added = false;
            for (auto& [count, ch] : chars) {
                if (count == 0) continue;  // 跳过数量为0的字符

                int n = result.size();
                if (n >= 2 && result[n-1] == ch && result[n-2] == ch) {
                    continue;  // 如果会造成三个连续相同字符,尝试下一个字符
                }

                result += ch;
                count--;
                added = true;
                break;
            }

            if (!added) break;  // 如果无法添加任何字符,结束循环
        }

        return result;
    }
};

day233 2024-10-17

670. Maximum Swap

You are given an integer num. You can swap two digits at most once to get the maximum valued number.

Return the maximum valued number you can get.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1017abzj4BaH6h48.png

题解

本题要想交换得到最大的数字,需要将前面的小数字与后面的大数字交换,要交换前面的小数字就需要后面有比它大的数字可供交换,交换时与后面的比它大的数字中最大的那个交换。如果最大的数字有多个,最与最后面的数字交换可以得到最大值(因为小的数字会被换到后面,换的位越低相对换到一个比较高的位数字整体就会更大)。则反向遍历数组,记录到当前数字的后面数字的最大值和对应位置,如果当前数字大于最大值则更新最大值和位置。再从头遍历数组,找到第一个后面最大值大于自己的数字并按照记录的位置执行交换即得最终结果。

代码

 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

class Solution {
public:
    int maximumSwap(int num) {
        struct backMax {
            int digit;
            int index;
        };

        string numstring = to_string(num);
        int len = numstring.length();
        vector<backMax> backVector(len);

        // 从后向前遍历,记录每个位置后面的最大数字及其索引
        int current = 0;
        int curindex = len - 1;
        for (int i = len - 1; i >= 0; i--) {
            if (numstring[i] - '0' > current) {
                current = numstring[i] - '0';
                curindex = i;
            }
            backVector[i] = {current, curindex};
        }

        // 从前向后遍历,找到第一个可以交换的位置
        for (int i = 0; i < len; i++) {
            if (backVector[i].digit > numstring[i] - '0') {
                // 交换数字
                swap(numstring[i], numstring[backVector[i].index]);
                break;  // 只交换一次
            }
        }

        // 将字符串转回整数
        return stoi(numstring);
    }
};

day224 2024-10-18

2044. Count Number of Maximum Bitwise-OR Subsets

Given an integer array nums, find the maximum possible bitwise OR of a subset of nums and return the number of different non-empty subsets with the maximum bitwise OR.

An array a is a subset of an array b if a can be obtained from b by deleting some (possibly zero) elements of b. Two subsets are considered different if the indices of the elements chosen are different.

The bitwise OR of an array a is equal to a[0] OR a[1] OR … OR a[a.length - 1] (0-indexed).

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1018evDL2tsFmo7a.png

题解

注意本题的位运算是or运算,因此两个数字or运算只会比两个数字都大。从二进制的角度来看,要得到最大值就是让二进制位1的个数最多,而或运算的特点是只要有一个数字在某一二进制位上为1,最终结果在该二进制位上就为1。因此可以根据哪个二进制位为1将数组中的数字分类。如第一个二进制位为1的数字为一组,第二个二进制位为1的数字为1组。最终要得到最大数字只需从各组中挑出一个数字组合即可。

但还存在数字重复的问题。首先考虑同一个数字在不同组的情况。如3同时拥有两个二进制位,可以同时放入两个组中。但通过一个简单例子如只有3,1两个数即可发现,可以直接从各个二进制位对应的组中选择数字组合,不需要去重。考虑3,1。3有两个二进制位,1有一个二进制位,因此第一个二进制位有两个数字(1,3),第二个二进制位有一个数字(3)。在组合的时候仍然直接2*1即得最终的组合个数。因为当从两个组中同时选择3时,相当于3自身这个组合。最终组合就是3和3,1共两个。

再考虑同一个数字在数组中多次出现的问题。如出现了3个2。此时可以在选择时先将其当作一个2处理,最后再将组合结果与重复的2的组合个数相乘。对于重复的n个数字其组合个数为2^n-1(排除空集)。则若目前根据二进制位可分为三个组,从三个组中分别选择一个数字后,如果选择的数字中包含2,就将这个组合个数乘2的组合数。

有了以上思路,则按二进制位分组后组合,枚举所有可能性并加和。

一通分析好像十分精彩,但看了看题目限制,数组长度最大16,数字大小却最大可以达到10^5。这种情况下用上面这种方式分组最差情况时间复杂度能达到n^10甚至更高。既然长度才16,那为何不直接算出能取得的最大值(所有数字做或运算)然后暴力枚举所有子数组的组合,能取到最大值的就计数。这不比上面说的一大堆简单多了,时间复杂度也就只有2^n。暴力枚举时还可以先将多个相同数字视为同一个,这样需要枚举的数目又减少了一部分。枚举可以使用递归加回溯的方式。可以做一些优化如当如果已经可以得到最大值时,后面未枚举的数字可以直接计算能得到的组合个数并加和。

则递归函数需要传递剩余的数字个数,当前已经遍历的部分可以取得的组合个数,以及剩余未遍历的数组。当遍历到剩余数字个数为0时返回。

代码

 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
class Solution {
public:
    int countMaxOrSubsets(vector<int>& nums) {
        // 计算所有元素的 Bitwise OR 的最大值
        int max_or = 0;
        for(auto num : nums){
            max_or |= num;
        }

        // 统计每个唯一元素的频率
        unordered_map<int, int> freq_map;
        for(auto num : nums){
            freq_map[num]++;
        }
        vector<pair<int, int>> uniq_nums;
        for(auto &[num, cnt] : freq_map){
            uniq_nums.emplace_back(make_pair(num, cnt));
        }

        // 递归枚举所有子集,处理重复元素
        long long total = 0; // 使用 long long 防止溢出
        backtrack(0, 0, uniq_nums, max_or, 1, total);
        return (int)total;
    }

private:
    void backtrack(int index, int current_or, const vector<pair<int, int>>& uniq_nums, int target_or, long long multiplier, long long& total){
        // 基本情况:所有唯一元素都已被考虑
        if(index == uniq_nums.size()){
            if(current_or == target_or){
                total += multiplier;
            }
            return;
        }

        // 获取当前唯一元素及其频率
        int num = uniq_nums[index].first;
        int count = uniq_nums[index].second;

        // 不包含当前元素
        backtrack(index + 1, current_or, uniq_nums, target_or, multiplier, total);

        // 包含当前元素
        long long ways = (1LL << count) - 1;
        int new_or = current_or | num;
        // 递归处理下一个唯一元素,并将组合数作为乘数传递
        backtrack(index + 1, new_or, uniq_nums, target_or, multiplier * ways, total);
    }
};

总结

这是优化了数字重复的情况,但实际上只纯粹的递归枚举所有组合,组合数的或运算和为目标和就将计数加一得到的代码非常简洁

 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

class Solution {
public:
    int maxOr;      // 用于存储所有元素的 Bitwise OR 的最大值
    int countMaxOr; // 用于统计达到 maxOr 的子集数量

    int countMaxOrSubsets(vector<int>& nums) {
        // 计算所有元素的 Bitwise OR 的最大值
        maxOr = 0;
        for(auto num : nums){
            maxOr |= num;
        }

        countMaxOr = 0;

        backtrack(nums, 0, 0);

        return countMaxOr;
    }

private:
    // 递归函数
    void backtrack(const vector<int>& nums, int index, int current_or){
        // 如果已经遍历完所有元素
        if(index == nums.size()){
            if(current_or == maxOr){
                countMaxOr++;
            }
            return;
        }

        // 选择不包含当前元素,继续递归下一个元素
        backtrack(nums, index + 1, current_or);

        // 选择包含当前元素,更新当前的 OR 值,继续递归下一个元素
        backtrack(nums, index + 1, current_or | nums[index]);
    }
};

而且在本题长度最多16的情况下,对可能存在多个重复数字情况的优化并没有效率上的提升,反而因为增加了更多的预处理和递归过程中的判断不如这个简单的方式来的快。因此任何优化都要结合具体场景,如果现在题目输入变为数组长度长得多但存在很多重复数字的情况,那么对重复数字的情况进行优化可能就会比单纯的直接枚举要快。

另外则是,尽管有时候我们思考过程中可能会出现一些错误的或不那么好的思路,但这些过程并非没有意义,只有经过了这样的思考,才能更加深入的理解问题出在哪里,明白什么样的问题用什么样的思想。

day225 2024-10-19

1545. Find Kth Bit in Nth Binary String

Given two positive integers n and k, the binary string Sn is formed as follows:

S1 = “0” Si = Si - 1 + “1” + reverse(invert(Si - 1)) for i > 1 Where + denotes the concatenation operation, reverse(x) returns the reversed string x, and invert(x) inverts all the bits in x (0 changes to 1 and 1 changes to 0).

For example, the first four strings in the above sequence are:

S1 = “0” S2 = “011” S3 = “0111001” S4 = “011100110110001” Return the kth bit in Sn. It is guaranteed that k is valid for the given n.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1019ARygswiVqFaP.png

题解

本题因为n最多只到20,所以理论上可以直接将S20的字符串是什么算出来,然后直接“查表”。对于k直接返回字符串的第k位的字符即可。

因为题目代码有提交长度限制,S20过长所以上面的方式不可行。故可以直接模拟构造过程构造出需要的字符串并取k位,但对于一个很小的k可能并不需要给定的n,故可以在构造过程中记录当前构造的字符串长度,当长度大于等于k时即停止构造并返回k位字符。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
public:
    char findKthBit(int n, int k) {
        int len = 1;
        string sn = "0";
        while (len < k){
            string rev;
            for (int i=len-1;i>=0;i--){
                rev.push_back(sn[i] ^ 1);
            }
            sn += "1"+rev;
            len = len*2 + 1;
        }
        return sn[k-1];
    }
};

总结

这时可以想到,构造字符串本身是一件非常耗时的任务,而我们其实只需要找到某一位字符是什么,其他字符都没用。那是否可以直接通过计算推断出某一位字符是什么呢?当然是可以的。我们思考构造字符串的过程,就是以1作为中间分割左右字符串互成与1异或后的镜像字符串。分割的1右侧的字符串是由左侧的字符串通过构造得来的,那么我们就可以直接将原本的落在右侧字符串某一位的k转换成左侧字符串的某一位(如001 和 011。则右侧的0对应左侧最后的1,右侧的两个1分别对应左侧的两个0。)则我们只需要知道到k的时候一共发生了几次转换即可得出最终k是什么。 给定n最终得到的字符串长度为2^n-1(找规律,sn = 2*s(n-1)+1)。则可以将字符串sn分为左右两部分,如果k的位置在sn的左半部分,则可退化到解决n-1,k的问题。如果在右半部分,则可以根据位置转换到左半部分并退化到解决n-1,new"k"的问题(新的k的位置根据字符串长度和原来的k计算出来),只是结果需要翻转一次。如果在正中间,那就是中间添加的字符"1"。如此将问题不断分解直到退化到n=1或者正巧遇到字符在中间即可得到解决。 解法为按照上面的思路不断将k和n减小,记录过程中k被翻转的次数。最终停止问题分解时将得到的原始字符翻转对应的次数(偶数不翻转,奇数翻转)即得最终结果。

 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
class Solution {
public:
    char findKthBit(int n, int k) {
        int len = (1 << n) - 1;
        int count = 0;
        char bit = '0';

        while (n > 1) {
            int mid = (len + 1) / 2;

            if (k == mid) {
                bit = '1';
                break;
            }

            if (k > mid) {
                k = len - k + 1;  // 映射到左半部分
                count++;  // 需要翻转
            }

            n--;
            len = (1 << n) - 1;
        }

        // 根据翻转次数决定是否需要翻转最终结果
        return (count % 2 == 0) ? bit : (bit == '0' ? '1' : '0');
    }
};

day226 2024-10-20

1106. Parsing A Boolean Expression

A boolean expression is an expression that evaluates to either true or false. It can be in one of the following shapes:

’t’ that evaluates to true. ‘f’ that evaluates to false. ‘!(subExpr)’ that evaluates to the logical NOT of the inner expression subExpr. ‘&(subExpr1, subExpr2, …, subExprn)’ that evaluates to the logical AND of the inner expressions subExpr1, subExpr2, …, subExprn where n >= 1. ‘|(subExpr1, subExpr2, …, subExprn)’ that evaluates to the logical OR of the inner expressions subExpr1, subExpr2, …, subExprn where n >= 1. Given a string expression that represents a boolean expression, return the evaluation of that expression.

It is guaranteed that the given expression is valid and follows the given rules.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1020ocXDWd19Qhxz.png

题解

本题是一道难题,其实就是一道编译原理中的简单语义分析的题目。解题结构就是将各种不同的表达式分别设计处理函数,并返回该表达式表示的布尔值。在读取字符串的过程中,根据当前读到的字符转入不同的表达式处理函数。 具体的表达式处理,以&表达式为例,当读取到的符号为&时,先读入一个左括号并丢掉,随后进入循环,只要没有读取到右括号就不断循环。循环时仍然根据读取的符号进入不同的表达式处理函数,如再次读到一个&则再次进入&表达式处理函数。处理完成得到该子表达式返回的布尔值,如果下一个字符是一个逗号,则移动指针指向逗号后面的字符,并进入下一次循环。 用指针可以避免字符串的复制,因为本题只需要解析表达式最终的表达值,所以任何复制都是多余的。 这里其实将词法分析的部分也嵌入到了分析当中,因为并没有什么变量之类的多字符,仅有限的几个字符,直接读取并处理。

代码

 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
class Solution {
public:
    int index = 0;  // 用于跟踪当前处理的字符位置

    bool parseBoolExpr(string expression) {
        return parseExpr(expression);
    }

private:
    bool parseExpr(const string& expr) {
        char c = expr[index];
        if (c == 't') {
            index++;
            return true;
        } else if (c == 'f') {
            index++;
            return false;
        } else if (c == '!') {
            return parseNot(expr);
        } else if (c == '&') {
            return parseAnd(expr);
        } else if (c == '|') {
            return parseOr(expr);
        }
        return false;
    }

    bool parseNot(const string& expr) {
        index += 2;  // 跳过 '!('
        bool result = !parseExpr(expr);
        index++;  // 跳过 ')'
        return result;
    }

    bool parseAnd(const string& expr) {
        index += 2;  // 跳过 '&('
        bool result = true;
        while (expr[index] != ')') {
            result &= parseExpr(expr);
            if (expr[index] == ',') index++;
        }
        index++;  // 跳过 ')'
        return result;
    }

    bool parseOr(const string& expr) {
        index += 2;  // 跳过 '|('
        bool result = false;
        while (expr[index] != ')') {
            result |= parseExpr(expr);
            if (expr[index] == ',') index++;
        }
        index++;  // 跳过 ')'
        return result;
    }
};

总结

本题如果熟悉编译原理相关的知识并做过一些简单的词法分析,语法分析和语义分析的手动实现,那么本题就不算一道难题了。

day227 2024-10-21

1593. Split a String Into the Max Number of Unique Substrings

Given a string s, return the maximum number of unique substrings that the given string can be split into.

You can split string s into any list of non-empty substrings, where the concatenation of the substrings forms the original string. However, you must split the substrings such that all of them are unique.

A substring is a contiguous sequence of characters within a string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1021vNMcHFiKmT1g.png

题解

本题最初会想到可以使用贪心+哈希表的方式解题。考虑题目要求最多能分割成多少个不一样的子字符串,则每次分割分割出一个尽可能短的字符串,在总长固定的情况下,就能得到尽可能多的字符串。但本题中局部最优并不一定是全局最优,这是因为我们分割时的遍历是从前向后遍历的,而能分割出数量最多的字符串可能将比较短的字符串放在中间位置才能得到。即分割出的比较短的字符串不一定总在整个字符串的前面。因此贪心不是总能得到最优解。

则可以考虑遍历所有的分割组合找到分割个数最多的组合。可以使用递归结合回溯的方式遍历所有的分割组合。递归函数中分割时从当前的起始位置开始,依次分割长度为1,2…的字符串,判断当前分割出来的字符串是否与之前分割的字符串有重复,没有则将分割出来的字符串放入set,同时从新的起始位置开始调用递归函数进行后面的分割,得到后面分割的字符串个数+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

class Solution {
public:
    int maxUniqueSplit(string s) {
        unordered_set<string> exist;
        return backtrack(s, 0, exist);
    }

private:
    int backtrack(const string& s, int start, unordered_set<string>& exist) {
        if (start == s.length()) {
            return 0;
        }

        int maxSplits = 0;
        for (int i = start; i < s.length(); i++) {
            string current = s.substr(start, i - start + 1);
            if (exist.find(current) == exist.end()) {
                exist.insert(current);
                int splits = 1 + backtrack(s, i + 1, exist);
                maxSplits = max(maxSplits, splits);
                exist.erase(current);
            }
        }

        return maxSplits;
    }
};

总结

从上面的过程也可以看出,有些分割是没必要继续进行的,如从头开始分割时分割的第一个字符串的长度即为整个字符串长度减一,这种情况必然只能得到两个字符串,同理分割长度为整个字符串长度减二时,整体分割完最多也只能得到三个字符串。那么什么时候可以剪枝呢,即当当前分割的字符串个数加上剩余的字符串长度小于等于当前分割个数的最大值时可以提前结束本次分割(剩余字符串长度表示将剩余字符串按每个单独字符进行分割得到的字符串个数,即剩余字符串能分割得到的最多个数)

 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
class Solution {
public:
    int maxUniqueSplit(string s) {
        unordered_set<string> seen;
        int maxCount = 0;
        backtrack(s, 0, seen, 0, maxCount);
        return maxCount;
    }

private:
    void backtrack(const string& s, int start, unordered_set<string>& seen,
                   int count, int& maxCount) {
        if (count + (s.size() - start) <= maxCount) return;

        if (start == s.size()) {
            maxCount = max(maxCount, count);
            return;
        }

        for (int end = start + 1; end <= s.size(); ++end) {
            string substring = s.substr(start, end - start);
            if (seen.find(substring) == seen.end()) {
                seen.insert(substring);
                backtrack(s, end, seen, count + 1, maxCount);
                seen.erase(substring);
            }
        }
    }
};

day228 2024-10-22

2583. Kth Largest Sum in a Binary Tree

You are given the root of a binary tree and a positive integer k.

The level sum in the tree is the sum of the values of the nodes that are on the same level.

Return the kth largest level sum in the tree (not necessarily distinct). If there are fewer than k levels in the tree, return -1.

Note that two nodes are on the same level if they have the same distance from the root.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/10229Q4LgL2N1L1b.png

题解

本题进行二叉树的层序遍历(BFS)对每一层求和并将其全部放入最大堆中,最终根据k将堆顶从堆中弹出k次即得第k大的和。

代码

 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
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    long long kthLargestLevelSum(TreeNode* root, int k) {
        priority_queue<long long int> heap;
        queue<TreeNode*> bfs;
        queue<TreeNode*> bfs2;
        bfs.push(root);
        TreeNode* front;
        while(!bfs.empty()){
            long long int layer = 0;
            while(!bfs.empty()){
                front = bfs.front();
                layer += front->val;
                if(front->left != nullptr){
                    bfs2.push(front->left);
                }
                if(front->right != nullptr){
                    bfs2.push(front->right);
                }
                bfs.pop();
            }
            heap.push(layer);
            swap(bfs,bfs2);
        }
        for(;k>1;k--){
            if(heap.empty()){
                return -1;
            }
            heap.pop();
        }
        if(!heap.empty()){
            return heap.top();
        }
        return -1;

    }
};

day229 2024-10-23

2641. Cousins in Binary Tree II

Given the root of a binary tree, replace the value of each node in the tree with the sum of all its cousins’ values.

Two nodes of a binary tree are cousins if they have the same depth with different parents.

Return the root of the modified tree.

Note that the depth of a node is the number of edges in the path from the root node to it.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1023VEjGON61Y5vJ.png

题解

本题不可能在一次遍历的时候就将节点的cousins的值计算出来并赋给节点。因为在遍历某一层的前面的节点时后面的节点还没有遍历到,不可能预知后面节点的值。因此可以遍历两次,第一次先将每一层的总和计算出来并保存(bfs,参考昨天题目的题解),后续遍历时只需用总和减去同一个父节点下的节点的和即得需要赋给节点的值。这里可以使用dfs,在父节点计算出子节点需要被赋的值并递归遍历子节点。

代码

 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
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<long long int> sum;
    void dfs(TreeNode* root, int targetvalue, int depth){
        root->val = targetvalue;
        if(depth == sum.size()){
            return;
        }
        int newtarget = sum[depth];
        if (root->left != nullptr){
            newtarget -= root->left->val;
        }
        if (root->right != nullptr){
            newtarget -= root->right->val;
        }
        if (root->left != nullptr){
            dfs(root->left, newtarget, depth+1);
        }
        if (root->right != nullptr){
            dfs(root->right, newtarget, depth+1);
        }
        return;
    }
    TreeNode* replaceValueInTree(TreeNode* root) {
        queue<TreeNode*> bfs;
        queue<TreeNode*> bfs2;
        bfs.push(root);
        TreeNode* front;
        while(!bfs.empty()){
            long long int layer = 0;
            while(!bfs.empty()){
                front = bfs.front();
                layer += front->val;
                if(front->left != nullptr){
                    bfs2.push(front->left);
                }
                if(front->right != nullptr){
                    bfs2.push(front->right);
                }
                bfs.pop();
            }
            sum.push_back(layer);
            swap(bfs,bfs2);
        }
        dfs(root,0,1);
        return root;

    }
};

day230 2024-10-24

951. Flip Equivalent Binary Trees

For a binary tree T, we can define a flip operation as follows: choose any node, and swap the left and right child subtrees.

A binary tree X is flip equivalent to a binary tree Y if and only if we can make X equal to Y after some number of flip operations.

Given the roots of two binary trees root1 and root2, return true if the two trees are flip equivalent or false otherwise.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1024M2CLc5WxLfOZ.png

题解

本题在使用dfs遍历树的基础上对dfs进行一些扩充,考虑如果两棵树是翻转等价的,则对于任何一个父节点,其两个子节点的值是一样的,只是值对应的左右子节点可能位置不同(一棵树是值1在左子节点,另一棵则是值1在右子节点),而在使用dfs遍历时无需考虑除当前节点左右子节点之外的节点,其余节点在递归时总会遍历到。

在dfs时,将两棵树当前节点的指针作为参数,只需判断当前两节点是否相等(包括都为空,或者一个为空一个不为空的情况),相等继续调用dfs判断左右子树是否相同或者为镜像翻转即可。即判断左树和左树相同,右树和右树相同(左右子树相同)或者左树和右树相同,右树和左树相同(左右子树翻转)即可。不相等直接返回false。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool flipEquiv(TreeNode* root1, TreeNode* root2) {
        if (root1 == nullptr && root2 == nullptr){
            return true;
        }
        if((root1 != nullptr && root2 == nullptr)||(root1==nullptr && root2 != nullptr)||(root1->val != root2->val)){
            return false;
        }
        return (flipEquiv(root1->left, root2->left) && flipEquiv(root1->right,root2->right)) || (flipEquiv(root1->right, root2->left) && flipEquiv(root1->left,root2->right));
    }
};

day231 2024-10-25

1233. Remove Sub-Folders from the Filesystem

Given a list of folders folder, return the folders after removing all sub-folders in those folders. You may return the answer in any order.

If a folder[i] is located within another folder[j], it is called a sub-folder of it. A sub-folder of folder[j] must start with folder[j], followed by a “/”. For example, “/a/b” is a sub-folder of “/a”, but “/b” is not a sub-folder of “/a/b/c”.

The format of a path is one or more concatenated strings of the form: ‘/’ followed by one or more lowercase English letters.

For example, “/leetcode” and “/leetcode/problems” are valid paths while an empty string and “/” are not.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1025zSXMNTpsIW7F.png

题解

本题使用前缀树可解,将文件夹的每一级的路径名视为前缀树中的一个字符串节点,对于遍历到的新的路径,如果路径已经在前缀树中存在,证明其上层文件夹路径已经存在,则该路径可以移除。不存在则在前缀树中创建一个新的路径。

代码

 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
class Solution {
public:
    vector<string> removeSubfolders(vector<string>& folder) {
        // 将文件夹路径排序,这样父文件夹会在子文件夹之前处理
        sort(folder.begin(), folder.end());
        TrieNode* root = new TrieNode();
        vector<string> result;

        for (const string& path : folder) {
            vector<string> components = split(path, '/');
            if (insert(root, components)) {
                // 如果插入成功(不是子文件夹),则将路径加入结果中
                result.push_back(path);
            }
        }


        return result;
    }

private:
    class TrieNode {
    public:
        unordered_map<string, TrieNode*> children;
        bool isEnd;
        TrieNode() : isEnd(false) {}
    };

    vector<string> split(const string& s, char delimiter) {
        vector<string> tokens;
        int i = 0;
        while (i < s.size()) {
            if (s[i] == delimiter) {
                i++;
            } else {
                int j = i;
                while (j < s.size() && s[j] != delimiter) j++;
                tokens.push_back(s.substr(i, j - i));
                i = j;
            }
        }
        return tokens;
    }

    // 插入路径到前缀树中,返回是否成功插入
    bool insert(TrieNode* root, const vector<string>& components) {
        TrieNode* node = root;
        for (const string& component : components) {
            if (node->isEnd) {
                // 当前节点是一个文件夹的结束,后续的是子文件夹,跳过
                return false;
            }
            if (node->children.find(component) == node->children.end()) {
                node->children[component] = new TrieNode();
            }
            node = node->children[component];
        }
        node->isEnd = true;
        return true;
    }


};

day232 2024-10-26

2458. Height of Binary Tree After Subtree Removal Queries

You are given the root of a binary tree with n nodes. Each node is assigned a unique value from 1 to n. You are also given an array queries of size m.

You have to perform m independent queries on the tree where in the ith query you do the following:

Remove the subtree rooted at the node with the value queries[i] from the tree. It is guaranteed that queries[i] will not be equal to the value of the root. Return an array answer of size m where answer[i] is the height of the tree after performing the ith query.

Note:

The queries are independent, so the tree returns to its initial state after each query. The height of a tree is the number of edges in the longest simple path from the root to some node in the tree.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1026ce2K07Jru8CR.png

题解

本题是一道难题,注意题目中明确说明各个节点的值不同,因此可以考虑通过某种方式将所有节点的一些性质保存下来,避免对树的重复遍历。在遍历过程中可以将到某个节点的高度(相当于从根节点到该节点的高度)保存下来传递给目标节点,同时对该节点极其子树进行遍历可以返回这棵子树的高度(相当于该节点到对应的树的分支的叶子节点即树底的高度),二者之和就是从根节点开始经过该子节点的子树的高度。那么对于树中同一层的所有节点都可以得到这个高度。

考虑树中同一层的节点,我们只需了解该层所有节点中对应的经过节点的所有树高度中的最大树高度和次大树高读(二者可以相同,表示经过两个不同节点均可得到最大树高度)即可确定每一个节点对应的子树如果被删除剩余树的最大高度。如果节点对应的高度为最大树高度,删掉以该节点为根的子树后整棵树的高度即为次大树高度,否则删掉节点为根对应的子树后整棵树的高度为最大树高度。

上述情况适用于同一层有多个节点的情况,同一层有多个节点时因为从根节点当该层所有节点的高度相同,所以删掉该层某个节点后整棵树的最大高度一定从其他节点对应的子树中取得,但当某一层只有一个节点时,删掉该节点及其子树会导致整棵树的高度改变,最大高度为根节点到该节点父节点的高度,即这个唯一节点上面的树的高度。因此要注意处理某一层只有一个节点的情况。

要得到树中经过某个节点的子树的高度,需要dfs,而要对树中每一层的节点进行分析确定删掉某个节点对应的子树后剩余的树高需要bfs,正是题目中明确说明节点值各不相同,因此可以先通过dfs得到各个节点对应的树高并保存,再通过bfs计算删掉某个节点后的剩余树高并保存。最终只需遍历queries数组直接根据被删除的节点值查询答案。

代码

 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
class Solution {
public:
    vector<int> treeQueries(TreeNode* root, vector<int>& queries) {
        vector<int> result(100001, 0);
        height.resize(100001, 0);
        int maxheight = dfs(root, 0);
        vector<TreeNode*> layer;
        vector<TreeNode*> layer2;
        layer.push_back(root);
        int currentDepth = 0;
        while (!layer.empty()) {
            vector<TreeNode*>().swap(layer2);
            int max1 = 0;
            int max2 = 0;
            for (auto node : layer) {
                if (height[node->val] > max1) {
                    max2 = max1;
                    max1 = height[node->val];
                } else if (height[node->val] > max2) {
                    max2 = height[node->val];
                }
                if (node->left != nullptr) {
                    layer2.push_back(node->left);
                }
                if (node->right != nullptr) {
                    layer2.push_back(node->right);
                }
            }
            for (auto node : layer) {
                if (layer.size() == 1) {
                    // 如果这一层只有一个节点,结果是上一层的高度
                    result[node->val] = currentDepth - 1;
                } else {
                    result[node->val] = height[node->val] == max1 ? max2 : max1;
                }
            }
            swap(layer, layer2);
            currentDepth++;
        }
        vector<int> returnvector;
        for (int node : queries) {
            returnvector.push_back(result[node]);
        }
        return returnvector;
    }

private:
    vector<int> height;

    int dfs(TreeNode* root, int depth) {
        if (root == nullptr) {
            return depth - 1;
        }
        int lefth = dfs(root->left, depth + 1);
        int righth = dfs(root->right, depth + 1);
        height[root->val] = max({lefth, righth, depth});
        return height[root->val];
    }
};

day233 2024-10-27

1277. Count Square Submatrices with All Ones

Given a m * n matrix of ones and zeros, return how many square submatrices have all ones.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1027QWifWPDPienP.png

题解

本题对于理解动态规划很有启发意义,数正方形这个任务让我们自己来做,一般会很快的将整个图中的所有大片正方形全部找到,然后依次计算包含的小正方形个数并加和,再计数落单的1并加和。

但这种解法显然是不能落实成算法来实现的,因为我们在“找”图中的所有大片正方形时处理的其实是整个图的全部信息,对图中全部数据都是并行化分析的,是通过一种“形”的思想来寻找这些正方形的。因此我们要考虑,一般而言,对于这样的图,设计算法来解决问题时必然要通过遍历图中数据(无论是按行还是按列)来获取某些信息从而解决问题,但这样的过程存在一个问题,即它不是“并行化”的,而是顺序执行的,这可能有些难理解,我们无论如何遍历数组,终究要从某个起点开始向后一个个遍历,这就意味着必然有一个信息获取的先后顺序,对某一时刻没遍历到的信息我们一无所知,而人脑不一样,我们在看图的时候相当于在一个时刻获取到了图中的全部信息,因此可以做出全局的判断。由此也可以明白为什么卷积要分块处理,其实就是模仿真实的人眼和人脑对图像的信息处理,但其实卷积仍然是一种“伪”并行,因为在每个卷积块内,用计算机来处理时仍然是通过遍历计算进行顺序处理的。

说的有点远,但可以帮助我们理解我们看待问题和通过算法解决问题的视角区别在哪,回归本题,既然用计算机算法无法一次获取“图”中的全部信息,那么至少我们可以通过某种方式将之前已经获取到的图中信息保存起来,这样就不需要再回去查找这些信息,一般而言我们在看待正方形时都会潜意识从左上到右下,因此对于寻找该图中的某个正方形究竟多大,可能也下意识的想固定一个左上的点,再分别向右向下遍历来找出这个正方形最大能有多大,但这样做存在的问题就是右和下都是未遍历过的区域,没有任何信息,因此需要我们自行遍历一遍,而继续移动时可能还要再遍历一些交叉的区域,这些区域有的我们访问过,有的没访问过,这样很难处理。

假如换个角度想想,好好思考下我们遍历这个矩阵的过程,在正常的按行遍历的情况下,我们遍历到的当前位置的左上区域是全部遍历过的,那么如果我们把当前位置看作正方形的右下角,则左上方全部信息都是已知的。想象一下遍历的过程,遍历到任意一个位置时其左上方到(0,0)构成的矩形内所有的点我们都遍历过。那么就可以通过一些保存的信息推知以当前坐标为右下角的正方形的边长最长为多少。

这时就要用到动态规划了,假设当前位置为(a,b)则若我们知道了(a,b-1)(即按照正常的行遍历时遍历到的前一个位置,因此是比较自然能想到的想法)作为右下坐标的正方形的最大边长,能否知道(a,b)的正方形最大边长。答案是不能,画图简单举个例子即可知道原因。如下所示,可知只有当(a,b-1),(a-1,b),(a-1,b-1)对应的正方形边长均为2时,再增加(a,b)对应的块后正方形边长才能达到3(缺少任意一个小正方形均会导致大正方形缺少某一部分)。则我们取(a,b-1),(a-1,b),(a-1,b-1)中的最小值,这个最小值即为三者对应正方形的最小值,也说明加上(a,b)后可以在该最小值的基础上获得边长大1的更大的正方形,对于正方形的总个数而言就是加上当前更大的正方形边长个数的正方形(如边长为3,则会增加计数边长为1,边长为2,边长为3的以当前坐标为正方形右下坐标的正方形)

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1027SWhMQlIMG_575F593D2E19-1.jpeg

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
public:
    int countSquares(vector<vector<int>>& matrix) {
        int ans = 0;
        for (int i = 0; i < matrix.size(); i++) {
            for (int j = 0; j < matrix[0].size(); j++) {
                if (i && j && matrix[i][j]) {
                    matrix[i][j] += min({matrix[i - 1][j - 1], matrix[i - 1][j], matrix[i][j - 1]});
                }
                ans += matrix[i][j];
            }
        }
        return ans;
    }
};

day234 2024-10-28

2501. Longest Square Streak in an Array

You are given an integer array nums. A subsequence of nums is called a square streak if:

The length of the subsequence is at least 2, and after sorting the subsequence, each element (except the first element) is the square of the previous number. Return the length of the longest square streak in nums, or return -1 if there is no square streak.

A subsequence is an array that can be derived from another array by deleting some or no elements without changing the order of the remaining elements.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/10283MKmcTHyw2Ig.png

题解

本题先排序,因为我们要找的其实也是一个有序子序列(后一个数均是前一个数的平方,当然比前面的数字更大),在乱序的情况下很难通过一次遍历找到这样的序列,乱序时可能会遇到一个很大的数还需要后面判断是否有它的因子和以它为因子的数,这样我们要处理更多的可能性,而有序我们只需在遍历时判断是否是以前遇到过的数字的平方。

这里可以使用一个队列用于存放遍历过的数字的平方数和该数字对应的子序列当前的长度,遍历有序数组,判断当前数字是否是队列头部的数字,如果是则将头部弹出并将当前序列长度加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
class Solution {
public:
    int longestSquareStreak(vector<int>& nums) {
        struct subseq{
            long long int val;
            int leng;
        };
        queue<subseq> seq;
        sort(nums.begin(),nums.end());
        int maxlen = 1;
        for (int num : nums){
            while(!seq.empty() && num>seq.front().val){
                seq.pop();
            }
            if (!seq.empty() && num==seq.front().val){
                seq.push(subseq{seq.front().val*seq.front().val,seq.front().leng+1});
                if (seq.front().leng+1 > maxlen){
                    maxlen = seq.front().leng+1;
                }
                seq.pop();
            }else if(!seq.empty() && num<seq.front().val){
                seq.push(subseq{long(num)*long(num),1});
            }else{
                seq.push(subseq{long(num)*long(num),1});
            }
        }
        return maxlen == 1?-1:maxlen;
    }
};

day235 2024-10-29

2684. Maximum Number of Moves in a Grid

You are given a 0-indexed m x n matrix grid consisting of positive integers.

You can start at any cell in the first column of the matrix, and traverse the grid in the following way:

From a cell (row, col), you can move to any of the cells: (row - 1, col + 1), (row, col + 1) and (row + 1, col + 1) such that the value of the cell you move to, should be strictly bigger than the value of the current cell. Return the maximum number of moves that you can perform.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1029VHe2DV4tTr5N.png

题解

本题每次移动时只能移动到自己后面一列的相邻三个方块内,而且必须移动到比当前方格内数字更大的方格中。则假如我们已知到当前方格的最长移动步数,再根据当前方格相邻方格的数字大小判断能否移动,将当前方格最长移动步数+1与相邻方格之前保存的最长步数比较即可知道相邻的几个方格的最长移动步数。在本题中,通过前一个方格的最长移动步数可以更新后面相邻方格的最长移动步数,因此大问题可以转换为从前一个方格移动到后面相邻方格过程中方格最长移动步数变化的小问题,这些小问题结构均相同。这样就可以通过循环,通过递推最终得到大问题的解。

具体而言,构造一个和矩阵大小相同的dp数组保存到某个方格的最长步数,因为题目限制起始方格只能在第一列,则先遍历第一列方格,找到能移动的相邻方格,并将dp数组中相邻方格位置处的步数设置为1。再遍历第二列,按照同样的方式更新dp数组中第三列的步数值,以此类推,在更新过程中将更新的步数与最大步数比较,不断更新最大步数,最终返回最大步数。

实现过程中要注意一个问题,即在遍历每一列时只有当前那些可以从前一列访问到的格子需要继续向后移动,而那些从前一列中无法访问到的格子是直接忽略的,因为题目要求起点必须从第一列开始,因此从第一列开始移动过程中所有不可达的格子都无需再访问,再访问就会导致从中间某一列的格子开始。同样基于这个原因,遍历过程中可以实现早停,如果某一列中所有格子在从第一列移动到该列时均已经无法访问,则可以结束遍历直接返回结果,因为不会再有从第一列起始的更长的路径了。

代码

 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
class Solution {
public:
    int maxMoves(vector<vector<int>>& grid) {
        vector<vector<int>> dp(grid.size(), vector<int>(grid[0].size(), -1));
        int maxmove = 0;
        for (int i = 0; i < grid.size(); i++) {
            dp[i][0] = 0;
        }
        for (int i=0;i<grid[0].size();i++){
            bool successmove = false;
            for (int j=0;j<grid.size();j++){
                for(int row=-1;row<=1;row++){
                    if (j+row>=0 && j+row<=grid.size()-1 && i+1<=grid[0].size()-1 && grid[j+row][i+1]>grid[j][i] && dp[j][i]!=-1){
                        dp[j+row][i+1] = max(dp[j+row][i+1], dp[j][i]+1);
                        maxmove = max(dp[j+row][i+1], maxmove);
                        successmove = true;
                    }
                }
            }
            if (!successmove){
                break;
            }
        }
        return maxmove;
    }
};

day236 2024-10-30

1671. Minimum Number of Removals to Make Mountain Array

You may recall that an array arr is a mountain array if and only if:

arr.length >= 3 There exists some index i (0-indexed) with 0 < i < arr.length - 1 such that: arr[0] < arr[1] < … < arr[i - 1] < arr[i] arr[i] > arr[i + 1] > … > arr[arr.length - 1] Given an integer array nums, return the minimum number of elements to remove to make nums a mountain array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1030CgyOV9qqLX1e.png

题解

如果做过接雨水这个经典题目,做这道题时可能会有一个相似的初始思路,即通过正向遍历将以下标i为结尾的递增最长递增子序列长度求出并保存,再反向遍历将以下标i起始的递减最长子序列长度求出并保存(相当于反向求递增最长子序列长度)。随后再遍历一次数组,将下标i对应的之前求出的两个长度加和再减一(i被重复计算)即得经过下标i的最长的“山峰”数组的长度,遍历一遍即得数组中最长的“山峰”长度,用总长度与该长度做差即得最终结果。

如何求出递增最长子序列,假如已知i之前的小于arr[i]的数字对应下标的递增最长子序列为下标j对应的子序列,则i对应的最长递增子序列等于j对应的子序列长度+1。但考虑i之前比arr[i]小的最靠近i的数字对应的子序列未必最长,而我们也无法确定哪个数字对应的子序列最长(考虑如下情况,如序列3,1,2,3,2,5。则在5之前对应的序列最长的应该为1,2,3长度为3。)因此需要遍历i之前所有小于arr[i]的数字和其对应的子序列长度,取其中最长的长度,将其+1作为i对应的最长子序列长度。

注意本题删除后得到的数组必须是“山峰”,因此对于到i仅单调增或者从i开始单调减的情况均不考虑。最终计算结果时要注意处理这种情况。

代码

 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
class Solution {
public:
    int minimumMountainRemovals(vector<int>& nums) {
        vector<int> normal(nums.size(),0);
        vector<int> reverse(nums.size(),0);
        for (int i=0;i<nums.size();i++){
            int maxl = 0;
            for(int j=0;j<i;j++){
                if (nums[j] < nums[i]){
                    maxl = max(maxl, normal[j]);
                }
            }
            normal[i] = maxl+1;
        }
        for (int i=nums.size()-1;i>=0;i--){
            int maxl = 0;
            for(int j=nums.size()-1;j>i;j--){
                if (nums[j]<nums[i]){
                    maxl = max(maxl, reverse[j]);
                }
            }
            reverse[i] = maxl+1;
        }
        int maxlen = 0;
        for (int i=0;i<nums.size();i++){
            // make i a mountain
            if (normal[i] > 1 && reverse[i] > 1 && normal[i]+reverse[i] > maxlen){
                maxlen = normal[i]+reverse[i];
            }
        }
        return nums.size()-maxlen+1;
    }
};

day237 2024-10-31

2463. Minimum Total Distance Traveled

There are some robots and factories on the X-axis. You are given an integer array robot where robot[i] is the position of the ith robot. You are also given a 2D integer array factory where factory[j] = [positionj, limitj] indicates that positionj is the position of the jth factory and that the jth factory can repair at most limitj robots.

The positions of each robot are unique. The positions of each factory are also unique. Note that a robot can be in the same position as a factory initially.

All the robots are initially broken; they keep moving in one direction. The direction could be the negative or the positive direction of the X-axis. When a robot reaches a factory that did not reach its limit, the factory repairs the robot, and it stops moving.

At any moment, you can set the initial direction of moving for some robot. Your target is to minimize the total distance traveled by all the robots.

Return the minimum total distance traveled by all the robots. The test cases are generated such that all the robots can be repaired.

Note that

All robots move at the same speed. If two robots move in the same direction, they will never collide. If two robots move in opposite directions and they meet at some point, they do not collide. They cross each other. If a robot passes by a factory that reached its limits, it crosses it as if it does not exist. If the robot moved from a position x to a position y, the distance it moved is |y - x|.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1031hTVL7LwAFYOv.png

题解

本题是一道难题,假如每个工厂能够维修的机器人个数是无限的,则本题可以直接通过贪心来解决,但本题中每个工厂能够维修的机器人个数是有限的,则此时的局部最优未必可以最终做到全局最优,因为要考虑工厂维修个数的限制,如一个机器人位于两个工厂1,2之间,尽管可能其离2更近,但若后面还有一个机器人离2同样近且2只能维修一个,那么可能将这个机器人派往1维修最终得到的全局结果是最佳的。

既然不知道对于每个工厂来说,维修几个机器人最终能够得到一个全局最优解,那就将所有的情况都保存下来,下一个工厂再根据上一个工厂的情况得到基于上一个工厂的最优情况。这样若知道了第n-1个工厂的全部情况,则相当于已经处理了全部n-1个工厂,就可以得到n个工厂的全部情况。

这里我们要考虑一个问题,以三个工厂为例,为什么处理了两个工厂的情况后,第三个工厂仅根据第二个工厂的情况就能得到全局的最优情况。假如每个工厂都只能维修两个机器人,三个工厂从左到右分别为1,2,3。机器人从左到右依次为a,b,c,d,e。则若工厂1维修了机器人a,此时工厂2要考虑的情况就是可以选择维修0个,1个,2个机器人,其选择维修的机器人必定从最左侧没有被维修的机器人(此时是b)开始。这里包含的隐藏信息就是,在将有限的机器人分配给左右两个工厂时,若分配的个数是固定的,则左侧的机器人选择左侧的工厂,右侧的机器人选择右侧的工厂得到的路径和一定比右侧的机器人选择左侧的工厂,左侧的机器人选择右侧的工厂近。这个性质和单调性有关,在两个序列保持单调的情况下,相同方向的匹配会得到更优的结果,这种性质也与重排不等式有几分相似。

基于此,将机器人和工厂排序,遍历工厂,根据前一个工厂的最优情况计算当前工厂在维修不同个数机器人情况下的最优情况,若工厂n维修不同个数机器人得到的最小总距离为数组dp,则工厂n+1维修机器人的情况为

$$

dp[n+1][j] = \min_{k=0}^j {dp[n][k] + \sum_{i=k+1}^j |location_robot[i] - location_fac[n+1]|}

$$

其中:

  • n+1 表示当前处理的工厂编号
  • j 表示需要修理的机器人总数
  • k 表示前n个工厂修理的机器人数量
  • \sum_{i=k+1}^j |location_robot[i] - location_fac[n+1]| 表示第n+1个工厂修理从第k+1到第j个机器人的总距离

但此时会发现,按照这样的思路,在计算的时候其实并不知道哪里是该工厂应该遍历的机器人的最左侧,因为只知道前一个工厂对维修不同个数机器人的最优情况,那么可以将第n个工厂维修前j个机器人的最小距离保存起来,每次都从第一个机器人开始遍历维修前n个工厂能维修的机器人总数的所有情况,得到该工厂能维修的前j+limit个机器人的所有情况并更新前j+limit个机器人被维修需要的最短距离。

最终取最后一个机器人被修好的最短总距离。

代码

 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:
    long long minimumTotalDistance(vector<int>& robot, vector<vector<int>>& factory) {
        int M = robot.size();
        int N = factory.size();
        sort(robot.begin(), robot.end());
        sort(factory.begin(), factory.end());
        int cum_limit = 0;

        // dp[j]: minimum total distance to repair first j robots
        vector<long long> dp(M+1, LLONG_MAX);
        dp[0] = 0;

        for(int n = 0; n < N; ++n) {
            int limit = factory[n][1];
            int pos = factory[n][0];
            int max_j = min(cum_limit, M);
            // We only need to consider up to all factory limits' sum or robots num
            vector<long long> new_dp(dp);
            for(int j = 0; j <= max_j; ++j) {
                if(dp[j] == LLONG_MAX) continue;
                long long cost = 0;
                // Try to assign up to 'limit' robots to current factory
                for(int k = 1; k <= limit && j + k <= M; ++k) {
                    cost += abs(robot[j + k - 1] - pos);
                    if(new_dp[j + k] > dp[j] + cost) {
                        new_dp[j + k] = dp[j] + cost;
                    }
                }
            }
            cum_limit += limit;
            dp = move(new_dp);
        }
        return dp[M];
    }
};

day238 2024-11-01

1957. Delete Characters to Make Fancy String

A fancy string is a string where no three consecutive characters are equal.

Given a string s, delete the minimum possible number of characters from s to make it fancy.

Return the final string after the deletion. It can be shown that the answer will always be unique.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1101eF6yDjgCcy2A.png

题解

本题是一道简单题只需每次遇到超过两个连续的相同字符时将该字符忽略。用一个变量记录当前字符,另一个变量记录字符重复的次数。若次数为2且新字符等于当前字符则忽略新字符,如此直到字符串末尾即构造得到新字符串。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
    string makeFancyString(string s) {
        string result;
        char current = s[0];
        int count = 0;
        for (auto ch : s){
            if (count == 2 && current == ch){
                continue;
            }else if(current == ch){
                count++;
            }else{
                current = ch;
                count = 1;
            }
            result.push_back(ch);
        }
        return result;
    }
};

day239 2024-11-02

2490. Circular Sentence

A sentence is a list of words that are separated by a single space with no leading or trailing spaces.

For example, “Hello World”, “HELLO”, “hello world hello world” are all sentences. Words consist of only uppercase and lowercase English letters. Uppercase and lowercase English letters are considered different.

A sentence is circular if:

The last character of a word is equal to the first character of the next word. The last character of the last word is equal to the first character of the first word. For example, “leetcode exercises sound delightful”, “eetcode”, “leetcode eats soul” are all circular sentences. However, “Leetcode is cool”, “happy Leetcode”, “Leetcode” and “I like Leetcode” are not circular sentences.

Given a string sentence, return true if it is circular. Otherwise, return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1102pGUOgMp8TmkY.png

题解

本题是一道简单题,先判断句子的首尾字符是否相同,不同直接返回false。再遍历句子,每当下一个字符为空格时,考虑到题目中明确说明单词和单词之间只有一个空格分隔,则判断当前字符和下下个字符是否相等,不相等直接返回false。遍历完成则返回true。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Solution {
public:
    bool isCircularSentence(string sentence) {
        if(sentence[0] != sentence[sentence.size()-1]){
            return false;
        }
        for (int i=0;i<sentence.size()-1;i++){
            if (sentence[i+1] == ' ' && sentence[i+2] != sentence[i]){
                return false;
            }
        }
        return true;
    }
};

day240 2024-11-03

796. Rotate String

Given two strings s and goal, return true if and only if s can become goal after some number of shifts on s.

A shift on s consists of moving the leftmost character of s to the rightmost position.

For example, if s = “abcde”, then it will be “bcdea” after one shift.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1103j7mfXJbRERz7.png

题解

本题是一道简单题,一个比较容易想到的思路是对于s,选定开头字母如字母a作为整个循环字符串的起始,随后遍历goal字符串,遇到字符a就将其当作s的开头字符a,向后遍历goal字符串并与s比对,成功则为true,失败则继续遍历goal寻找下一个a,直到遍历完整个goal为止。

代码

 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 Solution {
public:
    bool rotateString(string s, string goal) {
        if (s.size() != goal.size()){
            return false;
        }
        char label = s[0];
        for (int i=0;i<goal.size();i++){
            if (goal[i] == label){
                bool success = true;
                for (int j=0;j<s.size();j++){
                    if(s[j] != goal[(i+j)%s.size()]){
                        success = false;
                        break;
                    }
                }
                if (success){
                    return true;
                }
            }
        }
        return false;
    }
};

day241 2024-11-04

3163. String Compression III

Given a string word, compress it using the following algorithm:

Begin with an empty string comp. While word is not empty, use the following operation: Remove a maximum length prefix of word made of a single character c repeating at most 9 times. Append the length of the prefix followed by c to comp. Return the string comp.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1104fSYPQXvB2ERB.png

题解

本题读懂题意后其实并不难,就是一个单纯的字符串统计,从头遍历字符串,统计当前重复出现的字符个数,当个数达到9个时或者遇到新字符时就向comp字符串末尾添加当前重复字符的重复次数和字符本身。

代码

 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
class Solution {
public:
    string compressedString(string word) {
        char num = '0';
        char current = word[0];
        string result;
        for(auto ch:word){
            if (ch == current){
                if (num == '9'){
                    result.push_back(num);
                    result.push_back(current);
                    num = '1';
                    continue;
                }
                num++;
            }else if(ch != current){
                result.push_back(num);
                result.push_back(current);
                num = '1';
                current = ch;
            }
        }
        result.push_back(num);
        result.push_back(current);

        return result;
    }
};

day242 2024-11-05

2914. Minimum Number of Changes to Make Binary String Beautiful

You are given a 0-indexed binary string s having an even length.

A string is beautiful if it’s possible to partition it into one or more substrings such that:

Each substring has an even length. Each substring contains only 1’s or only 0’s. You can change any character in s to 0 or 1.

Return the minimum number of changes required to make the string s beautiful.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1105wvqV0TixLEvi.png

题解

本题可使用贪心,考虑到只要求每个分割出来的偶数长度字符串中只包含0或者1,且字符串的总长为偶数。则只需记录当前连续出现的字符是0还是1并记录该字符连续出现的次数,遇到另外一个字符时,如果当前出现的次数为奇数则将另外一个字符变为当前字符,将次数+1变为偶数,继续向后计数。若遇到另外一个字符时当前出现次数为偶数则改变当前记录字符并从头开始计数。如此反复直到达到字符串结尾。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
    int minChanges(string s) {
        int count = 0;
        char current = s[0];
        int result = 0;
        for (char ch : s){
            if (current == ch){
                count++;
            }else{
                if (count % 2 == 1){
                    result++;
                    count++;
                }else{
                    count = 1;
                    current = ch;
                }
            }
        }
        return result;
    }
};

day243 2024-11-06

3011. Find if Array Can Be Sorted

You are given a 0-indexed array of positive integers nums.

In one operation, you can swap any two adjacent elements if they have the same number of set bits . You are allowed to do this operation any number of times (including zero).

Return true if you can sort the array, else return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1106sIaxQDdMbk32.png

题解

本题要求从小到大排列数组,对于包含个数相同的1的连续数字,这些数字之间总可以通过不断的相邻交换最终从小到大排列。因此我们不必关心这些数字具体的排序方式,只需记录连续数字中值最大的那个,当遇到新的一组包含不同个数1的数字时只需该组数字中最小值大于前面组数字的最大值数组整体即可完成排序,若小于最大值则由于该数字包含的1的个数和前面的数字不同,该数字不可能与前面的数字交换位置,因此该数字不可能交换到数组的前面,此时就无法成功排序。

统计某个数字n中包含的二进制1的个数,可以使用n&(n-1),n&(n-1)每次可以消掉一个尾部所有0之前的第一个二进制1,通过不断的进行n&(n-1)的变换,记录变换的次数,当n为0时变换的次数即为该数字中包含的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
class Solution {
public:
    bool canSortArray(vector<int>& nums) {
        int currentbit = count(nums[0]);
        int lastmax = 0;
        int max = 0;
        int min = 0;
        int bit = 0;
        for (int num : nums){
            bit = count(num);
            if (currentbit == bit){
                if (num < lastmax){
                    return false;
                }
                if (num > max){
                    max = num;
                }
            }else{
                if (num < max){
                    return false;
                }else{
                    lastmax = max;
                    currentbit = bit;
                    max = num;
                }
            }
        }
        return true;
    }

    int count(int num){
        int result = 0;
        while(num != 0){
            num = num&(num-1);
            result++;
        }
        return result;
    }
};

day244 2024-11-07

2275. Largest Combination With Bitwise AND Greater Than Zero

The bitwise AND of an array nums is the bitwise AND of all integers in nums.

For example, for nums = [1, 5, 3], the bitwise AND is equal to 1 & 5 & 3 = 1. Also, for nums = [7], the bitwise AND is 7. You are given an array of positive integers candidates. Evaluate the bitwise AND of every combination of numbers of candidates. Each number in candidates may only be used once in each combination.

Return the size of the largest combination of candidates with a bitwise AND greater than 0.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1107BWOzOr4ex2ui.png

题解

本题要得到最长的按位与和大于0的数字组合的长度。这种涉及位运算的题目就要从二进制的角度来看,按位与的特点是参与运算的数字中只要有一个在第n位上为0,最终得到的结果在该位上就为0。因此要保证最终得到的结果不为0,至少要保证所有参与运算的数字在某一个相同的二进制位上均为1,则可构造一个数组表示某个二进制位上为1的数字个数。遍历candidates,对每个遍历的数字,将其所有为1的二进制位对应的数组中的数字加1,最终即可得到全部二进制位对应的为1的数字个数,取其中的最大值即得结果。

注意本题给定条件candidates中数字不大于10^7,用24位二进制位即可表示,可以构建一个长度位25的数组来表示25个不同二进制位上为1的数字个数。

代码

 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 largestCombination(vector<int>& candidates) {
        vector<int> count(25,0);
        for (int can : candidates){
            int i = 1;
            while(can > 0){
                if((can & 1) == 1){
                    count[i]++;
                }
                can = can >> 1;
                i++;
            }
        }
        int max = 0;
        for (int co : count){
            if (co > max){
                max = co;
            }
        }
        return max;
    }
};

day245 2024-11-08

1829. Maximum XOR for Each Query

You are given a sorted array nums of n non-negative integers and an integer maximumBit. You want to perform the following query n times:

Find a non-negative integer k < 2maximumBit such that nums[0] XOR nums[1] XOR … XOR nums[nums.length-1] XOR k is maximized. k is the answer to the ith query. Remove the last element from the current array nums. Return an array answer, where answer[i] is the answer to the ith query.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/11084I5skvlFd009.png

题解

本题仍然从二进制的角度来看,对于整个数组的异或和,若要再与一个有限位数(maximumBit)的二进制数异或后得到最大值,显然要使最终结果中的maximumBit均为1。根据异或的特性,假如当前数字是一个三位数x,在与一个数字y异或后得到三位二进制数的最大值111(7),则由x^y=111可知x^111=y,可以将其称为异或的“恢复性”,即能根据结果和其中一个参与运算的值恢复出另一个参与运算的值。

则本题同理,将数组异或的结果和maximumBit位数的最大值异或,即可得到使maximumBit位数最大的数,但注意数组异或的结果位数可能比maximumBit位数多,此时在和maximumBit最大值异或后应该取maximumBit位(可以通过与maximumBit位全1二进制数做按位与得到)作为最终的结果k。前面的位数因为k取不到故默认为0(可以保留这些位上的原始的数组异或值)。

每次算出一个k后,数组要删掉末尾的数字,此时不必再重新从头计算一遍数组的异或和,只需将以前的异或和与删掉的数字做异或即得剩余数字的异或和。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
public:
    vector<int> getMaximumXor(vector<int>& nums, int maximumBit) {
        int max = (1LL << maximumBit) - 1;
        int nxor = 0;
        for (int num : nums){
            nxor ^= num;
        }
        vector<int> result;
        for (int i=nums.size()-1;i>=0;i--){
            result.push_back(nxor ^ max & max);
            nxor ^= nums[i];
        }
        return result;
    }
};

day246 2024-11-09

3133. Minimum Array End

You are given two integers n and x. You have to construct an array of positive integers nums of size n where for every 0 <= i < n - 1, nums[i + 1] is greater than nums[i], and the result of the bitwise AND operation between all elements of nums is x.

Return the minimum possible value of nums[n - 1].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1109Jgwkq5zpvUym.png

题解

本题仍然是一道位运算相关的题目,要得到题目中的目标结果,必然要以x作为起始数字,因为按位与得到的结果不大于参与运算的数字,则若将x和小于x的数字做按位与,得到的结果必然小于x,不可能满足题目要求。考虑比x大的数字若要与x进行按位与后仍等于x,则该数字必须满足所有x中为1的二进制位也为1。则我们要得到的是从小到大排列的满足该条件的数字中第n大(包含x)的数字。考虑如何构造这样的数字,可以通过固定必须为1的二进制位,再去填充其余二进制位来得到这样的数字,因为我们只要得到第n大的数,不需要产生中间的数字,则直接将n-1(第n大包含x,去掉x还剩n-1个数)的二进制从低到高按位填入x从低到高的0二进制位中即得最终结果。如图

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1109Bf4UtQIMG_AC0B3274C5EB-1.jpeg

代码

 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 Solution {
public:
    long long minEnd(int n, int x) {
        long long int xlong = x;
        long long int bitadd = n-1;
        long long int copyx = xlong;
        int xcurrent = 0;
        int bitaddcurrent = 0;
        int shift = 0;
        while(bitadd > 0){
            xcurrent = copyx & 1;
            if (xcurrent == 0){
                bitaddcurrent = bitadd & 1;
                if (bitaddcurrent == 1){
                    xlong |= ((long long int)1 << shift);
                }
                bitadd = bitadd >> 1;
            }
            copyx = copyx >> 1;
            shift++;
        }
        return xlong;
    }
};

day247 2024-11-10

3097. Shortest Subarray With OR at Least K II

You are given an array nums of non-negative integers and an integer k.

An array is called special if the bitwise OR of all of its elements is at least k.

Return the length of the shortest special non-empty subarray of nums, or return -1 if no special subarray exists.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1110NmdAgo0xX2zT.png

题解

按位或运算的结果大于等于参与运算的数字,故可以使用滑动窗口,窗口扩大的终止条件为当前窗口内所有数字的按位或大于等于k。问题在于,如何缩小窗口,缩小窗口的终止条件是什么。

考虑对窗口左侧的数字,在我们只有整个窗口的按位或的结果和该数字的情况下,无法判断缺少该数字后新窗口的按位或的结果和原始窗口的按位或结果之间的关系,因为或运算对每个二进制位只要有一个数字为1最终结果中这个二进制位就为1,因此可以同时保存窗口内各个二进制位上出现1的次数,在缩小窗口时,对移出窗口的数字中所有为1的二进制位,从保存的次数中减1,若减1后该二进制位在该窗口中出现次数为0,则从或运算结果中减去该二进制位为1,其余位置为0对应的数(如1000即为减8),这样就成功处理了窗口左侧数字移出后的情况。缩小窗口的终止条件为窗口内的所有数字按位或的结果小于k。

按上述方式不断移动窗口,每当窗口内数字按位或的结果大于等于k,均将其和保存的当前的最小窗口长度比较并更新最小窗口长度,此处可以进行简单的剪枝,当最小窗口长度已经为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

class Solution {
public:
    int minimumSubarrayLength(vector<int>& nums, int k) {
        int n = nums.size();
        // 维护每个位上1的出现次数
        int bitCount[32] = {0};
        int currentOR = 0;
        int minLen = n + 1;
        int left = 0;

        for(int right = 0; right < n; ++right){
            if(nums[right] != 0){
                int num = nums[right];
                while(num > 0){
                    int bit = num & -num; // 获取最低位的1
                    int bitPos = __builtin_ctz(bit); // 计算该位的位置
                    bitCount[bitPos]++;
                    currentOR |= (1 << bitPos);
                    num -= bit;
                }
            }

            // 当currentOR >= k时,尝试缩小窗口
            while(currentOR >= k && left <= right){
                // 更新最小长度
                minLen = min(minLen, right - left + 1);

                // 如果已经找到最小长度,提前终止
                if(minLen == 1){
                    return 1;
                }

                if(nums[left] != 0){
                    int num = nums[left];
                    while(num > 0){
                        int bit = num & -num; // 获取最低位的1
                        int bitPos = __builtin_ctz(bit); // 计算该位的位置
                        bitCount[bitPos]--;
                        if(bitCount[bitPos] == 0){
                            currentOR &= ~(1 << bitPos);
                        }
                        num -= bit;
                    }
                }
                left++;
            }
        }

        return minLen <= n ? minLen : -1;
    }
};

day248 2024-11-11

2601. Prime Subtraction Operation

You are given a 0-indexed integer array nums of length n.

You can perform the following operation as many times as you want:

Pick an index i that you haven’t picked before, and pick a prime p strictly less than nums[i], then subtract p from nums[i]. Return true if you can make nums a strictly increasing array using the above operation and false otherwise.

A strictly increasing array is an array whose each element is strictly greater than its preceding element.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/11115kvCLNRF0iUh.png

题解

注意题目nums[i]的取值范围在1~1000之间,数字范围不大,则可先用筛法求出该范围内的所有素数备用。这里使用线性筛法。

考虑本题中要求最终得到的是严格递增的数组,则需要求出最后一部分符合严格递增的子数组的起始位置,对于末尾的这部分符合严格递增的子数组不需要做任何改动,因为本题中只能将数字减小,不能变大,而对这段严格递增子数组的起始数字,如果将其变得更小,则前面的数字可调整的范围就会变得更小,就不一定能够构造出符合题目要求的数组。

在找到起始位置后,设该位置的数字为p,对在该位置之前的子数组,可从头遍历并使用贪心算法结合二分搜索,贪心是指每次都将数字变得尽可能小,假设下标为k的数字为n,遍历到下标k+1时数字应该大于n,假设k+1处的数字当前为m,则我们要找的即为小于m-n的最大素数。二分搜索用于在求出的全部素数中快速找到满足该要求的素数,如果找不到则说明不可能构造出满足题目要求的数组。同时在每次找到这样的素数后,得到的下标k+1处对应的新数字应严格小于p,如果不小于p则由于要构造严格递增的数组,后面的数字只会大于p,由于最后一部分严格递增的子数组不能变动,因此前面若有数字大于p最终无法构造出严格递增的数组,这两种情况均返回false,正常遍历到数字p处则返回true。

代码

 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 {
public:
    bool primeSubOperation(std::vector<int>& nums) {
        // 筛法求素数
        vector<int> primes = linearSieve(1000);

        int n = nums.size();
        int start_suffix = n - 1;
        while(start_suffix > 0 && nums[start_suffix-1] < nums[start_suffix]){
            start_suffix--;
        }

        // 整个数组本来就递增直接返回
        if(start_suffix == 0){
            return true;
        }

        int p = nums[start_suffix];

        int last = 0;
        for(int i = 0; i < start_suffix; ++i){
            int current = nums[i];

            int upper = nums[i] - last ;
            int idx = lower_bound(primes.begin(), primes.end(), upper) - primes.begin();
            // 满足条件m-n的最大素数下标为idx-1
            int chosen_prime = 0;
            if(idx != 0 ){
                chosen_prime = primes[idx - 1];
            }
            int new_num = nums[i] - chosen_prime;
            if(new_num <= last || new_num >= p){
                return false;
            }
            last = new_num;
        }
        return true;
    }

private:
    vector<int> linearSieve(int max){
        vector<int> primes;
        vector<bool> is_prime(max + 1, true);
        is_prime[0] = is_prime[1] = false;
        for(int i = 2; i <= max; ++i){
            if(is_prime[i]){
                primes.push_back(i);
            }
            for(auto p : primes){
                if(p * i > max){
                    break;
                }
                is_prime[p * i] = false;
                if(i % p == 0){
                    break;
                }
            }
        }
        return primes;
    }
};

总结

在查看他人代码时发现,可以直接逆序构造符合题目要求的数组,即让下标k的数字小于k+1,如果本来就满足这一条件则继续逆序遍历,不满足则将下标k的数字减去一个素数使其满足条件,这里减去能满足条件的最小素数即可,可以直接从头遍历素数,直到找到能满足条件的素数即减去该素数。

 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
class Solution {
public:
    bool primeSubOperation(vector<int>& nums) {
        vector<int> prime(1000,true);
        for(int i=2;i *i<1000;i++)
        {
            if(prime[i])
            {
                for(int j=2;j*i<1000;j++)
                {
                    prime[j*i]=false;
                }
            }
        }
        int n=nums.size();
        for(int i=n-2;i>=0;i--)
        {
            if(nums[i]<nums[i+1])
            continue;
            else
            {
                bool flag=true;
                for(int j=2;j<nums[i];j++)
                {
                    if(prime[j] && (nums[i]-j)<nums[i+1])
                    {
                        nums[i]=nums[i]-j;
                        flag=false;
                        break;
                    }
                }
                if(flag)
                return false;

            }
        }
        return true;
    }
};

day249 2024-11-12

2070. Most Beautiful Item for Each Query

You are given a 2D integer array items where items[i] = [pricei, beautyi] denotes the price and beauty of an item respectively.

You are also given a 0-indexed integer array queries. For each queries[j], you want to determine the maximum beauty of an item whose price is less than or equal to queries[j]. If no such item exists, then the answer to this query is 0.

Return an array answer of the same length as queries where answer[j] is the answer to the jth query.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1112gEj9X4Z6RGNF.png

题解

本题要求对每个query,不大于该query的price能得到的最大beauty是多少。则一定需要在items中查找满足不大于query的price最大是多少,查找无疑使用经典的二分查找,二分查找需要数组是有序的,因此需要给items排序,排序后,考虑items中存在相同price对应不同的beauty,并且有可能更小的price却能得到更大的beauty,故遍历一遍有序items在过滤掉重复的price的同时,将每个price对应的beauty设置为不大于该price的所有price中能得到的beauty的最大值。再根据query的值对price二分查找,找到不大于query的最大price,其对应的beauty即为该query对应的beauty。

代码

 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
class Solution {
public:
    vector<int> maximumBeauty(vector<vector<int>>& items, vector<int>& queries) {
        sort(items.begin(), items.end());

        vector<vector<int>> newitems;
        int lastprice = 0;
        int maxbeauty = 0;

        for (auto &item : items) {
            if (item[0] != lastprice) {
                newitems.emplace_back(item);
                lastprice = item[0];
            }
            if (maxbeauty < item[1]) {
                maxbeauty = item[1];
            }
            newitems.back()[1] = maxbeauty;
        }

        vector<int> sols;
        sols.reserve(queries.size());

        for (int query : queries) {
            // 手动实现二分查找,找到第一个价格大于查询值的位置
            int left = 0;
            int right = newitems.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (newitems[mid][0] <= query) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }

            if (left == 0) {
                sols.push_back(0);
            } else {
                sols.push_back(newitems[left - 1][1]);
            }
        }

        return sols;
    }
};

总结

其实过滤重复的price对本题影响不大,即使有重复的price也不影响二分查找最终找到的最大的满足条件的price和其对应的最大beauty,因此可以直接修改有序items数组,仅修改每个item的beauty为不大于该price的最大beauty即可

 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
class Solution {
public:
    vector<int> maximumBeauty(vector<vector<int>>& items, vector<int>& queries) {
        sort(items.begin(), items.end());

        int maxbeauty = 0;
        for (auto &item : items) {
            if (maxbeauty < item[1]) {
                maxbeauty = item[1];
            }
            item[1] = maxbeauty;
        }

        vector<int> sols;

        for (int query : queries) {
            // 手动实现二分查找,找到第一个价格大于查询值的位置
            int left = 0;
            int right = items.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (items[mid][0] <= query) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }

            if (left == 0) {
                sols.push_back(0);
            } else {
                sols.push_back(items[left - 1][1]);
            }
        }

        return sols;
    }
};

day250 2024-11-13

2563. Count the Number of Fair Pairs

Given a 0-indexed integer array nums of size n and two integers lower and upper, return the number of fair pairs.

A pair (i, j) is fair if:

0 <= i < j < n, and lower <= nums[i] + nums[j] <= upper

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1113OojXLKHEvict.png

题解

本题要找满足两个数的和在某个范围内且下标满足题目要求的组合有多少对,要找到和在某个范围内的两个数字,在知道其中一个数字的情况下即可知道另外一个数字的取值范围,若要能根据数字的取值范围确定数字在数组中的范围,则必须是有序数组,因此第一步先将数组排序。

在将数组排序后,遍历有序数组,对每个数字知道了其另一个匹配数字的左右边界,通过二分法找出满足左右边界的有序数组中的下标,再求出这个边界内的数组长度并加和到最终结果中。至于题目中的原始数组的下标条件其实对本题并没有什么影响,因为只要两个数字的和满足范围条件,则二者必有一个数字在前另一个数字在后。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
    long long countFairPairs(vector<int>& nums, int lower, int upper) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        long long count = 0;

        for(int i = 0; i < n - 1; ++i){
            long long min_val = static_cast<long long>(lower) - nums[i];
            long long max_val = static_cast<long long>(upper) - nums[i];

            int left = lower_bound(nums.begin() + i + 1, nums.end(), (int)min_val) - nums.begin();


            int right = upper_bound(nums.begin() + i + 1, nums.end(), (int)max_val) - nums.begin();

            count += (right - left);
        }

        return count;
    }
};

day251 2024-11-14

2064. Minimized Maximum of Products Distributed to Any Store

You are given an integer n indicating there are n specialty retail stores. There are m product types of varying amounts, which are given as a 0-indexed integer array quantities, where quantities[i] represents the number of products of the ith product type.

You need to distribute all products to the retail stores following these rules:

A store can only be given at most one product type but can be given any amount of it. After distribution, each store will have been given some number of products (possibly 0). Let x represent the maximum number of products given to any store. You want x to be as small as possible, i.e., you want to minimize the maximum number of products that are given to any store. Return the minimum possible x.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1114BskX5PnrXb1r.png

题解

本题初始可能会想到将产品的数量加和后按照商店个数均分,让每个商店分配到的产品个数尽可能接近平均数,这样就可以最小化单个商店可能分得的最大产品个数,但这样分配未必能保证每个商店都能分到产品,抑或在给每个商店分配完接近平均数的产品后产品仍有剩余(有的产品个数远小于平均数但每个商店只能分得一个产品)。

但这样的思路有其道理,其中可取的地方在于我们需要找到一个数字p,使得每个商店分配得到的产品数量都不大于p,同时能够将产品最终分配完,但p不一定是之前考虑的平均数,而且p肯定有多个,因为当p已经能够让商店完成产品分配时,比p大的数肯定同样可以。我们需要找到的是最小的p。

验证当每个商店分配的产品数量不大于p时能否分配完成比较简单,对每个产品,当个数大于p时,将p个分配给一个商店,否则将剩余全部产品分配给一个商店。若到最后产品能正好分配给全部商店没有剩余则分配成功。

则此时可以想到,要找的最小的p有如下的特性,大于p的产品个数限制可以让产品分配给商店,小于p则不行,p是二者的交界,则这其实就变成了一个查找问题,考虑一般的在有序数组中查找某个具体的数,其实也隐含了类似的性质,即该数字右侧的数都大于该数字,该数字左侧的数都小于该数字,这个数字本身是与它的相对大小的分界。因此这类有二分性质的问题都可以考虑用二分查找来解决。

代码

 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:
    // 检查是否能在每个商店最多分配 mid 个产品的情况下完成分配
    bool canDistribute(int n, vector<int>& quantities, int mid) {
        int stores_needed = 0;
        for (int q : quantities) {
            stores_needed += (q + mid - 1) / mid;
        }
        return stores_needed <= n;
    }

    int minimizedMaximum(int n, vector<int>& quantities) {
        // 二分查找的左边界是1(每个商店至少能分配1个产品)
        int left = 1;
        // 右边界是单个产品的最大数量(因为最差情况下,最大的那堆产品也要能分完)
        int right = *max_element(quantities.begin(), quantities.end());

        while (left < right) {
            int mid = left + (right - left) / 2;
            if (canDistribute(n, quantities, mid)) {
                // 如果当前的mid可以完成分配,尝试减小mid
                right = mid;
            } else {
                // 如果当前的mid不能完成分配,需要增大mid
                left = mid + 1;
            }
        }

        return left;
    }
};

day252 2024-11-15

1574. Shortest Subarray to be Removed to Make Array Sorted

Given an integer array arr, remove a subarray (can be empty) from arr such that the remaining elements in arr are non-decreasing.

Return the length of the shortest subarray to remove.

A subarray is a contiguous subsequence of the array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1115QNTmvrmirWKX.png

题解

本题需要移除一个子数组使得剩余的数组是一个非减数组。由于子数组一定是连续的,则我们需要从中间删去某个长度的子数组使得数组剩余的两边拼接在一起后满足题目条件,那么前后的两个子数组自身必须要满足非减条件,因此可以先找到以数组开头作为起始和以数组末尾作为末尾的两个最长的符合条件的子数组。

找到该符合条件的子数组后,需要让两个子数组拼接后满足题目条件,则需要找到前面子数组中以某个数字结尾的部分和后面子数组中以某个数字开头的部分拼接后可以得到完整的满足条件的数组,则这个结尾的数字需不大于后面的开头的数字。同时使得从这两个数组中被丢弃的部分的长度和最小。

在得到前后两个最长数组后,若找到符合要求的同时让被丢弃部分最小的拼接数组,可使用双指针,分别指向前后两个数组的开头,前面的指针不断向后移动,指向的数字不断变大,同时移动后面的指针直到找到符合不小于前面数字的数字位置,不断计算被丢弃的数组的长度和,如此反复,直到前面的指针指向前面数组的末尾或者后面的指针指向后面数组的末尾为止。

代码

 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:
    int findLengthOfShortestSubarray(vector<int>& arr) {
        int n = arr.size();
        int left = 0, right = n - 1;

        while (left < n - 1 && arr[left] <= arr[left + 1]) {
            left++;
        }

        if (left == n - 1) return 0;

        while (right > left && arr[right - 1] <= arr[right]) {
            right--;
        }

        int result = min(n - left - 1, right);

        int i = 0, j = right;
        while (i <= left && j < n) {
            if (arr[i] <= arr[j]) {
                result = min(result, j - i - 1);
                i++;
            } else {
                j++;
            }
        }

        return result;
    }
};

day253 2024-11-16

3254. Find the Power of K-Size Subarrays I

You are given an array of integers nums of length n and a positive integer k.

The power of an array is defined as:

Its maximum element if all of its elements are consecutive and sorted in ascending order. -1 otherwise. You need to find the power of all subarrays of nums of size k.

Return an integer array results of size n - k + 1, where results[i] is the power of nums[i..(i + k - 1)].

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1116HC7wQlBZIZfk.png

题解

本题子数组的长度是固定的,故可以使用滑动窗口,将窗口长度固定为k不断向后滑动,记录窗口中以最后一个数字结尾的递增子数组的长度和最后一个数字。这样将窗口向后移动时每当添加了一个新的数字进入窗口,将该数字和最后一个数字比较,如果和最后一个数字相邻且比最后一个数字大则将记录的递增子数组长度加一,比最后一个数字小或者不相邻则将子数组长度初始化为1。若以最后一个数字结尾的递增子数组长度和k相同则将最后一个数字(因为是递增数组,最后一个数字就是最大的数字)放入results数组中,否则放入-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
class Solution {
public:
    vector<int> resultsArray(vector<int>& nums, int k) {
        int sortedlen = 0;
        int last = 0;
        for(int i=0;i<k;i++){
            if(nums[i] > last && nums[i] == last+1){
                sortedlen++;
            }else{
                sortedlen = 1;
            }
            last = nums[i];
        }
        vector<int> results;
        for(int i=k;i<nums.size();i++){
            if(sortedlen == k){
                results.push_back(last);
            }else{
                results.push_back(-1);
            }
            if(nums[i] > last && nums[i] == last+1){
                if(sortedlen < k){
                    sortedlen++;
                }
            }else{
                sortedlen = 1;
            }
            last = nums[i];
        }
        if(sortedlen == k){
            results.push_back(last);
        }else{
            results.push_back(-1);
        }
        return results;
    }
};

day254 2024-11-17

862. Shortest Subarray with Sum at Least K

Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1.

A subarray is a contiguous part of an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/11170J0aunvLBzAm.png

题解

本题求子数组的和满足大于等于K的全部子数组中长度最小的长度是多少。这种求子数组和的有关性质的题目首先可以想到使用前缀和来解决,但之前使用前缀和解题时是因为前缀和有一个很重要的性质即单调性,当所有数字均为非负数时前缀和是单调增的,因此我们可以根据题目要求来找到对应的下标,如同样是求子数组和至少为K的问题,若是单调增的前缀和,则根据下标为i对应的前缀和减去K的前缀和P来找到对应的小于等于P的前缀和对应的下标,即可知道所有子数组和至少为K的子数组。

但本题中存在负数,因此前缀和不是单调增的,这时可以考虑能否构造一个单调增的前缀和,则可使用单调栈来构造这样的前缀和,考虑栈顶前缀和的大小和新的前缀和的大小关系,若栈顶前缀和比新的前缀和大,则可弹出栈顶,因为对于还未访问到的前缀和,若后面的前缀和和栈顶的差大于等于k,则因为新的前缀和比栈顶小同时其对应的下标位置在栈顶的后面,则后面的前缀和和新前缀和的差必定也大于等于k且二者对应下标的差比和当前栈顶下标的差更小,故栈顶可以舍弃。

要求和不小于K的子数组就需要根据当前的前缀和计算出符合要求的前缀和大小,并在单调栈中找到不大于这个符合要求的前缀和大小的前缀和对应的下标,寻找这个符合要求的前缀和每次都要从头遍历单调栈,能否通过优化减少从头遍历的次数呢。很简单,题目要求找到满足要求的前缀和的最短长度,则我们只要找到刚好满足要求前缀和对应的下标位置,在这个下标之前的都可以直接丢弃,后续不再遍历这些位置。因为后面未遍历的前缀和要么比当前的大,要么比当前的小,比当前大则当前前缀和能取得的使子数组大于等于k的下标位置对于后面的同样是可以取得的,但后面到该下标的距离一定比当前下标大,如果比当前小,那么要取得使子数组大于等于k需要在当前前缀和对应的解的位置前面找更小的前缀和来使得子数组满足要求,这样得到的长度最多只能和当前前缀和对应的解的长度相同,不会更优。

根据这两个优化,可以构造一个单调的队列,使得两端都能弹出元素并满足上面的优化方法。先计算出前缀和,再不断遍历前缀和并求得满足条件的子数组长度与记录的最小长度比较并不断更新即可。

代码

 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 shortestSubarray(vector<int>& nums, int k) {
        int n = nums.size();

        vector<long long> preSum(n + 1);
        preSum[0] = 0;

        for (int i = 0; i < n; i++) {
            preSum[i + 1] = preSum[i] + nums[i];
        }

        int res = n + 1;
        // 使用双端队列存储下标
        deque<int> dq;

        // 遍历前缀和数组
        for (int i = 0; i <= n; i++) {

            while (!dq.empty() && preSum[i] <= preSum[dq.back()]) {
                dq.pop_back();
            }


            while (!dq.empty() && preSum[i] - preSum[dq.front()] >= k) {
                res = min(res, i - dq.front());
                dq.pop_front();
            }

            dq.push_back(i);
        }

        return res == n + 1 ? -1 : res;
    }
};

day255 2024-11-18

1652. Defuse the Bomb

You have a bomb to defuse, and your time is running out! Your informer will provide you with a circular array code of length of n and a key k.

To decrypt the code, you must replace every number. All the numbers are replaced simultaneously.

If k > 0, replace the ith number with the sum of the next k numbers. If k < 0, replace the ith number with the sum of the previous k numbers. If k == 0, replace the ith number with 0. As code is circular, the next element of code[n-1] is code[0], and the previous element of code[0] is code[n-1].

Given the circular array code and an integer key k, return the decrypted code to defuse the bomb!

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1118eBsjBEnoKqjE.png

题解

本题是简单题,题目场景的设置非常有意思,题目本身只需按照题面将对应的数字按条件替换,只要了解循环数组即可,因为本题要么使用ith后面的k个数字要么使用ith前面的k个数字,即窗口大小是固定的,因此可使用滑动窗口。当k大于0时,从头遍历数组向后滑动,k小于0则从尾部遍历数组向前滑动,使用滑动窗口每次去掉一个数字再加上新加入窗口的数字即得当前窗口内的数字和。可以避免重复计算已有的部分数字和。

代码

 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:
    vector<int> decrypt(vector<int>& code, int k) {
        int n = code.size();
        if (k == 0) {
            return vector<int>(n, 0);
        } else if (k > 0) {
            int sumnow = 0;
            vector<int> result;
            for (int i = 1; i <= k; i++) {
                sumnow += code[i];
            }
            for (int j = 1; j <= n; j++) {
                result.push_back(sumnow);
                sumnow -= code[j % n];
                sumnow += code[(j + k) % n];
             }
            return result;
        } else {
            k = -k;
            int start = n - k;
            int end = n - 1;
            int sumnow = 0;
            vector<int> result;
             for (int i = start; i <= end; i++) { // Calculate initial sum
                sumnow += code[i];
            }
            for (int j = 0; j < n; j++) {
                result.push_back(sumnow);
                sumnow -= code[(j + n -k)%n];
                sumnow += code[j];
            }
            return result;
        }
    }
};

day256 2024-11-19

2461. Maximum Sum of Distinct Subarrays With Length K

You are given an integer array nums and an integer k. Find the maximum subarray sum of all the subarrays of nums that meet the following conditions:

The length of the subarray is k, and All the elements of the subarray are distinct. Return the maximum subarray sum of all the subarrays that meet the conditions. If no subarray meets the conditions, return 0.

A subarray is a contiguous non-empty sequence of elements within an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1119nHyDAHqs3qsJ.png

题解

对于这样固定长度的子数组问题,首先肯定要使用滑动窗口,再思考本题中的限制条件,子数组中所有的数字都必须是不相同的,那么可以用一个数组来记录窗口中所有数字的个数,但仅记录某个数字自身的出现次数仍不方便我们了解窗口中有重复的数字有多少个。因此还可以用一个变量记录窗口中有重复的数字个数,个数为0时说明窗口中不再有重复数字。向前不断滑动窗口,根据移出数字和移入数字处理相关情况,在窗口内没有重复数字时更新窗口数字和的最大值即可。

代码

 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
class Solution {
public:
    long long maximumSubarraySum(vector<int>& nums, int k) {
        int exist[100001] = {};
        long long int maxsum = 0;
        int repeat = 0;
        long long int arraysum = 0;
        for(int i=0;i<k;i++){
            if(exist[nums[i]] == 1){
                repeat++;
                exist[nums[i]]++;
                arraysum += nums[i];
            }else{
                exist[nums[i]]++;
                arraysum += nums[i];
            }
        }
        if(repeat == 0){
            maxsum = arraysum;
        }
        int left = 0;
        int right = k;
        while(right < nums.size()){
            if(exist[nums[right]] == 1){
                repeat++;
            }
            exist[nums[right]]++;
            arraysum += nums[right];
            if(exist[nums[left]] == 2){
                repeat--;
            }
            exist[nums[left]]--;
            arraysum -= nums[left];
            if(repeat == 0){
                maxsum = max(maxsum, arraysum);
            }
            right++;
            left++;
        }
        return maxsum;
    }
};

day257 2024-11-20

2516. Take K of Each Character From Left and Right

You are given a string s consisting of the characters ‘a’, ‘b’, and ‘c’ and a non-negative integer k. Each minute, you may take either the leftmost character of s, or the rightmost character of s.

Return the minimum number of minutes needed for you to take at least k of each character, or return -1 if it is not possible to take k of each character.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1120GQzlFRPPMxa3.png

题解

本题要求返回能取得k个a,b,c字符需要的最少拿取次数,并不能使用贪心,因为选择从左面或者右面拿取字符会影响后面能得到目标结果时拿取的总个数,如左右都是字符a,但左侧第二个字符是b,右侧第二个字符是a,如果只需要每个字符拿1个,拿左侧的a后就可以继续拿取左侧的b,但若拿取右侧的a则想拿取b就要再拿掉左侧的a后才能拿到b。

因此需要考虑左侧的选择会对右侧的选择造成什么影响,要把这种影响全部找出来并记录下来。因此可以找到刚好可以满足题目条件的左侧数组的长度,并将左侧指针指向这个子数组的末尾,在此情况下,当左侧指针向左移动时,左侧数组会不满足题目条件,因此需要右侧数组来补充缺少的字符使得总体满足题目条件,因此右侧指针向左移动直到移动过的部分的右侧数组包含的字符和左侧数组包含的字符和满足题目条件。计算此时左右子数组的长度和,如此反复,直到左侧指针移动到数组开头。

但有可能数组本身就不可能满足题目要求,因此要先判断一下数组中存在的各个字符的个数,如果不满足要求直接返回-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

class Solution {
public:
    int takeCharacters(string s, int k) {
      int ca=0,cb=0,cc=0;
        int n=s.size();
        int ans=n;
        for(int i=0;i<n;i++){
            if(s[i]=='a') ca++;
            if(s[i]=='b') cb++;
            if(s[i]=='c') cc++;
        }
        if(ca<k||cb<k||cc<k) return -1;
        int i=n-1,j=n-1;
        while(i>=0){
            if(s[i]=='a') ca--;
            if(s[i]=='b') cb--;
            if(s[i]=='c') cc--;
            while(ca<k||cb<k||cc<k){
            if(s[j]=='a') ca++;
            if(s[j]=='b') cb++;
            if(s[j]=='c') cc++;
                j--;
            }
            ans=min(ans,i+n-1-j); i--;
        }
        return ans;

    }
};

day258 2024-11-21

2257. Count Unguarded Cells in the Grid

You are given two integers m and n representing a 0-indexed m x n grid. You are also given two 2D integer arrays guards and walls where guards[i] = [rowi, coli] and walls[j] = [rowj, colj] represent the positions of the ith guard and jth wall respectively.

A guard can see every cell in the four cardinal directions (north, east, south, or west) starting from their position unless obstructed by a wall or another guard. A cell is guarded if there is at least one guard that can see it.

Return the number of unoccupied cells that are not guarded.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1121QXH56gcN6DEq.png

题解

本题模拟守卫能看到的区域,首先创建mxn的数组,先遍历所有墙的位置将对应位置的值设置为2,记录墙的个数,再遍历守卫,对每个守卫所在的位置向四个方向遍历直到到数组边界或者遇到墙为止,将这些位置的值设置为1,同时记录遍历过程中将0变为1的个数,最终用数组总数减去墙和守卫以及守卫看守的位置数的和得最终结果。

这种遍历并且按照属性设置对应位置的值的方法可以得到正确的结果,但对有些示例会超时,思考为什么会超时以及如何优化,可以想到如果从某个守卫出发开始向某一方向(比如向右)遍历数组时遇到了另一个守卫,起始没有必要再继续向该方向遍历了,因为后面的位置遇到的新守卫同样也可以看到,此时可以直接结束遍历,因此我们可以将墙的位置值设置为3,将守卫位置的值先全部设置为2,在遍历数组时判断要遍历的位置的值是否小于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
28
class Solution {
public:
    int countUnguarded(int m, int n, vector<vector<int>>& guards, vector<vector<int>>& walls) {
        vector<vector<int>> cells(m,vector<int>(n,0));
        for (auto wall : walls){
            cells[wall[0]][wall[1]] = 3;
        }
        int guardpos = 0;
        int direction[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
        for (auto guard : guards){
            cells[guard[0]][guard[1]] = 2;
        }
        for (auto guard : guards){
            for(int i=0;i<4;i++){
                int pos[2] = {guard[0]+direction[i][0], guard[1]+direction[i][1]};
                while(pos[0]>=0&&pos[0]<m&&pos[1]>=0&&pos[1]<n&&cells[pos[0]][pos[1]]<2){
                    if(cells[pos[0]][pos[1]] == 0){
                        guardpos++;
                        cells[pos[0]][pos[1]] = 1;
                    }
                    pos[0] += direction[i][0];
                    pos[1] += direction[i][1];
                }
            }
        }
        return m*n-guardpos-guards.size()-walls.size();
    }
};

day259 2024-11-22

1072. Flip Columns For Maximum Number of Equal Rows

You are given an m x n binary matrix matrix.

You can choose any number of columns in the matrix and flip every cell in that column (i.e., Change the value of the cell from 0 to 1 or vice versa).

Return the maximum number of rows that have all values equal after some number of flips.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1122r0stlh0LaeX9.png

题解

本题先思考简单例子,对于两行01串,在什么情况下通过多次翻转同一个位置上的数字最终可以使得两行行内数字完全相同,如00,10显然无论怎么翻转都只可能有一行数字是完全相同的,另一行则不可能相同,再考虑10,01,只需翻转任意一列,两行数字都会相同。由此可以猜测对于任意两行数字,若这两行数字每一位上的数都不相同,则这两行数字最终可以通过翻转实现两行数字行内完全相同,注意数字只能取0,1两种,因此若两行数字满足每一位都不相同,要想三行数字满足行内相同,第三行数字必定和这两行数字中的某一行完全相同。

则若能通过翻转使得任意两行的行内数字完全相同,这两行要么相同,要么相反。我们只需统计相同行和对应的相反行的数量和,找到最大值就得到本题的解。

问题在于如何统计,考虑要统计的行要么完全相同,要么相反,则可以将二者用一个同样的值来表示,此处就要用到哈希,问题是如果将每一行中的每一位视为简单的二进制位,考虑行的长度最大为300,显然不能用整数直接装下,因此要么使用分块哈希,要么充分利用c++中已有的哈希实现,将每一行转换为字符串再作为哈希的键。由于相反的行我们想使用同一个键值来表示,因此对每一行都构造出该行对应的字符串和该行对应的相反行的字符串,取两个字符串中字典序小的字符串,这样就将相反行都用二者中字典序小的字符串来表示,进而统计满足相同或者相反行的总数。

代码

 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
class Solution {
public:
    int maxEqualRowsAfterFlips(vector<vector<int>>& matrix) {
        unordered_map<string, int> count;
        int m = matrix.size(), n = matrix[0].size();

        for (const auto& row : matrix) {
            string pattern(n, '0');
            string flipped(n, '0');

            for (int j = 0; j < n; j++) {
                pattern[j] = row[j] + '0';
                flipped[j] = (1 - row[j]) + '0';
            }

            count[min(pattern, flipped)]++;
        }

        int maxCount = 0;
        for (const auto& [_, cnt] : count) {
            maxCount = max(maxCount, cnt);
        }
        return maxCount;
    }
};

day260 2024-11-23

1861. Rotating the Box

You are given an m x n matrix of characters box representing a side-view of a box. Each cell of the box is one of the following:

A stone ‘#’ A stationary obstacle ‘*’ Empty ‘.’ The box is rotated 90 degrees clockwise, causing some of the stones to fall due to gravity. Each stone falls down until it lands on an obstacle, another stone, or the bottom of the box. Gravity does not affect the obstacles’ positions, and the inertia from the box’s rotation does not affect the stones’ horizontal positions.

It is guaranteed that each stone in box rests on an obstacle, another stone, or the bottom of the box.

Return an n x m matrix representing the box after the rotation described above.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1123pEbOZz2P5uHK.png

题解

本题图里的石头挺有意思的,像哪里来的魔法石。题目直观的想象是将一个容器翻转90°,翻转后石头会自由落体直到落到一块比较坚实的“地面”上。考虑原来的行和翻转后的列的关系,原来的第一行翻转后会变为第一列,如果共有m行n列,那么原来位于(p,q)位置的会变为位于(q,m-p-1)。先构建一个n x m的空数组,遍历原始的m x n数组,当按行遍历时统计该行中遇到过的石头的个数直到遇到一个障碍物,此时将该障碍物按照转换后的位置填入空数组中,并根据该障碍物前面的石头个数在空数组中该障碍物的位置向上填充对应数量的石头。如此反复,即得最终转换后的数组。

代码

 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:
    vector<vector<char>> rotateTheBox(vector<vector<char>>& box) {
        int m = box.size();
        int n = box[0].size();
        vector<vector<char>> rotate(n,(vector<char>(m,'.')));
        int stones = 0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if (box[i][j] == '#'){
                    stones++;
                }else if(box[i][j] == '*'){
                    rotate[j][m-i-1] = '*';
                    while(stones > 0){
                        rotate[j-stones][m-i-1] = '#';
                        stones--;
                    }
                }
            }
            while(stones > 0){
                rotate[n-stones][m-i-1] = '#';
                stones--;
            }
        }
        return rotate;
    }
};

day261 2024-11-24

1975. Maximum Matrix Sum

You are given an n x n integer matrix. You can do the following operation any number of times:

Choose any two adjacent elements of matrix and multiply each of them by -1. Two elements are considered adjacent if and only if they share a border.

Your goal is to maximize the summation of the matrix’s elements. Return the maximum sum of the matrix’s elements using the operation mentioned above.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1124n2svbPikMZWV.png

题解

本题通过不断将相邻数字变为自己的相反数的操作,最终一定可以将负数变为相邻位置(相当于对负号进行了传递),这时再进行一次操作即可使得两个负数都变为正数。因此遍历并记录数组中负数的个数,同时记录下绝对值最小的数(在加和时减少的最少)。若负数个数为偶数则直接将数字全部变为正数加和,若为奇数则除绝对值最小的保持为负数外其余均按照正数加和。

因此本题可以直接将所有负数均变为正数并加和,最后根据总体负数的奇偶性再对绝对值最小的数进行加减。这样仅需遍历一遍数组就能得到结果。

这样计算没有考虑包含0的情况,在包含0的情况下,所有负数均可通过将负号最终传递给0从而变成正数,0自身是没有符号的,就像黑洞一样吸掉了全部符号。因此在包含0的情况下无需考虑负数个数的奇偶性,直接将所有数字按绝对值加和就得到了最终结果。

代码

 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:
    long long maxMatrixSum(vector<vector<int>>& matrix) {
        int addsmall = 100001;
        int nums = 0;
        long long int sum = 0;
        bool zero = false;
        for (const auto& row : matrix){
            for(const auto& num : row){
                if(num > 0){
                    if(num < addsmall){
                        addsmall = num;
                    }
                    sum += num;
                }else if(num < 0){
                    nums++;
                    if (-num < addsmall){
                        addsmall = -num;
                    }
                    sum += -num;
                }else{
                    zero = true;
                }
            }
        }
        if(nums % 2 == 1 && !zero){
            sum += -2 * addsmall;
        }
        return sum;
    }
};

day262 2024-11-25

773. Sliding Puzzle

On an 2 x 3 board, there are five tiles labeled from 1 to 5, and an empty square represented by 0. A move consists of choosing 0 and a 4-directionally adjacent number and swapping it.

The state of the board is solved if and only if the board is [[1,2,3],[4,5,0]].

Given the puzzle board board, return the least number of moves required so that the state of the board is solved. If it is impossible for the state of the board to be solved, return -1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1125NLB8Q0i1LvJO.png

题解

本题是一道难题,可以将其视为一个搜索问题,但这种搜索问题正因非常经典从而非常难。类似这种问题比较出名的场景是围棋,但围棋的棋子是属于不同方的,因此在构建棋盘状态时除了棋子自身的位置还要加上棋子的归属这一属性,而本题相对简化,不存在这个问题,相当于每个棋子仅有数字作为自身的属性。最终搜索的目标也是固定的状态。

则可以一边构造出状态树一边搜索,状态树中每个节点是棋盘当前的布局,边表示边相连的节点可以通过一次0的移动互相转换。通过广度优先搜索不断遍历树的每一层,直到找到目标节点,此时遍历的深度即为需要的移动步数,而若全部遍历完后仍未达到目标状态,则返回-1。

难点在于,在展开这棵状态树时,我们不想让底层的节点重复已经构造过的状态节点,如何记录已经构造过的状态节点呢,可以将棋盘上的数字按顺序写成一个字符串,将字符串放入set中,这样只需查看set中是否已经包含这个字符串即可知道是否已经构造过这个状态。

另一方面,对于每个位置可以替换的数字的位置是固定的,因此将每个位置可以替换的数字的位置在字符串中的下标保存起来,使用时直接将字符串的当前位置与可替换的位置进行交换即得到构造的两个新字符串。

本题如果能想到将状态作为节点,将边作为状态之间关联的连接,后续其实相当比较容易,将这个模型提炼出来本身就是一个难点。这要求我们要更深入的理解数据结构,如图,图中的节点未必只能表示一个值,它可以是一种抽象的状态,只要状态之间有某种关联就可以构造边来连接,树同理。

代码

 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
class Solution {
public:
    int slidingPuzzle(vector<vector<int>>& board) {
        string target = "123450";
        string start;
        for (const auto& row : board) {
            for (int num : row) {
                start += to_string(num);
            }
        }

        vector<vector<int>> neighbors = {
            {1, 3},
            {0, 2, 4},
            {1, 5},
            {0, 4},
            {3, 1, 5},
            {2, 4}
        };

        queue<pair<string, int>> q;
        unordered_set<string> visited;

        q.push({start, 0});
        visited.insert(start);

        while (!q.empty()) {
            auto [state, moves] = q.front();
            q.pop();

            if (state == target) {
                return moves;
            }

            int zero_pos = state.find('0');
            for (int neighbor : neighbors[zero_pos]) {
                string new_state = state;
                swap(new_state[zero_pos], new_state[neighbor]);

                if (visited.find(new_state) == visited.end()) {
                    q.push({new_state, moves + 1});
                    visited.insert(new_state);
                }
            }
        }

        return -1;
    }
};

day263 2024-11-26

2924. Find Champion II

There are n teams numbered from 0 to n - 1 in a tournament; each team is also a node in a DAG.

You are given the integer n and a 0-indexed 2D integer array edges of length m representing the DAG, where edges[i] = [ui, vi] indicates that there is a directed edge from team ui to team vi in the graph.

A directed edge from a to b in the graph means that team a is stronger than team b and team b is weaker than team a.

Team a will be the champion of the tournament if there is no team b that is stronger than team a.

Return the team that will be the champion of the tournament if there is a unique champion, otherwise, return -1.

Notes

A cycle is a series of nodes a1, a2, …, an, an+1 such that node a1 is the same node as node an+1, the nodes a1, a2, …, an are distinct, and there is a directed edge from the node ai to node ai+1 for every i in the range [1, n]. A DAG is a directed graph that does not have any cycle.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1126WmfZcBlSPLaH.png

题解

本题使用简化版拓扑排序可以解决,拓扑排序在以前的题目中曾经讲解过,在有向无环图中,拓扑排序表示的是一种抽象的先后关系,如若节点均表示数字,则这种先后关系就可以是数字之间的相对大小,若把节点看成一个个在排队的人,则这种先后关系可以是一个人排在另一个人的前面。这种抽象的关系可以对应到各种实际情况中。对于本题,一条有向边相当于一支队伍打赢了另外一支队伍,那么拓扑排序的起始队伍就相当于打赢了其他队伍但没人打赢它,如果这样的队伍只有一个,显然就是冠军,不止有一个那说明还需要更多比赛。

之所以说是简化的拓扑排序,在于本题理解题面可以用拓扑排序的思想,但实际解题,只需要记录下所有节点(队伍)的入度(即有几支队伍打赢了它),最终找到所有入度为0的队伍,只有一个直接返回,否则返回-1。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
public:
    int findChampion(int n, vector<vector<int>>& edges) {
        vector<int> nodein(n,0);
        for(const auto& edge : edges){
            nodein[edge[1]]++;
        }
        int zero = -1;
        for(int i=0;i<n;i++){
            if(nodein[i] == 0 && zero == -1){
                zero = i;
            }else if(nodein[i] == 0){
                return -1;
            }
        }
        return zero;
    }
};

day264 2024-11-27

3243. Shortest Distance After Road Addition Queries I

You are given an integer n and a 2D integer array queries.

There are n cities numbered from 0 to n - 1. Initially, there is a unidirectional road from city i to city i + 1 for all 0 <= i < n - 1.

queries[i] = [ui, vi] represents the addition of a new unidirectional road from city ui to city vi. After each query, you need to find the length of the shortest path from city 0 to city n - 1.

Return an array answer where for each i in the range [0, queries.length - 1], answer[i] is the length of the shortest path from city 0 to city n - 1 after processing the first i + 1 queries.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1127SRxWXxnZ7b3M.png

题解

本题每次在增加了一条新路线后可以使用dijistra算法从0节点开始寻找到其他节点的最短路径,一旦找到到n-1节点的最短路径就停止dijistra算法。

最初会想到在添加新路径后可以直接从最初的距离减去添加的新路径中间节省的距离,但这种做法的问题在于新添加的路径可能会与之前添加过的路径有交叉,则无法确定应该减去的节省的距离是多少(如a->b为4,b->c为3,但a->d为5,d->c为1,实际选择d这条路径总距离更短)。而用dijistra算法求出的到每个节点的距离已经是最短距离,一旦确定了到n-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

class Solution {
public:
    vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>>& queries) {
        vector<vector<pair<int, int>>> adj(n);

        for(int i = 0; i < n-1; i++) {
            adj[i].push_back({i+1, 1});
        }

        vector<int> answer;

        for(const auto& query : queries) {
            int u = query[0];
            int v = query[1];

            adj[u].push_back({v, 1});

            answer.push_back(dijkstra(adj, n, 0, n-1));
        }

        return answer;
    }

private:
    int dijkstra(const vector<vector<pair<int, int>>>& adj, int n, int start, int end) {
        vector<int> dist(n, INT_MAX);
        dist[start] = 0;

        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
        pq.push({0, start});

        while(!pq.empty()) {
            int d = pq.top().first;
            int u = pq.top().second;
            pq.pop();

            if(u == end) return d;

            if(d > dist[u]) continue;

            for(const auto& [v, weight] : adj[u]) {
                if(dist[v] > dist[u] + weight) {
                    dist[v] = dist[u] + weight;
                    pq.push({dist[v], v});
                }
            }
        }

        return dist[end];
    }
};

day265 2024-11-28

2290. Minimum Obstacle Removal to Reach Corner

You are given a 0-indexed 2D integer array grid of size m x n. Each cell has one of two values:

0 represents an empty cell, 1 represents an obstacle that may be removed. You can move up, down, left, or right from and to an empty cell.

Return the minimum number of obstacles to remove so you can move from the upper left corner (0, 0) to the lower right corner (m - 1, n - 1).

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/11288J5wLzndHQBS.png

题解

本题是一道难题,关键在于如何建模寻路的过程,一般提到寻路或者路径规划相关的问题,都会想到这是一个图问题,本题中如何将在数组中寻路转换为在图上寻路,并能得到最小成本就是解题的关键。在图上找到成本最小的路径无异于在图上寻找两个节点之间的最短路径,由于边的权重不存在负值,这就是一道典型的可以用dijistra算法解决的问题。

问题在于如何转换,考虑从任何一个位置出发向四个方向移动,如果遇到墙,想要经过墙就必须要花费移除墙的“成本”。最终要求的是移除最少数量的墙的路径,则每个墙都可以视为要花费1点成本。对于没有墙的位置,移动过去不需要花费成本,则将矩阵中每个位置都视为一个独立的节点,如果某个位置有墙,则向这个位置移动的成本就为1,每个位置都和四个方向的其他位置节点之间存在边,如果移动的成本为1,则边权为1,否则为0,此时就转化成了在图上寻找最短路径的问题,可以使用dijistra算法解决。

代码

 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

class Solution {
public:
    int minimumObstacles(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();

        vector<int> dx = {-1, 1, 0, 0};
        vector<int> dy = {0, 0, -1, 1};

        // 优先级队列实现dijistra算法
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;

        vector<vector<int>> dist(m, vector<int>(n, INT_MAX));

        dist[0][0] = grid[0][0];
        pq.push({dist[0][0], 0});

        while (!pq.empty()) {
            auto [cost, pos] = pq.top();
            pq.pop();

            int x = pos / n;
            int y = pos % n;

            if (cost > dist[x][y]) continue;

            for (int i = 0; i < 4; i++) {
                int nx = x + dx[i];
                int ny = y + dy[i];

                if (nx >= 0 && nx < m && ny >= 0 && ny < n) {
                    int newCost = cost + grid[nx][ny];

                    if (newCost < dist[nx][ny]) {
                        dist[nx][ny] = newCost;
                        pq.push({newCost, nx * n + ny});
                    }
                }
            }
        }

        return dist[m-1][n-1];
    }
};

day266 2024-11-29

2577. Minimum Time to Visit a Cell In a Grid

You are given a m x n matrix grid consisting of non-negative integers where grid[row][col] represents the minimum time required to be able to visit the cell (row, col), which means you can visit the cell (row, col) only when the time you visit it is greater than or equal to grid[row][col].

You are standing in the top-left cell of the matrix in the 0th second, and you must move to any adjacent cell in the four directions: up, down, left, and right. Each move you make takes 1 second.

Return the minimum time required in which you can visit the bottom-right cell of the matrix. If you cannot visit the bottom-right cell, then return -1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1129uNi8EoWpGKuf.png

题解

本题仍可以使用dijistra算法解决,像昨天的问题一样,将每个位置视为节点,只不过将每个位置对应的时间视为到这个节点的总成本,同时因为可以在两个节点间来回移动直到时间足够能移动到下一个节点,则只要能从原始位置通过一个时间间隔移动到相邻的位置就可以通过来回移动直到时间足够能移动到下一个相邻可移动位置。但要注意,在来回移动的时候要想继续向当前位置的下一个相邻位置移动,需要移动偶数次步数,因为奇数次步数会移动回当前位置的前一个位置,偶数才会移动回当前位置。

在最开始,只要能从初始位置移动到相邻位置,后面就可以使用dijistra找到到达右下角的最短路径。

代码

 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
class Solution {
public:
    int minimumTime(vector<vector<int>>& grid) {
        // 如果一开始就无法移动,直接返回-1
        if (grid[0][1] > 1 && grid[1][0] > 1) return -1;

        int m = grid.size(), n = grid[0].size();
        vector<vector<int>> dist(m, vector<int>(n, INT_MAX));
        dist[0][0] = 0;

        vector<pair<int, int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

        priority_queue<pair<int, pair<int, int>>,
                      vector<pair<int, pair<int, int>>>,
                      greater<>> pq;
        pq.push({0, {0, 0}});

        while (!pq.empty()) {
            auto [time, pos] = pq.top();
            auto [x, y] = pos;
            pq.pop();

            if (time > dist[x][y]) continue;

            for (const auto& dir : dirs) {
                int nx = x + dir.first;
                int ny = y + dir.second;

                if (nx >= 0 && nx < m && ny >= 0 && ny < n) {
                    int nextTime = time + 1;
                    // 需要等待到满足要求的时间
                    if (grid[nx][ny] > nextTime) {
                        // 如果时间差是奇数,需要多等一步
                        nextTime = grid[nx][ny];
                        if ((grid[nx][ny] - time) % 2 == 0) {
                            nextTime++;
                        }
                    }

                    if (nextTime < dist[nx][ny]) {
                        dist[nx][ny] = nextTime;
                        pq.push({nextTime, {nx, ny}});
                    }
                }
            }
        }

        return dist[m-1][n-1] == INT_MAX ? -1 : dist[m-1][n-1];
    }
};

day267 2024-11-30

2097. Valid Arrangement of Pairs

You are given a 0-indexed 2D integer array pairs where pairs[i] = [starti, endi]. An arrangement of pairs is valid if for every index i where 1 <= i < pairs.length, we have endi-1 == starti.

Return any valid arrangement of pairs.

Note: The inputs will be generated such that there exists a valid arrangement of pairs.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1130P2sjjTCttOLr.png

题解

本题是一道典型的图问题,每一个pair有一个start和end相当于一个有向边的起始和终止节点。而本题即找图中一条可以一次性经过图中全部边且不重复的路径问题,也就是一个欧拉图问题。这种图问题如果不了解相关知识是比较难解的,像在一个欧拉图中寻找欧拉路径的问题当前已经有比较经典的算法,故本题可以使用Hierholzer算法解决。注意本题中要寻找的是欧拉通路而不是欧拉回路。对于欧拉图的简单介绍可以参考

欧拉图

我们要思考的是,Hierholzer算法是怎么来的,为什么可以想到这样的算法思路。考虑有向图的情况,在有向图中,若图是欧拉图,则起点

代码

 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
class Solution {
public:
    vector<vector<int>> validArrangement(vector<vector<int>>& pairs) {
        unordered_map<int, vector<int>> graph;
        unordered_map<int, int> inDegree, outDegree;

        for (const auto& pair : pairs) {
            int u = pair[0], v = pair[1];
            graph[u].push_back(v);
            outDegree[u]++;
            inDegree[v]++;
        }

        int start = pairs[0][0];
        for (const auto& [node, _] : graph) {
            if (outDegree[node] > inDegree[node]) {
                start = node;
                break;
            }
        }

        deque<int> path;
        stack<int> stk;
        stk.push(start);

        while (!stk.empty()) {
            int u = stk.top();
            if (graph[u].empty()) {
                path.push_front(u);
                stk.pop();
            } else {
                stk.push(graph[u].back());
                graph[u].pop_back();
            }
        }

        vector<vector<int>> result;
        for (auto it = path.begin(); it != prev(path.end()); ++it) {
            result.push_back({*it, *next(it)});
        }

        return result;
    }
};

day268 2024-12-01

1346. Check If N and Its Double Exist

Given an array arr of integers, check if there exist two indices i and j such that :

i != j 0 <= i, j < arr.length arr[i] == 2 * arr[j]

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1201DFIHsnacqzZu.png

题解

本题可以使用集合,在遍历数组的过程中将每个数的2倍和1/2都放入集合中,在继续遍历的过程中一旦碰到了集合中已有的数字就返回true,否则返回false。本题只要求返回是否存在这样的组合不要求返回具体的组合对应的下标,故使用集合记录满足条件的数字即能得到最终结果,若要返回具体的下标,则可以构造结构体将每个数对应的原始下标也保存下来。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
public:
    bool checkIfExist(vector<int>& arr) {
        set<int> sets;
        for(const int& num : arr){
            if (sets.find(num) != sets.end()){
                return true;
            }
            if(num % 2 == 0){
                sets.insert(num/2);
            }
            sets.insert(num*2);
        }
        return false;
    }
};

day269 2024-12-02

1455. Check If a Word Occurs As a Prefix of Any Word in a Sentence

Given a sentence that consists of some words separated by a single space, and a searchWord, check if searchWord is a prefix of any word in sentence.

Return the index of the word in sentence (1-indexed) where searchWord is a prefix of this word. If searchWord is a prefix of more than one word, return the index of the first word (minimum index). If there is no such word return -1.

A prefix of a string s is any leading contiguous substring of s.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1202hl42cHF424xu.png

题解

本题将待搜索的单词直接与句子中的单词比较,遇到不同的字符就跳到下一个被空格分隔的单词重新比较,直到遇到能比对成功的单词(计数并返回单词的下标)或者遍历到句子结尾为止。

代码

 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 {
public:
    int isPrefixOfWord(string sentence, string searchWord) {
        int n = searchWord.size();
        int curindex = 1;
        int curchar = 0;
        bool success = true;
        for (const auto& ch : sentence){
            if(!success && ch!=' '){
                continue;
            }
            if(ch == ' '){
                success = true;
                curchar = 0;
                curindex++;
                continue;
            }
            if(ch == searchWord[curchar]){
                curchar++;
                if(curchar == n){
                    return curindex;
                }
            }else{
                success = false;
            }

        }
        return -1;
    }
};

day270 2024-12-03

2109. Adding Spaces to a String

You are given a 0-indexed string s and a 0-indexed integer array spaces that describes the indices in the original string where spaces will be added. Each space should be inserted before the character at the given index.

For example, given s = “EnjoyYourCoffee” and spaces = [5, 9], we place spaces before ‘Y’ and ‘C’, which are at indices 5 and 9 respectively. Thus, we obtain “Enjoy Your Coffee”. Return the modified string after the spaces have been added.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/12032vPuVF4iJOC4.png

题解

本题按照题意在对应的位置插入空格并构造新字符串,可以创建一个空的新字符串,在扫描原字符串的同时在给定的下标位置插入空格,再继续遍历原字符串,用一个变量记录原字符串遍历到的下标。当遍历到的下标满足要插入空格的位置时在新字符串中插入一个空格,继续遍历并复制原始字符串。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Solution {
public:
    string addSpaces(string s, vector<int>& spaces) {
        string news = "";
        int index = 0;
        int n = s.size();
        for(const int& space : spaces){
            while(index != space){
                news.push_back(s[index]);
                index++;
            }
            news.push_back(' ');
        }
        news.append(s,index,n);
        return news;
    }
};

day271 2024-12-04

2825. Make String a Subsequence Using Cyclic Increments

You are given two 0-indexed strings str1 and str2.

In an operation, you select a set of indices in str1, and for each index i in the set, increment str1[i] to the next character cyclically. That is ‘a’ becomes ‘b’, ‘b’ becomes ‘c’, and so on, and ‘z’ becomes ‘a’.

Return true if it is possible to make str2 a subsequence of str1 by performing the operation at most once, and false otherwise.

Note: A subsequence of a string is a new string that is formed from the original string by deleting some (possibly none) of the characters without disturbing the relative positions of the remaining characters.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1204yOsknB49hUdI.png

题解

本题仍是字符串匹配问题,只是做了一些小变动。因为题目中允许我们将指定位置上的字符变换到下一个字符,因此在str1中匹配str2的子序列时,除了和str2中的字符完全相同外,在字母表上比str2的字符小一个位置的字符也可以。

则用指针p遍历str2字符串,并同时用指针q遍历str1字符串直到碰到str2[p]和str1[q]相同或等于str1[q]+1。此时成功匹配str2中的一个字符,移动指针p至下一位,继续用q向后遍历str1重复上述过程,直到将str2完全匹配完返回true或str1已经到末尾但str2仍未完全匹配则返回false。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
    bool canMakeSubsequence(string str1, string str2) {
        int m = str1.size();
        const char map[26] = {'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y'};
        int str1index = 0;
        for (const char& ch : str2){
            while(ch != str1[str1index] && map[ch-'a'] != str1[str1index]){
                if(str1index < m){
                    str1index++;
                }else{
                    return false;
                }
            }
            str1index++;
        }
        return true;
    }
};

day272 2024-12-05

2337. Move Pieces to Obtain a String

You are given two strings start and target, both of length n. Each string consists only of the characters ‘L’, ‘R’, and ‘_’ where:

The characters ‘L’ and ‘R’ represent pieces, where a piece ‘L’ can move to the left only if there is a blank space directly to its left, and a piece ‘R’ can move to the right only if there is a blank space directly to its right. The character ‘_’ represents a blank space that can be occupied by any of the ‘L’ or ‘R’ pieces. Return true if it is possible to obtain the string target by moving the pieces of the string start any number of times. Otherwise, return false.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1205wpH4d2LqKbin.png

题解

本题是一道字符串问题,对于这种问题我们无需考虑将start变为target的具体移动步骤,只需考虑什么条件下一定能通过移动将start中的字符移动为target中的某个字符。对于字符’L’来说,只要target中存在一个’L’在start中这个’L’的相同或者左侧位置并且二者之间的位置全部为空格,就可以将start的’L’移动到target中对应的’L’处,‘R’同理。

则可以同时用两个指针分别遍历target和start,每当在target中遇到一个非空字符时,若为’L’则移动start中的指针直到找到一个’L’,若start中的’L’的下标和target中’L’下标相同或者更大(即start中的’L’在target中对应’L’的右侧,这样就可以通过左移移动到target中对应的’L’)。对’R’同理,注意target中L R出现的顺序要与start中相同且start中的非空格字符满足上述的下标条件(‘L’在target右侧,‘R’在target左侧)。如果全部满足条件则说明可以转换成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
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
    bool canChange(string start, string target) {
        int startindex = 0;
        int targetindex = 0;
        int n = start.size();
        for(targetindex=0;targetindex<n;targetindex++){
            if (target[targetindex] == '_'){
                continue;
            }else if(target[targetindex] == 'L'){
                while(start[startindex] == '_'){
                    startindex++;
                }
                if (start[startindex] == 'R' || startindex<targetindex || startindex >= n){
                    return false;
                }
                startindex++;
            }else{
                while(start[startindex] == '_'){
                    startindex++;
                }
                if (start[startindex] == 'L' || startindex>targetindex || startindex >= n){
                    return false;
                }
                startindex++;
            }
        }
        while(startindex<n){
            if(start[startindex] != '_'){
                return false;
            }
            startindex++;
        }
        return true;
    }
};

day273 2024-12-06

2554. Maximum Number of Integers to Choose From a Range I

You are given an integer array banned and two integers n and maxSum. You are choosing some number of integers following the below rules:

The chosen integers have to be in the range [1, n]. Each integer can be chosen at most once. The chosen integers should not be in the array banned. The sum of the chosen integers should not exceed maxSum. Return the maximum number of integers you can choose following the mentioned rules.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1206e4MHgtw2CXec.png

题解

本题先将banned数组排序,再根据有序的banned数组中的数字和n的范围限制将banned数组中相邻两个数字中间的数字段的全部数字加和,与maxSum比较,小于maxSum则说明这些数字可以全部取得,加到计数总数中,否则使用二分法找到这个数字区间内满足n的范围限制且加和后和小于等于maxSum的最大数字,累加计数即得最终结果。

代码

 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

class Solution {
public:
    int maxCount(vector<int>& banned, int n, int maxSum) {
        sort(banned.begin(), banned.end());

        vector<int> nums;
        nums.push_back(0);
        for (int x : banned) {
            if (x <= n) nums.push_back(x);
        }
        nums.push_back(n + 1);

        int ans = 0;
        long long sum = 0;

        for (int i = 1; i < nums.size(); i++) {
            int left = nums[i-1] + 1;
            int right = nums[i] - 1;

            if (left > right || left > n) continue;
            right = min(right, n);

            long long count = right - left + 1;
            long long rangeSum = (left + right) * count / 2;

            if (sum + rangeSum <= maxSum) {
                ans += count;
                sum += rangeSum;
            } else {
                int low = left;
                int high = right;
                while (low <= high) {
                    int mid = low + (high - low) / 2;
                    count = mid - left + 1;
                    rangeSum = (left + mid) * count / 2;

                    if (sum + rangeSum <= maxSum) {
                        low = mid + 1;
                    } else {
                        high = mid - 1;
                    }
                }
                if (high >= left) {
                    count = high - left + 1;
                    ans += count;
                    sum += (left + high) * count / 2;
                }
                break;
            }
        }

        return ans;
    }
};

day274 2024-12-07

1760. Minimum Limit of Balls in a Bag

You are given an integer array nums where the ith bag contains nums[i] balls. You are also given an integer maxOperations.

You can perform the following operation at most maxOperations times:

Take any bag of balls and divide it into two new bags with a positive number of balls. For example, a bag of 5 balls can become two new bags of 1 and 4 balls, or two new bags of 2 and 3 balls. Your penalty is the maximum number of balls in a bag. You want to minimize your penalty after the operations.

Return the minimum possible penalty after performing the operations.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/12072UfFSwaDuO8f.png

题解

本题起初想到计算出最终能分出的袋子的总数,将球的总数求和再按照尽可能均分的方式将球均分到袋子中,但这种思路在本题并不适用,因为均分后的最少的球的个数可能比当前某些袋子中已有的球的个数要多,而题目只允许将袋子中的球分开而不允许向袋子中添加球,因此这种情况是不符合题目要求的。我们只能通过模拟拆分的方式来模拟球的拆分过程最终得到每个袋子中球的个数。

既然只能通过模拟拆分方式来得到最终每个袋子中球的分配,那么必须要对拆分过程进行一个限制,这里我们可以限制允许拆分出来的每个袋子中球的最大个数,可以从具有最多球的袋子中球的个数减一开始,依次减一作为最大个数限制并模拟拆分过程来判断最终能否在满足我们自己定义的限制条件下拆分成功。如果成功,则可继续减小限制,不成功则不能继续减小限制。

一个一个的减少个数限制效率比较低,此时发现其实“能否在最大个数限制下成功拆分”是一个二元条件,具有一个临界值,即比该临界个数大的个数限制必定都可以成功拆分,而小于该临界个数的个数限制必定不能成功拆分。因此可以使用二分法找到这个临界限制,这个临界限制就是我们最终要求的最小可能惩罚。

代码

 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

class Solution {
public:
    // 检查在给定的最大球数限制下是否可以完成分割
    bool canSplit(vector<int>& nums, int maxOperations, int limit) {
        int operations = 0;
        for (int num : nums) {
            operations += (num - 1) / limit;
            if (operations > maxOperations) {
                return false;
            }
        }
        return true;
    }

    int minimumSize(vector<int>& nums, int maxOperations) {
        int maxNum = 0;
        for (int num : nums) {
            maxNum = max(maxNum, num);
        }

        int left = 1;
        int right = maxNum;
        int result = maxNum;

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (canSplit(nums, maxOperations, mid)) {
                result = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return result;
    }
};

day275 2024-12-08

2054. Two Best Non-Overlapping Events

You are given a 0-indexed 2D integer array of events where events[i] = [startTimei, endTimei, valuei]. The ith event starts at startTimei and ends at endTimei, and if you attend this event, you will receive a value of valuei. You can choose at most two non-overlapping events to attend such that the sum of their values is maximized.

Return this maximum sum.

Note that the start time and end time is inclusive: that is, you cannot attend two events where one of them starts and the other ends at the same time. More specifically, if you attend an event with end time t, the next event must start at or after t + 1.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1208LVIE9osJog7C.png

题解

本题只能取两个互不重叠的事件并取二者的值的和,因此我们只需考虑某个事件a开始时间之前已经结束的所有事件中价值最大的事件,并将其和事件a的价值加和即为事件a与其前面可以共同取得的事件的价值和的最大值。

那么为什么不考虑事件a后面的事件呢,当遍历到后面的事件时,按照同样的方法与前面的事件的最大价值加和,此时若事件a不是这个最大值对应的事件,则事件a与后面事件的加和必然没有最大值对应事件与后面事件加和大,因此并不影响最终结果,换言之,我们将事件a和事件a后面发生的事件的和的大小问题推迟到处理后面的事件时一起处理,这样充分利用了前面已经处理过的事件的信息。

要实现前面所讲的思路,我们需要将事件按开始时间和结束时间分别排序,按开始时间遍历事件,确定事件的开始时间后遍历结束时间数组,找到所有开始时间之前结束的事件并更新这些事件中的价值的最大值,此处仅记录最大值即可,无需保存其他信息。随后将当前事件的价值和最大值加和并与全局最大值比较并更新全局最大值。

代码

 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
class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        int n = events.size();
        // 创建两个数组分别存储按开始时间和结束时间排序的事件
        vector<pair<int, int>> starts(n);
        vector<pair<int, int>> ends(n);

        for(int i = 0; i < n; i++) {
            starts[i] = {events[i][0], i};
            ends[i] = {events[i][1], i};
        }

        sort(starts.begin(), starts.end());
        sort(ends.begin(), ends.end());

        int maxValue = 0;  // 记录已处理事件中的最大价值
        int result = 0;
        int endIndex = 0;

        for(int i = 0; i < n; i++) {
            int currentStart = starts[i].first;
            int currentIndex = starts[i].second;

            while(endIndex < n && ends[endIndex].first < currentStart) {
                int idx = ends[endIndex].second;
                maxValue = max(maxValue, events[idx][2]);
                endIndex++;
            }

            result = max(result, events[currentIndex][2] + maxValue);

        }

        return result;
    }
};

day276 2024-12-09

3152. Special Array II

An array is considered special if every pair of its adjacent elements contains two numbers with different parity.

You are given an array of integer nums and a 2D integer matrix queries, where for queries[i] = [fromi, toi] your task is to check that subarray nums[fromi..toi] is special or not.

Return an array of booleans answer such that answer[i] is true if nums[fromi..toi] is special.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1209UA8a8x4jop6Z.png

题解

本题若要query中的每个查询范围内都满足相邻元素具有不同的奇偶性,本题数字的奇偶性是有用的信息,数字本身的数值没什么用,故可以先遍历数组确定每个数字和其相邻数字的奇偶性的差异,我们用奇偶性差异值来表示这一差异,若下标i和下标i+1的数字的奇偶性不同,则称下标i处的奇偶性差异值为1,否则为0。在判断相邻数字奇偶性差异的同时,构造一个前缀和数组,保存从开头到每个下标处的子数组中奇偶性差异值的和。

对于一个query范围,若范围内的所有数字和其相邻数字的奇偶性均不同,则范围内的数字的奇偶性差异值的和应该和该范围的长度-1相等(每个奇偶差异值均为1),否则不同。求某个范围内的数字和是一个之前已经做过多次的题目,这种题目可以用前缀和求解,只需先求出确定了奇偶性的nums数组的全部下标的奇偶性差异前缀和,对每个query范围就可以快速确定范围内的子数组奇偶性差异和为多少。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
    vector<bool> isArraySpecial(vector<int>& nums, vector<vector<int>>& queries) {
        vector<int> prefix;
        prefix.push_back(0);
        int pre = 0;
        for(int i=0;i<nums.size()-1;i++){
            pre += (nums[i]%2)^(nums[i+1]%2);
            prefix.push_back(pre);
        }
        vector<bool> result;
        for(const auto& query : queries){
            if(query[1]-query[0] == prefix[query[1]]-prefix[query[0]]){
                result.push_back(true);
            }else{
                result.push_back(false);
            }
        }
        return result;
    }
};

day277 2024-12-10

2981. Find Longest Special Substring That Occurs Thrice I

You are given a string s that consists of lowercase English letters.

A string is called special if it is made up of only a single character. For example, the string “abc” is not special, whereas the strings “ddd”, “zz”, and “f” are special.

Return the length of the longest special substring of s which occurs at least thrice, or -1 if no special substring occurs at least thrice.

A substring is a contiguous non-empty sequence of characters within a string.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1210VCWus8hjQqrD.png

题解

本题若在遇到重复字符时直接统计字符的个数,由于相同字符的个数情况可能有很多,如当有四个重复字符时,其实包含了四个重复一次的字符,三个重复两次的字符,两个重复三次的字符及一个重复四次的字符,则每次遇到重复字符时,先将局部字符的个数全部记录,再利用一个哈希表,将每个重复个数的出现次数加入到哈希表对应的项中是可行的,但这样实际上需要将每个重复个数的字符串项都遍历一遍,如上面的例子,在有四个重复字符时,我们要在哈希表中分别更改1个,2个,3个,4个重复字符对应的项的值。

为了避免每次都要将所有重复字符的项都遍历一遍,可以想到如果我们先确定重复字符的个数,再去找这样的重复个数出现了几次就可以不用在意其他重复个数出现的情况了。如果这个重复个数的某个字符串在字符串中有三次及以上的出现次数,说明这个重复个数是可以取到的,否则不能取到,这种说法是不是感觉非常熟悉,没错这又是一个经典的二分法可以发挥作用的场景。存在一个临界值,使得临界值上成立,临界值下不成立,而要找的最大值也正是这个临界值。

使用二分法的左侧当然指向0,右边界可以先遍历数组,找到数组中相同字符最多重复了几次,以此作为右边界,再使用二分法,判断当前重复个数是否存在某个字符串出现了三次及以上(此处可使用滑动窗口,确定窗口内字符是否完全一样并用哈希表记录个数即可),最终找到刚好满足条件的临界个数即得结果。

代码

 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
class Solution {
public:
    int maximumLength(string s) {
        int maxrepeat = 0;
        char cur = ' ';
        int repeat = 0;
        for(const auto& ch : s){
            if (ch != cur){
                maxrepeat = max(maxrepeat, repeat);
                repeat = 1;
                cur = ch;
            }else{
                repeat++;
            }
        }
        maxrepeat = max(maxrepeat, repeat);
        int left = 0;
        while(left <= maxrepeat){
            int mid = (left+maxrepeat)/2;
            if(valid(s,mid)){
                left = mid+1;
            }else{
                maxrepeat = mid-1;
            }
        }
        if(maxrepeat == 0){
            return -1;
        }else{
            return maxrepeat;
        }
    }

    bool valid(string s, int repeat){
        vector<int> count(26,0);
        for(int i=0;i<s.size();i++){
            char cur = s[i];
            bool flag = true;
            for(int j=0;j<repeat;j++){
                if(s[i+j] != s[i]){
                    flag = false;
                    break;
                }
            }
            if(flag){
                count[s[i]-'a']++;
                if(count[s[i]-'a'] >= 3){
                    return true;
                }
            }
        }
        return false;
    }
};

day278 2024-12-11

2779. Maximum Beauty of an Array After Applying Operation

You are given a 0-indexed array nums and a non-negative integer k.

In one operation, you can do the following:

Choose an index i that hasn’t been chosen before from the range [0, nums.length - 1]. Replace nums[i] with any integer from the range [nums[i] - k, nums[i] + k]. The beauty of the array is the length of the longest subsequence consisting of equal elements.

Return the maximum possible beauty of the array nums after applying the operation any number of times.

Note that you can apply the operation to each index only once.

A subsequence of an array is a new array generated from the original array by deleting some elements (possibly none) without changing the order of the remaining elements.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1211Mrv12dH4NOvS.png

题解

首先注意本题要找的是最长的子序列而不是子数组,因此不需要数字之间相邻,数字的先后顺序在本题中也不重要。考虑题目中允许的操作为将任意一个数字加上k或者减去k,且该操作只能执行一次。则设当前的数字为i,则在i-k和i+k范围内的全部数字均可通过题目所述操作变为相同数字。这个范围的大小是固定的2k,因此可以使用滑动窗口不断确定在这个范围内的所有数字的个数。

使用滑动窗口要有一个窗口的增长和收缩条件,我们知道要将窗口的范围始终限制在2k,则可先移动窗口左端的数字,使其变大,再根据左侧数字确定右侧的数字范围,移动窗口右侧的指针。这要求数组必须是有序的,有序情况下窗口左右才可以随着指针的移动数值自然变大。因此要先将数组排序,再使用上述滑动窗口方法即可得最终结果。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
    int maximumBeauty(vector<int>& nums, int k) {
        sort(nums.begin(),nums.end());
        int left = 0;
        int right = 0;
        int n = nums.size();
        int maxlen = 0;
        while(left<n){
            while(right<n && nums[right] <= nums[left]+2*k){
                right++;
            }
            maxlen = max(maxlen, right-left);
            while(left<n-1 && nums[left+1]==nums[left]){
                left++;
            }
            left++;
        }
        return maxlen;
    }
};

总结

看了看最快的示例代码,发现了一个非常妙的思路,即可以直接通过设定每个nums中的值可以覆盖的数值范围,超出范围则减去相应的数字个数。这种方式可以直接通过不断加和的方式自动求得不同范围内的有效数字有多少个。

用这种方法要根据nums中的值来构造数组,数组的大小为nums中的最大值加上2*k+1。这样构造的数组覆盖了nums中的全部值范围,上面说到的设定覆盖范围可以通过例子来理解,如当前数字为1,k的值为2,则可以直接将构造的新数组中下标为1处的值加1,而将1+2*k+1,即6处的值减1。含义即为从1~1+2k范围内的数字都可以通过加减k的方式变成同一个数字,所以已有的1在这个范围内是一个有效的计数,但一旦离开这个范围,1就无法通过变换和其他数字变成同一个数了,因此1就不再是一个有效计数,因此可以减去数字1的计数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Solution {
public:
    int maximumBeauty(vector<int>& nums, int k) {
        int m = *max_element(nums.begin(), nums.end()) + k * 2 + 2;
        vector<int> d(m);
        for (int x : nums) {
            d[x]++;
            d[x + k * 2 + 1]--;
        }
        int ans = 0, s = 0;
        for (int x : d) {
            s += x;
            ans = max(ans, s);
        }
        return ans;
    }
};

day279 2024-12-12

2558. Take Gifts From the Richest Pile

You are given an integer array gifts denoting the number of gifts in various piles. Every second, you do the following:

Choose the pile with the maximum number of gifts. If there is more than one pile with the maximum number of gifts, choose any. Leave behind the floor of the square root of the number of gifts in the pile. Take the rest of the gifts. Return the number of gifts remaining after k seconds.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1212dmH4CaCDkGtz.png

题解

本题是一道比较常规的模拟问题,对题目中每次都取所有礼物中的最大值自然想到这是一个最大堆的典型场景。先构建最大堆同时求出所有礼物个数的总和。使用最大堆记录礼物的数量,每次都取堆顶的个数,对其按照题目要求取平方根,计算原数字和平方根后取下整的数字的差并从总和中减去,再将新的取下整后的数字放入最大堆中。如此重复k次即得最终结果。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
public:
    long long pickGifts(vector<int>& gifts, int k) {
        priority_queue<int> pq;
        long long int sum = 0;
        for(const auto& gift : gifts){
            sum += gift;
            pq.push(gift);
        }
        for(int i=0;i<k;i++){
            int newgift = (int)floor(sqrt(pq.top()));
            sum -= pq.top()-newgift;
            pq.pop();
            pq.push(newgift);
        }
        return sum;
    }
};

day280 2024-12-13

2593. Find Score of an Array After Marking All Elements

You are given an array nums consisting of positive integers.

Starting with score = 0, apply the following algorithm:

Choose the smallest integer of the array that is not marked. If there is a tie, choose the one with the smallest index. Add the value of the chosen integer to score. Mark the chosen element and its two adjacent elements if they exist. Repeat until all the array elements are marked. Return the score you get after applying the above algorithm.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1213RjyAoXeqr6rX.png

题解

本题每次都要选择未标记的数字中最小的那个,并将其相邻位置标记,可以构造一个布尔型标记数组来记录被标记的下标位置,该数组下标和nums中的下标对应。因为每次都选最小的,故先排序后直接遍历就能得到当前的最小数字,只需再判断其是否被标记即可。

对nums中的数字先构造结构体,将数字和对应的下标绑定,再使用稳定的排序方法对数字进行排序。按照上述算法不断遍历有序数组,用一个变量记录当前已经被标记的个数,当标记个数和数组长度相同时结束遍历并返回最终分数。

代码

 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
class Solution {
public:
    long long findScore(vector<int>& nums) {
        int n = nums.size();
        vector<bool> mark(n,false);
        vector<pair<int,int>> sorted;
        for(int i=0;i<n;i++){
            sorted.push_back({nums[i],i});
        }
        sort(sorted.begin(),sorted.end());
        int marked = 0;
        long long int score = 0;
        for(const auto& sort_num : sorted){
            if(!mark[sort_num.second]){
                score += sort_num.first;
                mark[sort_num.second] = true;
                marked++;
                if(sort_num.second > 0 && !mark[sort_num.second-1]){
                    mark[sort_num.second-1] = true;
                    marked++;
                }
                if(sort_num.second < n-1 && !mark[sort_num.second+1]){
                    mark[sort_num.second+1] = true;
                    marked++;
                }
                if(marked == n){
                    break;
                }
            }
        }
        return score;
    }
};

day281 2024-12-14

2762. Continuous Subarrays

You are given a 0-indexed integer array nums. A subarray of nums is called continuous if:

Let i, i + 1, …, j be the indices in the subarray. Then, for each pair of indices i <= i1, i2 <= j, 0 <= |nums[i1] - nums[i2]| <= 2. Return the total number of continuous subarrays.

A subarray is a contiguous non-empty sequence of elements within an array.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1214xvWH0FkwSGld.png

题解

本题思考题目条件,要求子数组中任意两个数字之间差的绝对值不超过2。则可使用双指针来指示子数组当前的范围,同时使用两个变量分别记录当前子数组内的最大值和最小值。先将右指针向后移动,同时更新子数组内的最大值和最小值,判断二者的差是否小于等于2。等于则说明该子数组满足条件,要给结果加上子数组长度(相当于新加入的数字自身,加上数字和前面的各个子数组的组合)。

当新遍历的数使得数组内的最大值和最小值差大于2时,考虑两种情况,如上述情况数字范围为3~5,则若新数字为6,则之前的数组中若尾部仅包含4,5两个数字,4,5,6仍然符合题目要求的差的绝对值为2。故找到之前子数组的尾部仅包含4,5两个数字的部分保留。对于7同理。但若数字大于7,则之前的数组中不可能存在可以和7组合满足条件的数字,因此直接从7开始向后遍历即可。由此得出对于新数字使得子数组极差大于2时,若新数字和数组内的最大值或最小值差的绝对值不超过2,此时移动左指针直到子数组中的数字全部在新的给定范围内(具体实现上,只需超出范围的数字的个数为0即可)。否则直接从新数字开始向后遍历数组(将左右指针都设为该数字的位置)。

将每次新找到的满足条件的子数组长度加和即得最终结果。

代码

 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
class Solution {
public:
    long long continuousSubarrays(vector<int>& nums) {
        long long int result = 0;
        int left = 0;
        int right = 0;
        int n = nums.size();
        unordered_map<int,int> count;
        int current_min = nums[0];
        int current_max = nums[0];
        
        while (right < n) {
            if (right > left && 
                (abs(nums[right] - current_max) > 2 && 
                 abs(nums[right] - current_min) > 2)) {
                count.clear();
                left = right;
                current_max = nums[right];
                current_min = nums[right];
            }
            
            count[nums[right]]++;
            
            current_max = max(current_max, nums[right]);
            current_min = min(current_min, nums[right]);
            
            while (current_max - current_min > 2) {
                count[nums[left]]--;
                if (count[nums[left]] == 0) {
                    count.erase(nums[left]);
                }
                left++;
                
                current_max = nums[left];
                current_min = nums[left];
                for (auto& pair : count) {
                    if (pair.second > 0) {
                        current_max = max(current_max, pair.first);
                        current_min = min(current_min, pair.first);
                    }
                }
            }
            
            // 计算当前窗口内的所有有效子数组数量
            result += (right - left + 1);
            right++;
        }
        
        return result;
    }
};

day282 2024-12-15

1792. Maximum Average Pass Ratio

There is a school that has classes of students and each class will be having a final exam. You are given a 2D integer array classes, where classes[i] = [passi, totali]. You know beforehand that in the ith class, there are totali total students, but only passi number of students will pass the exam.

You are also given an integer extraStudents. There are another extraStudents brilliant students that are guaranteed to pass the exam of any class they are assigned to. You want to assign each of the extraStudents students to a class in a way that maximizes the average pass ratio across all the classes.

The pass ratio of a class is equal to the number of students of the class that will pass the exam divided by the total number of students of the class. The average pass ratio is the sum of pass ratios of all the classes divided by the number of the classes.

Return the maximum possible average pass ratio after assigning the extraStudents students. Answers within 10-5 of the actual answer will be accepted.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1215H3eQAQ3kny1p.png

题解

本题首先要了解一个分数自身的性质,对一个分数给分子分母同时加1,会使得分数的值向1靠近,意味着若分数小于1,则同时加1会使分数变大,分数大于1,同时加1会使分数变小。

本题学生的通过率都是小于等于1的,因此给通过率小于1的class分配一个通过的学生会使分子分母同时加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

class Solution {
public:
    struct Class {
        int pass;
        int total;
        double delta;
        
        Class(int p, int t) {
            pass = p;
            total = t;
            delta = (double)(p + 1) / (t + 1) - (double)p / t;
        }
    };
    
    double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
        // 按照delta降序排列的优先队列
        auto comp = [](const Class& a, const Class& b) {
            return a.delta < b.delta;
        };
        priority_queue<Class, vector<Class>, decltype(comp)> pq(comp);
        
        for (const auto& c : classes) {
            pq.push(Class(c[0], c[1]));
        }
        
        while (extraStudents--) {
            Class curr = pq.top();
            pq.pop();
            
            curr.pass++;
            curr.total++;
            curr.delta = (double)(curr.pass + 1) / (curr.total + 1) - (double)curr.pass / curr.total;
            
            pq.push(curr);
        }

        double sum = 0;
        int n = classes.size();
        while (!pq.empty()) {
            const Class& c = pq.top();
            sum += (double)c.pass / c.total;
            pq.pop();
        }
        
        return sum / n;
    }
};

day283 2024-12-16

3264. Final Array State After K Multiplication Operations I

You are given an integer array nums, an integer k, and an integer multiplier.

You need to perform k operations on nums. In each operation:

Find the minimum value x in nums. If there are multiple occurrences of the minimum value, select the one that appears first. Replace the selected minimum value x with x * multiplier. Return an integer array denoting the final state of nums after performing all k operations.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1216vNu3jp82Fvij.png

题解

本题使用最小堆可解。构造一个pair将数字和对应的下标存储起来,再将这些pair插入最小堆,此处注意定义最小堆的比较规则,当比较数值后,若数值相同还要比较下标。每次从最小堆中弹出顶部数字,按照下标将nums对应数字和multiplier相乘即可。

代码

 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 Solution {
public:
    vector<int> getFinalState(vector<int>& nums, int k, int multiplier) {
        auto cmp = [](const pair<int,int> a, const pair<int,int> b){
            if(a.first == b.first){
                return a.second > b.second;
            }
            return a.first>b.first;
        };
        priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(cmp)> pq(cmp);
        for(int i=0;i<nums.size();i++){
            pq.push({nums[i],i});
        }
        while(k>0){
            auto num = pq.top();
            nums[num.second] *= multiplier;
            num.first *= multiplier;
            pq.pop();
            pq.push(num);
            k--;
        }
        return nums;
    }
};

day284 2024-12-17

2182. Construct String With Repeat Limit

You are given a string s and an integer repeatLimit. Construct a new string repeatLimitedString using the characters of s such that no letter appears more than repeatLimit times in a row. You do not have to use all characters from s.

Return the lexicographically largest repeatLimitedString possible.

A string a is lexicographically larger than a string b if in the first position where a and b differ, string a has a letter that appears later in the alphabet than the corresponding letter in b. If the first min(a.length, b.length) characters do not differ, then the longer string is the lexicographically larger one.

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/12173l9LfAwG3dJo.png

题解

本题先统计字符串s中各个字母的字数,再根据题意构造最大字符串,构造最大字符串需要将所有字符从最大字符开始排列,但排列时相同字符的连续重复次数不能超过repeatLimit。则每次当最大字符重复了repeatLimit后,需要插入一个次大字符,再继续插入最大字符,因此可以使用双指针,分别指示当前的最大字符和次大字符,再按照题目要求通过不断交替插入尽可能多的最大字符和用于分隔的次大字符来构造整个字符串。

这样得到的就是最大的字符串,对于剩余的多余字符,如剩下了很多a没有使用并不影响这个字符串的最大性,因为题目只要求使用s中的字符得到最大字符串,不要求将字符全部用完。

代码

 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
class Solution {
public:
    string repeatLimitedString(string s, int repeatLimit) {
        int maxindex = 0;
        int secondindex = -1;
        vector<int> count(26,0);
        for(const auto& ch : s){
            count[ch-'a']++;
        }
        for(int i=25;i>=0;i--){
            if(count[i] > 0){
                maxindex = i;
                for(int j=i-1;j>=0;j--){
                    if(count[j]>0){
                        secondindex = j;
                        break;
                    }
                }
                break;
            }
        }
        string result;
        int limit = repeatLimit;
        while(maxindex >= 0){
            while(count[maxindex] > 0 && limit > 0){
                result.push_back('a' + maxindex);
                count[maxindex]--;
                limit--;
            }
            
            if(count[maxindex] > 0){
                if(secondindex >= 0){
                    if(count[secondindex] > 0){
                        result.push_back('a' + secondindex);
                        count[secondindex]--;
                        limit = repeatLimit;
                    } else {
                        secondindex = -1;
                        for(int i = maxindex-1; i >= 0; i--){
                            if(count[i] > 0){
                                secondindex = i;
                                break;
                            }
                        }
                        if(secondindex < 0) break; 
                        result.push_back('a' + secondindex);
                        count[secondindex]--;
                        limit = repeatLimit;
                    }
                } else {
                    break; 
                }
            } else {
                maxindex = secondindex;
                secondindex = -1;
                for(int i = maxindex-1; i >= 0; i--){
                    if(count[i] > 0){
                        secondindex = i;
                        break;
                    }
                }
                limit = repeatLimit;
            }
        }
        return result;
    }
};

总结

这次代码写的有点丑陋了,但思路上是没问题的,可以参考下其他人写的思路基本一样的代码

 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
class Solution {
public:
    string repeatLimitedString(string s, int repeatLimit) {
        vector<int> freq(26, 0);
        for (char ch : s) {
            freq[ch - 'a']++;
        }

        string result;
        int currentCharIndex = 25;  // Start from the largest character
        while (currentCharIndex >= 0) {
            if (freq[currentCharIndex] == 0) {
                currentCharIndex--;
                continue;
            }

            int use = min(freq[currentCharIndex], repeatLimit);
            result.append(use, 'a' + currentCharIndex);
            freq[currentCharIndex] -= use;

            if (freq[currentCharIndex] >
                0) {  // Need to add a smaller character
                int smallerCharIndex = currentCharIndex - 1;
                while (smallerCharIndex >= 0 && freq[smallerCharIndex] == 0) {
                    smallerCharIndex--;
                }
                if (smallerCharIndex < 0) {
                    break;
                }
                result.push_back('a' + smallerCharIndex);
                freq[smallerCharIndex]--;
            }
        }

        return result;
    }
};