1. 程式人生 > 實用技巧 >第15周Leetcode記錄

第15周Leetcode記錄

12.22 71. 兩地排程

公司計劃面試 2N 人。第 i 人飛往 A 市的費用為costs[i][0],飛往 B 市的費用為 costs[i][1]。

返回將每個人都飛到某座城市的最低費用,要求每個城市都有 N 人抵達。

輸入:[[10,20],[30,200],[400,50],[30,20]]
輸出:110
解釋:
第一個人去 A 市,費用為 10。
第二個人去 A 市,費用為 30。
第三個人去 B 市,費用為 50。
第四個人去 B 市,費用為 20。

最低總費用為 10 + 30 + 50 + 20 = 110,每個城市都有一半的人在面試。

最優解思路

公司首先將這 2N 個人全都安排飛往 BB 市,再選出 N 個人改變它們的行程,讓他們飛往 A 市。如果選擇改變一個人的行程,那麼公司將會額外付出 price_A - price_B 的費用,這個費用可正可負。因此最優的方案是,選出 price_A - price_B

最小的 NN 個人,讓他們飛往 A 市,其餘人飛往 B 市。

最優解

class Solution:
    def twoCitySchedCost(self, costs: List[List[int]]) -> int:
        # Sort by a gain which company has 
        # by sending a person to city A and not to city B
        costs.sort(key = lambda x : x[0] - x[1])
        
        total = 0
        n = len(costs) // 2
        # To optimize the company expenses,
        # send the first n persons to the city A
        # and the others to the city B
        for i in range(n):
            total += costs[i][0] + costs[i + n][1]
        return total

12.23 72. 最長單詞

給定一組單詞words,編寫一個程式,找出其中的最長單詞,且該單詞由這組單詞中的其他單詞組合而成。若有多個長度相同的結果,返回其中字典序最小的一項,若沒有符合要求的單詞則返回空字串。

輸入: ["cat","banana","dog","nana","walk","walker","dogwalker"]
輸出: "dogwalker"
解釋: "dogwalker"可由"dog"和"walker"組成。

最優解思路

先把字串陣列排序,字串長的在前面,相同長度的字典序小的在前面,排好序後加入到set裡判斷是否包含,從第一個字串開始判斷,看是否由其它字串組成,這裡可以用遞迴
遞迴出口: 如果字串長的長度為0,說明遍歷完了,之前的都滿足條件,返回true
遞迴操作: 遍歷字串的第0個位置開始,判斷set裡是否有,如果0到i的字串正好包含在set裡,下次從i+1的位置開始判斷,直到遍歷完了,字串長度為0,沒找到則返回false

最優解

class Solution {
    public String longestWord(String[] words) {
        Arrays.sort(words,(o1,o2)->{
            if(o1.length() == o2.length())
                return o1.compareTo(o2);
            else{
                return Integer.compare(o2.length(),o1.length());
            }
        });

        Set<String> set = new HashSet<>(Arrays.asList(words));
        for(String word : words){
            set.remove(word);
            if(find(set,word))
                 return word;
        }
        return "";
    }

    public boolean find(Set<String> set, String word){
        if(word.length() == 0)
            return true;
        for(int i = 0; i < word.length(); i++){
            if(set.contains(word.substring(0,i+1)) && find(set,word.substring(i+1)))
                return true;
        }
        return false;
    }
}

12.26 73. 二叉搜尋樹迭代器

實現一個二叉搜尋樹迭代器。你將使用二叉搜尋樹的根節點初始化迭代器。

呼叫 next() 將返回二叉搜尋樹中的下一個最小的數。

BSTIterator iterator = new BSTIterator(root);
iterator.next();    // 返回 3
iterator.next();    // 返回 7
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 9
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 15
iterator.hasNext(); // 返回 true
iterator.next();    // 返回 20
iterator.hasNext(); // 返回 false

思路

中序遍歷樹,各節點的值加入列表,pop。

最優解

class BSTIterator:

    def __init__(self, root: TreeNode):
        
        # Array containing all the nodes in the sorted order
        self.nodes_sorted = []
        
        # Pointer to the next smallest element in the BST
        self.index = -1
        
        # Call to flatten the input binary search tree
        self._inorder(root)
        
    def _inorder(self, root):
        if not root:
            return
        self._inorder(root.left)
        self.nodes_sorted.append(root.val)
        self._inorder(root.right)

    def next(self) -> int:
        """
        @return the next smallest number
        """
        self.index += 1
        return self.nodes_sorted[self.index]

    def hasNext(self) -> bool:
        """
        @return whether we have a next smallest number
        """
        return self.index + 1 < len(self.nodes_sorted)

12.26 74. 買賣股票的最佳時機

給定一個整數陣列 prices,其中第 i 個元素代表了第 i 天的股票價格 ;非負整數 fee 代表了交易股票的手續費用。

你可以無限次地完成交易,但是你每筆交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。

返回獲得利潤的最大值。

注意:這裡的一筆交易指買入持有並賣出股票的整個過程,每筆交易你只需要為支付一次手續費。

輸入: prices = [1, 3, 2, 8, 4, 9], fee = 2
輸出: 8
解釋: 能夠達到的最大利潤:  
在此處買入prices[0] = 1
在此處賣出 prices[3] = 8
在此處買入 prices[4] = 4
在此處賣出 prices[5] = 9
總利潤:((8 - 1) - 2) + ((9 - 4) - 2) = 8.

最優解思路

解一:動態規劃

考慮 dp[i][0]的轉移方程,如果這一天交易完後手裡沒有股票,那麼可能的轉移狀態為前一天已經沒有股票,即 dp[i-1][0],或者前一天結束的時候手裡持有一支股票,即 dp[i-1][1],這時候我們要將其賣出,並獲得prices[i] 的收益,但需要支付 fee 的手續費。因此為了收益最大化,我們列出如下的轉移方程:

dp[i][0]=max{dp[i−1][0],dp[i−1][1]+prices[i]−fee}

再來按照同樣的方式考慮 dp[i][1] 按狀態轉移,那麼可能的轉移狀態為前一天已經持有一支股票,即 dp[i-1]][1],或者前一天結束時還沒有股票,即dp[i-1]][0] ,這時候我們要將其買入,並減少prices[i] 的收益。可以列出如下的轉移方程:

dp[i][1]=max{dp[i−1][1],dp[i−1][0]−prices[i]}

對於初始狀態,根據狀態定義我們可以知道第 0 天交易結束的時候有dp[0][0]=0 以及dp[0][1]=-price[0].

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        dp = [[0, -prices[0]]] + [[0, 0] for _ in range(n - 1)]
        for i in range(1, n):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
        return dp[n - 1][0]
解二:貪心演算法

當我們賣出一支股票時,我們就立即獲得了以相同價格並且免除手續費買入一支股票的權利

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        buy = prices[0] + fee
        profit = 0
        for i in range(1, n):
            if prices[i] + fee < buy:
                buy = prices[i] + fee
            elif prices[i] > buy:
                profit += prices[i] - buy
                buy = prices[i]
        return profit

12.28 75. LRU快取機制

運用你所掌握的資料結構,設計和實現一個 LRU (最近最少使用) 快取機制 。
實現 LRUCache 類:

LRUCache(int capacity) 以正整數作為容量 capacity 初始化 LRU 快取
int get(int key) 如果關鍵字 key 存在於快取中,則返回關鍵字的值,否則返回 -1 。
void put(int key, int value) 如果關鍵字已經存在,則變更其資料值;如果關鍵字不存在,則插入該組「關鍵字-值」。當快取容量達到上限時,它應該在寫入新資料之前刪除最久未使用的資料值,從而為新的資料值留出空間。

思路

LRU快取機制是通過hash表輔以雙向連結串列實現的。

  • 雙向連結串列按照被使用的順序儲存了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。
  • 雜湊表即為普通的雜湊對映(HashMap),通過快取資料的鍵對映到其在雙向連結串列中的位置。

最優解

class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:

    def __init__(self, capacity: int):
        self.cache = dict()
        # 使用偽頭部和偽尾部節點    
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        self.capacity = capacity
        self.size = 0

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        # 如果 key 存在,先通過雜湊表定位,再移到頭部
        node = self.cache[key]
        self.moveToHead(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        if key not in self.cache:
            # 如果 key 不存在,建立一個新的節點
            node = DLinkedNode(key, value)
            # 新增進雜湊表
            self.cache[key] = node
            # 新增至雙向連結串列的頭部
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                # 如果超出容量,刪除雙向連結串列的尾部節點
                removed = self.removeTail()
                # 刪除雜湊表中對應的項
                self.cache.pop(removed.key)
                self.size -= 1
        else:
            # 如果 key 存在,先通過雜湊表定位,再修改 value,並移到頭部
            node = self.cache[key]
            node.value = value
            self.moveToHead(node)
    
    def addToHead(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
    
    def removeNode(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def moveToHead(self, node):
        self.removeNode(node)
        self.addToHead(node)

    def removeTail(self):
        node = self.tail.prev
        self.removeNode(node)
        return node

最優解總結

hash表key是存出鍵,value是連結串列的索引,連結串列的值時value。這樣查詢,插入複雜度都是o1

get操作,判斷是否存在,不存在返回-1,存在把對應的連結串列索引換到頭部。

put操作,若存在和get操作類似,不存在則建立一個新的連結串列節點,放到頭部再判斷長度,超過規定長度就會從尾部去除。