【OfferX】常见题目
編程要素
1.代碼要完全對應思考的邏輯
每一個分支都要熟悉它是在做什么,對應的是哪一部分思考
2.考慮special case
常見的special case如下:
i.整數的最大值: 2147483647, -2147483648
ii.構成整數的數據,不能以0開頭:179. Largest Number
iii.
3.sort的坑
當使用sort時,如果按照下面的方式實現排序,可能存在溢出的情況
因為如果a-b的值超過int的范圍,就會發生反轉,這里應當如下使用:
Arrays.sort(nums, Comparator.comparingInt((a)->a));while(n-->0) 循環n次
Java常用的數據結構
| 堆 | PriorityQueue | remove() 彈出頭部; remove(i) 移除元素–復雜度O(n) | 數組 |
| 二叉樹 | TreeMap | ||
| 隊列 | Queue | remove, removeLast() | 鏈表LinkedList |
| 快速排序 | |||
二進制
1.位操作
n-1 將n的最右側的第一個1以及余下的0取反
n&(n-1) 將n的最右側都一個1置0
n&(n-1) == 0 判斷一個數字是否是2的冪(是否只包含一個1)
n & (k-1) 求n%k,即模,k是2的冪。
n >>>k 即 n/(2^k),對2的冪做除法可以使用移位運算
c=0;while( (n>>>=1)!=0)++c; n是2的冪,求n的階
n^0=n 0與任何數異或,結果不變
使用異或交換兩個數字
int a=1,b=2; a=a^b; b=a^b; a=a^b;位運算細節:
n>>1 在java中,n是有符號數,則右移會導致補1
n>>>1 補0(盡量使用該操作)
2.數組中有一個數字出現了一次,其他數字都出現了兩次,求這個數
使用異或運算,結果就是該數字。
3.數組中有兩個數字出現了一次,其他數字都出現了兩次,求這兩個數
按第0位是0或1對數組中的數字進行分類,得兩個分組,如果兩個分組的數量是奇數,則說明兩個數字被分配在不同的組里面,分別使用異或即可求得;否則,兩個數字的尾號是相同的,可能存在于其中任何一組
然后按第1位是0或者1對數組進行分類,如果仍然是偶數,按第3位…重復下去,直到有一位能夠將兩個數分到不同的組(其實這個過程就是查找兩個數的第一位不同)
最后找到兩組都為奇數,然后使用異或即可求得兩個數。
關鍵點:尋找兩個數的第一個不同的數字出現的位置
加速:由于知道異或結果,所有可以快速找出第一位不同的位置,然后按這個位置進行分組即可。
4.數組中一個數字出現了一次,其他數字出現了三次,求出現一次的數字
仍然按第0位對數字進行分組,則數量mod 3=1的那一組就是該數所在的組,確定第0位
然后在剩下的組里面按第1位進行分組,數量mod 3 = 1的那一組仍然是該數所在的組,確定第1位
以此類推。
關鍵點:按位分組,確定數字所在的組
5.計算[0,n]上各個數的1的個數
n&(n-1) 可以將第一個1置為0,從而循環n次即可求得count(n)
所以存在遞推: count(n) = count(n & (n-1)) + 1
可以使用緩存進行計算,只需要保證計算i時,小于等于i的所有數字都已經計算完成,從而在O(n)時間內完成計算。
6.不用比較,使用位運算返回a,b中的最大值
其實最大值由最高位1所在的位置決定
排序和查找
1.快速排序
以A[i]為基準元素,第1輪排序的結果是什么?
快速排序偽代碼參考https://blog.csdn.net/xhdxhdxhd/article/details/104229669
2.二分查找
二分查找
5.歸并排序
題目:合并K個排序的數組
3.分塊查找
思想:首先
4.二叉查找樹
關鍵點:極限情況下的數高度是n,所有退化為普通的有序表。
鏈表
1.反轉鏈表
2.刪除鏈表倒數第K個節點
關鍵點:刪除單向鏈表節點的關鍵是定位到前一個節點
不考慮遞歸,使用循環,首先需要遍歷鏈表獲得鏈表大小n,重新遍歷鏈表獲得鏈表的倒數第K+1個節點,也就是指針移動 n - K - 1次。
如果使用遞歸,則需要在每個狀態下記錄當前位置i,在最終狀態下記錄n,則倒數第K+1個節點的位置 n - i = K + 1, i = n - K - 1
2.鏈表的環
題目:判斷鏈表是否存在環, 并返回環的入口
第一步,使用兩個指針,前者步長為2,后者步長為1遍歷鏈表,如果有一個指針為空,則沒有環;否則它們必然相遇在環中的某個點。
第二部,首先計算環的長度n,然后重新使用兩個指針,第一個指針先走n步,然后它們以步長1遍歷鏈表,最終相遇的點就是環的入口。
證明:待續
0 1 2 3 4 5 6 7 8 9 5 6 7 8 9
設環的長度是n,初始部分長度是k。
我們設指針在第i輪的位置是f(i)
(i>k)
顯然 f(i,1) = (i - k + 1)%n
其實上面的算法還是復雜了,維護指針時,在快慢指針相遇時,只需要從頭指針開始同步遍歷直到相遇即可。
public class Solution {public ListNode detectCycle(ListNode head) {ListNode slow = head, fast = head, start = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) {while (slow != start) {slow = slow.next;start = start.next;}return slow;}}return null;} }還可以考慮將鏈表反轉
ListNode* reverseList(ListNode* head) {ListNode* prev = NULL;ListNode* follow = NULL;while (head){follow = head->next;head->next = prev;prev = head;head = follow;}return prev; } bool hasCycle(ListNode *head) {ListNode* rev = reverseList(head);if (head && head->next && rev == head){return true;}return false; }4.兩個鏈表的第一個公共點
需要從尾端對其兩個鏈表,因此需要兩個棧,來存儲兩個鏈表節點。然后從棧頂開始比較,記錄最后相等的那個節點,就是公共點。
5.移除指定元素
題目:203. Remove Linked List Elements
解:保持循環不變式:prev.next = head
class Solution {public ListNode removeElements(ListNode head, int val) {while(head!=null && head.val==val)head=head.next;if(head==null)return null;ListNode res = head;ListNode prev = head;head = head.next;// invariant: prev.next = headwhile(head!=null){if(head.val==val){head = prev.next = head.next;}else{prev = prev.next;head = head.next;}}return res;} }棧
1.設計一個getMin,push, pop為O(1)時間的棧
對正常棧的狀態進行記錄,棧的棧頂元素稱為狀態,每個元素都記錄其作為棧頂元素時的最小值。入棧時,根據加入的值與棧頂最小值比較,然后更新;出棧時,最小棧彈出棧頂元素。
第二個輔助棧稱為最小棧。
2.使用兩個棧實現隊列
一個棧負責壓入,另一個棧負責彈出。當需要彈出元素時,把第一個棧的所有元素都彈入第二個棧中,實現了順序的逆轉。
只要第二個棧有元素,就優先彈出。
堆
1.從數組構建一個堆
堆有兩個關鍵操作:1.冒泡(siftUp) 2.下沉(siftDown)
以最小堆為例,新增元素時,從最底部開始,向上冒泡,如果小于父元素,與父元素交換即可。此操作命名為siftUp
抽象幾個操作: parent(i), left(i), right(i) 分別表示節點i的父節點,左子節點,右子節點
一種實現:數組arr[0]用于存儲數量;
數組中父節點計算: parent(i) = i/2
左子節點:2i, 右子節點:2i + 1
偽代碼:
siftUp(arr,i):while i > 1 and arr[i] < arr[i/2] :exchange arr[i], arr[i/2]i = i/2siftDown(arr,i):while i < n/2:find min of arr[i], arr[2*i], arr[2*i+1] as arr[k]if k != i:exchange arr[i], arr[k]i=kelse:break二叉樹
1.前序,中序,后序遍歷確定位置
首先通過先序遍歷遍歷,確定根節點
2.構建遍歷鏈表
關鍵點:使用靜態變量記錄遍歷順序
3.紅黑樹
紅黑樹的幾種翻轉操作, LL, LR, RL, RR等幾種類型。
數組
1.最小的k個數
使用一個大小為k的最大堆維護最小的k個數,每當新增元素時,如果該元素小于最大堆的值,說明它更小,則移除頭部,將其加入到堆中。
2.數組中出現次數超過一半的數字
解1:實際上,該數組的中位數一定是這個數字。所以使用一個最大堆和一個最小堆仿照上面的解法可以解決這個問題,只不過還有更優的解法。
解2:因為這個元素排序后一定出現在中間位置,所以實際上可以轉化為尋找數組的第n/2個元素。設f(A,i,j,k)為在數組A[i,j]中尋找第k大的元素,則
如果將A[i,j]分區為[i,r],[r+1,j]兩個部分,
如果[i,r]元素大于等于k,則重復子問題f(A,i,r,k)即可;
如果[i,r]元素小于k,則在右邊查找, f(A,r+1,r,k-i+r-k)
約束條件:1<=k<=r-i+1
終止條件:i==j
使用Lomuto分區,可以保證每次確定第r個元素;使用Hoare分區則只能將問題規模降為1。
解3:(暫時沒有理解為什么這樣能夠選擇出)遍歷數組,并記錄最后一次出現最多的元素e和相應的次數,如果下一個元素和e相同,則次數增加;如果不同,如果次數大于0,則次數減1;如果次數等于0,則將次數設置為1,且更新結果為當前元素
遍歷完成后,結果就是最后一次將次數置為1的元素。
3.數據流的中位數
中位數涉及中間的兩個數據,因此肯定要對數組進行排序。
我們可以將排序后前一半元素存儲在一個最大堆中,排序后的后一半元素存在在一個最小堆中,如果元素數量是偶數,則顯然取兩個堆的最大值和最小值取平均值即可;如果元素數量是奇數,我們將中間元素限定為包含在前一個堆中,因此中位數就是最大堆的的最大值。
由于數據流涉及更新,每新增一個數時,如果新增之前有偶數個元素,則將這個數加入到最大堆中;如果新增之前有奇數個元素,則將這個元素加入到最小堆中。
4.連續子數組的最大和
問題:求數組的連續子數組的和的最大值
解:考慮A[i]和[i,j-1]的關系, 以A[i]結尾的連續子數組和不以A[i]結尾的連續子數組。因為連續子數組的性質就是,A[j]只能影響以A[j-1]結尾的子數組,因此其他的值都可以忽略。
f(j) 表示以A[j]結尾的最大連續子數組和
則 f(j) = max { f(j-1) + A[j], A[j] }
最終的最大值就是f(i)的最大值
5.數組中的逆序對數
問題:在數組中的A[i]和A[j] (i<j),如果A[i] > A[j],稱其為逆序。求數組中的逆序對數
解:考慮[0,i]區間中小于A[i+1]的元素數量,就是A[i+1]逆序數量。
因此,考慮使用一個二叉搜索樹來確定元素的數量。
但是注意,手動實現一個平衡的二叉樹相當困難,因此,本題可以使用歸并排序的思想,歸并排序需要額外的空間來存儲合并的元素。
在分治的過程中,如果兩邊的數組已經排序完成,則統計逆序的十分方便。
6.剪繩子(切割問題)
題目:給定一段長為n的繩子,將其剪成m段,計算所有段段長的乘積,求最大值。
解:考慮問題的第i段與第i-1段的關系,考慮第i段長為1的繩子片段,它可以選擇與 i-1合并,也可以不合并。
使用f(i)表示解決[0,i]的問題,則
選擇1:不與i-1合并
則第i段是游離的,長度為1,將此狀態記錄為g(i)表示第i段不與前i-1進行合并。由于i與i-1分離,所以繩子被分成兩段,變成子問題: f(i-1)*1
選擇2:與i-1進行合并
此狀態即為h(i), 則 h(i)可以轉化為f的問題
因為 i與i-1合并,所以末端的長度是2,此時剩余f(i-2)的問題,但是此時問題狀態還包括右側的長度,因此使用擴展f的狀態f(i,k)表示解決 [0,i]的問題,且右側還有k個長度
則h(i) = f(i-2,2)
由于擴展f后多出了k,所以需要對選擇1和選擇2重新考慮
若k==0,則選擇1和選擇2如上;若k>0, 則將k視為第i+1個元素即可
所以最終有:
f(i,k) = 若 k \== 0: max {f(i-1,0)*1, f(i-2,2) } 若k>0,則將k視為一個單獨的元素: max {不與i合并: f(i,0) * k與i合并: f(i-1,k+1) }解2:上面的解法實際上過于復雜,實際上繩子一旦在某個點切斷,就是兩個獨立的子問題了,因此有 f(n) = max { f(n-i) * f(i) }
解3:貪心算法
實際上由于乘法的性質,當n>=5時,比如n=6或者n=12, 只需要將繩子分成3段一份即可,將得到3^k;當n<=4時,不用切割即是最大值
關鍵點:切割問題可以劃分為獨立的子問題進行簡單求解。
滑動窗口的最大值(單調棧)
問題:給定一個數組,大小為n,一個窗口m(m<=n), 在窗口滑動過程中會產生該區間內的最大值,求滑動產生的最大值。
解:單調棧或者單調隊列是解決這類區間最值問題的關鍵。
我們考察窗口隊列中的最大值,因為隊列在向前移動,每次會刪除隊首元素,加入新的元素。
考慮下面的高度序列,每一個高度代表數字大小
如果窗口為3,第1個窗口A[1]是最大值
第2個窗口,A[1]仍然是最大值
第3個窗口,A[1]被移除。剩下的點中A[2]不可能成為最大值,因為在A[2]的右側有大于A[2]的值存在,而[2,4]的窗口中最大值是 A[2]或者[2+1,4]的最大值。因此在比較過程中A[2]可以直接忽略掉。
更一般的說,在窗口[i,j]中,對于任意點k, 如果k的右側存在比k更大的數,則k可以從比較隊列中移除掉。
因此,算法的思想就是,維護一個待比較隊列,每次窗口移動,
對于新加入的元素,它將會淘汰掉所有比較隊列中小于等于它的元素。
因此,這個比較隊列從最先加入的元素到后加入的元素,實際上形成了一個嚴格單調遞減的序列。
由于窗口移動需要移除區間最左側的元素,因此需要使用一個下標記錄最大值是否是隊首的值,如果是,則需要將這個值也移除掉。
注意,也可以不必使用下標記錄,我們將比較隊列放寬為非嚴格單調遞減,則只需要在移除隊列是判斷隊首的值是否與待移除的值相等即可。如果隊列中相同元素較多,則這里會浪費較多時間比較。如果相同元素較少,則該方法更加節省空間。
單調棧和單調隊列:無論使用棧還是使用隊列,都可以完成上面的問題。當使用棧時,只需要棧能夠移除棧頂元素即可。相對而言,使用隊列更加容易理解刪除操作;使用棧更加容易理解比較過程。
問題2:選擇數組中的一個區間,使得區間內最小值與區間的和的乘機最大
解:該問題需要搜索數組,搜索的過程中,以某個元素為最小值,則這個最小值的所在的區間的兩端肯定是比這個元素更小的值,因此我們可以使用單調隊列來查找該元素的區間。
我們維護一個遞增的單調序列,當遇到小于隊尾(最新的元素)時,則該元素的區間就確定了(它的前一個元素與這個元素之間的區間),因此將這個元素從隊尾移除。如此循環,可以遍歷完所有的組合。復雜度:O(n)。
注意:實際上由于不需要對隊首進行操作,因此使用單調棧更加簡單。
問題3: 柱狀圖的最大矩形
問題4:柱狀圖的最大容納面積
單調棧
單調棧解決問題使用的數據結構:一個隊列,問題的狀態包括已經出隊的元素
單調棧的循環不變式:所有不在隊列的元素已經解決
3.約瑟夫問題
題目:把0到n-1這n個數字排成一個圓圈,從數字0開始每次依次刪除第m個數字,最后剩余的是哪一個數字?
遞推公式:f(n,m) = [f(n-1,m)+m]%n (n>1), 如果n==1,則f(n,m)=0.
7.股票的最大利潤
題目:把某支股票的價格按照時間順序存儲在數組中,買入和賣出該股票的最大價值是多少?比如 {9,11,8,5,7,12,16,14} 買入5,賣出16,利潤11最大。
解:只需要從數組的最左側選擇一個最小的數,然后從數組的最右側選擇一個最大的數,兩者相減即是最大利潤。
如何劃分這些解呢?每一組解必然包含一個中間界限i,[0,i]的最小值和(i,n-1]的最大值,就是以i為界限的一個解。中間界限有 n-2個,所有只需要選擇n-2個解里面得最大值即可。
簡化:選擇界限其實是存在重復的,以i為界限,如果A[i+1]比A[i]大,但是小于右側最大值,則存在重復。
考慮以i為賣出點的最大值,顯然只需要知道[0,i)的最小值即可求出。共有n-1個可行解,選擇最大值即可。
9.遞增數組中,求和為s的一對數
考慮f(i,j)表示 [i,j]內的解,考慮以A[i],A[j]作為其中一個解,則A[i] + A[j]是最大的那個;如果A[i]+A[j]=s,則成為解;如果A[i]+A[j] < s,則A[i]不可能是解,轉化到f(i+1,j);如果A[i] +A[j] > s, 則A[j]不可能是解,轉化到f(i,j-1)
即:
f(i,j) = [i,j] 如果A[i]+A[j] == s
f(i,j) = f(i+1,j) 如果A[i]+A[j] < s
f(i,j) = f(i,j-1) 如果A[i]+ A[j] > s
10.數組區間的最小值與和的乘積最大值
關鍵點:考慮數組的分區,以小于某個數可分成多個分區。
題目:給定一個數組,求一個區間[i,j]使得sum(i,j) * min(i,j) 具有最大值。
注:數組的每個元素保證在[0,100]范圍內
解1:注意此題的一個顯著的條件,元素值范圍是[0,100]
我們考慮從0到100遍歷即可。從解的角度考慮,我們需要一個最小值,然后在保證最小值的情況下求最大的和,然后求乘積。
從0到100依次假設最小值j,然后遍歷數組A,每當遇到比j小的值時,結束一個區間的計算;直到遍歷完所有的最小值,最終得到最大計算結果。
以j為最小值的視角: [A0,A1, … , j-1) [Ax, Ay, …,j, j-2)
可以看到這些區間實際上都是以小于j的值作為分界點的。
復雜度:O(100*N)
解2:遍歷數組時,使用一個棧記錄區間狀態,棧頂元素是當前最小值的起始下標。如果當前元素比棧頂元素小,說明 [棧頂元素,當前元素) 構成了一個區間,此時可得一個候選解,然后將棧頂元素彈出,直到當前元素比棧頂元素大。這樣可以保證棧頂元素到當前元素是以當前元素為最小值的最大區間。
關鍵點:使用單調棧形成隔離區間
區間性質:以 A[i]為最小值的最大區間中Zi,如果A[j]屬于Zi且A[i]<A[j], 則A[j]的最大區間Zj是Zi的子區間。 因此,可以使用棧來記錄嵌套區間,當子區間求完之后,父區間可繼續延伸。
例如:5 9 6 8 7
棧的變化:
0: 5
1: 5 9
2: 5 6 彈出區間 [1,2)
3: 5 6 8
4: 5 6 7 彈出區間 [3,4)
5: 最后 彈出所有剩余區間 [4,5), [2,5), [0,5)
其結果就是:以A[i]為最小值的所有最大區間都能確定下來,從而能夠確定 sum(i,j) * min(i,j)
復雜度:O(N)
11.移除數組連續元素獲得的最大值
鏈接:https://leetcode.com/problems/remove-boxes/
不斷從一個正數數組中移除k個相同的數,每一次移除獲得k*k個價值,求最后將數組移除完畢后能夠獲得的最大價值。
示例:
[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (33=9 points)
----> [1, 3, 3, 3, 1] (11=1 points)
----> [1, 1] (33=9 points)
----> [] (22=4 points)
最終獲得23
解:考慮問題的空間狀態,如果使用一個二進制數反映數組的元素是否存在,則問題狀態圖中,共有2^n個節點,節點之間的路徑就是移除數目的平方,問題轉化為求解從起始狀態到終止狀態的最大路徑。
考慮對同一個元素而言,如果從一個數組中1個,和移除多個,顯然一次移除多個的價值是最大的(因為 (a+b)^2 > a^2 + b^2), 所以每次可從數組中移除一組相鄰的元素。
考慮移除第i組然后移除第i+1組元素,和先移除第i+1組,然后移除第i組,最終問題都轉化成前i-1組的子問題,因此存在子問題重疊。
考慮f(i,j)表示移除[i,j]之間元素的最大值,則
若設r[j]為A[j]在[i,j-1]之間出現的所有位置。則A[j]可選擇與r[j]中的任意元素進行進行合并。
由于合并后,末尾元素的數量會累積,所以使用g(i,j,s)表示求解[i,j]且A[j]數量為s的解;顯然有
f(i,j) = max{ g(i,r,C[j] + C[r]) + f(r+1,j-1) }, r為[i,j-1]中所有與A[j]相同的下標
g(i,j,s) = max{ g(i,r,C[r]+C[r]) + f(r+1,j-1) }, r為[i,j-1]中所有與A[j]相同的下標
解2:上面的解法實際上是存在冗余的,因為每次都需要試圖匹配一個點,導致算法需要查找所有的點,如果我們將子問題改成,與左側區間的一個合適的值匹配,而不必循環即可解決;則子問題1.不與左側區間任何一個值匹配 2.與左側第一個值匹配 3.與左側區間的第一個值的左側區間匹配
我們將普通問題表述為f(i,j), 即[i,j]的最大值,則問題2可使用h表述:
由于問題2產生匹配時,會累積邊界的值,因此h需要3個參數,即h(i,j,m)
則 h(i,j,m) = h(i,r,m+1) + f(r+1,j-1)
表示子問題[i,r]和[r+1,j-1]
其中r為j的左鄰接點
問題3假定使用g來表示,r表示j的左側第一個鄰接點;我們發現問題3實際上具有遞歸結構,因為左側區間的左側區間仍然構成一個子問題,則
g(i,j,r,m) = max { f(i,r,m+1,m) + f(r+1,j-1,m), g(i,j,r’,m) }
表示子問題由于左鄰接點匹配的結果與左鄰接點的左側匹配的結果的最大值
g的示例圖:
[i, ... ,r', r, ... ,j] j與r匹配 或者j與r左側的點匹配,即j與r'或者r'的左側的點匹配,具有問題g的結構 # prev[r]表示r的前驅下標 # 最終結果是 f(0,n-1,1) f(i,j,m):if i>j:return 0if i==j:return m*mreturn max(f(i,j-1,1) + m*m, h(i,j,m), g(i,j,prev[j],m)) h(i,j,m):if i>j:return 0if i==j:return m*mreturn prev[j]<i ? 0 : f(i,prev[j],m+1) + f(prev[j]+1,j-1,1) g(i,j,r,m):if i>j || r<i:return 0return max(f(i, r, m + 1) + f(r + 1, j - 1, 1), g(i, j, prev[r], m))注:上面的代碼不經過優化,即使對f,h做緩存,運行時間仍然是204ms,離最優時間3ms相差甚遠。
關鍵點:問題分類討論,使用不同的符號表示不同的子問題。如果可能,合并不同符號減小子問題數量。
優化1:嘗試合并子問題,第一步,去掉函數h,因為h只是單純調用f。
優化后時間:160ms, 減少了40ms
優化2:使用g代表通用的轉化形式
實際上,我們發現g才是函數的通用形態,f可以化成g的形式。令g能夠解決點j不與[i,j]中的任何一個點合并的問題,然后將問題降低到二維并進行可能的剪枝。
當f全部化成g的形式調用之后,f不再需要m參數,因此緩存的空間降低一階。
上面的解最終的運行時間是2ms, 超過了100% 的解法。而其中最重要的一行代碼就是
if r+1 == j:return g(i,r,prev[r],m+1,sum)這一步消除了許多無用的中間狀態,達到剪枝的目的。
最終提交的java代碼:
(此問題我抓狂了一天一夜 --2020年02月13日)
我之前的解析(2017年寫的,稍微有點復雜):https://leetcode.com/problems/remove-boxes/discuss/101320/a-dp-solution-an-extra-thought-of-using-dancing-links/408318
12.判斷數組中的元素是否只出現了一次
排序,然后統計。
如果要求盡量少的空間復雜度,我們我們將遞歸也考慮進去,則要排除快速排序。
所以,最終只有堆排序能夠在O(1)的空間要求下完成O(NlogN)的時間復雜度。
13.選擇小于k的數中最大的價值
題目:給定一個二維數組A, A[i][0] 表示選擇i所需的最小值,A[i][1]表示選擇i獲得價值,給定值k,求能夠獲得最大價值。
鏈接:https://www.nowcoder.com/practice/46e837a4ea9144f5ad2021658cb54c4d?tpId=98&tqId=32824&tPage=1&rp=1&ru=/ta/2019test&qru=/ta/2019test/question-ranking
此題粗想不難,但是有許多細節值得注意。
首先,我們對數組按照A[i][0]進行升序排序,然后找到k在數組中的最接近的位置r,返回排序后小于等于r的最大價值。
為此,我們需要實現二分查找來獲得k的下標,然后使用一個最大值數組記錄區間最大值。
14.序列轉換的次數
題目:已知一個奇怪的隊列,這個隊列中有n個數,初始狀態時,順序是1,2,3,4,…n,是1-n按順序排列。這個隊列只支持一種操作,就是把隊列中的第i號元素提前到隊首(1<i<=n),如有4個元素,初始為1,2,3,4,可以將3提前到隊首,得到3,1,2,4 。 現在給出一個經過若干次操作之后的序列,請你找出這個序列至少是由原序列操作了多少次得到的。
鏈接:https://www.nowcoder.com/practice/02f2bbaadbcf424b8cbc7e264ddef9b4?tpId=98&tqId=33056&rp=1&ru=%2Fta%2F2019test&qru=%2Fta%2F2019test%2Fquestion-ranking&tPage=12
考慮序列[1,2,3,4,5], 它的下一個狀態如下:
[2,1,3,4,5], [3 1 2 4 5], [4 1 2 3 5],[5 1 2 3 4]
不考慮狀態的回退,這就意味著,后續的元素只能選擇從右側的單調遞增區間中選擇一個元素來變換,從而,我們可知,數組末端最長遞增的序列就是所有無需調整的元素,而左側的元素則是需要調整的。
因此,解就是統計數組末端的最長遞增序列長度s,則需要調整n-s次。
15. k-sum
題目:
圖論
1.BFS和DFS
BFS的性質:在一個所有距離都為1的圖中,能夠找到任意兩個點之間的最短路徑。因為BFS的性質就是擴展。這個性質在格子地圖的搜索中使用比較多,比如推箱子,迷宮問題。
BFS
廣度優先搜索需要選擇一個起點,其結果是該點能夠到大的所有最終節點v,且形成的路徑具有最少的邊。
廣度的含義:該算法僅在遍歷完其距離為k的節點時,才會遍歷距離為k+1的節點。
初始時,所有的節點標記為未訪問;起點標記為已訪問。維護一個隊列表示將要訪問的隊列
偽代碼
注意visited設置的時機,應當是在加入到隊列之后就設置,不然可能存在重復入隊的問題。
DFS: DFS對圖進行深度優先遍歷
DFS(G):for u in G:visited[u]=falsed[u]=0t=0for u in G:if not visited[u]:DFS-VISIT(G,u) DFS-VISIT(G,u):d[u]=t++visited[u]=truefor v in G.adj[u]:if not visited[v]:DFS-VISIT(G,v)遍歷完成后,d將保存每一個節點遍歷的先后順序。
2.拓撲排序和最短路徑
拓撲排序:是有向無環圖的一個線性次序,所有的有向邊都指向右側。
排序算法:
topo-sort(G):dfs(G) with when each u is finished, insert u to front of listreturn list如果存在環,則拓撲排序結果無效。
有向無環圖的單源最短路徑:對圖的拓撲順序進行遍歷,我們可以知道排在前面的點不存在其他可能的路徑,因此對其進行松弛后就是最短路徑。
因為G.adj[u]只能是在拓撲順序的后面位置
正確性證明:對于圖中的所有節點v,s到v的最短路徑為 (v0,v1,…,vi) ,v0=s, vi=v; 則路徑仍然是拓撲排序的,所以其松弛的次序為 (v0,v1), (v1,v2), … (vi-1,vi). 由于 (v0,v1)松弛后, v1不存在其他的可選路徑,所以(v0,v1)是一條最短路徑,同理 (v0,v1,v2)也是一條最短路徑, … (v0,v1,…,vi)是一條最短路徑
很顯然,拓撲排序中在s前面的節點距離都是0,只有在s后面的節點才具有最短路徑。
最短路徑中的松弛性質:如果p=(v0,…vi)是一條最短路徑,則只要保證松弛順序是 (v0,v1), (v1,v2) … (vi-1,vi),則d(v0,vk)是v0到vk的最短路徑。
3.Bellman-Ford算法求最短路徑
解決一般情況下的單源單源路徑問題,權重可以為負值。
Bellman-Ford算法的核心是最短路徑松弛性質,設s到v的所有可能的無環路徑為p0,p1,…pi, 則pi的最大長度是|G.V|. Bellman-Ford算法首先找到長度小于等于0的最短路徑,也就是s本身;然后找到長度小于等于1的最短路徑,依次找到長度小于等于|G.V|的最短路徑。最終,得到路徑距離。
但是注意,如果路徑中存在負環,則最短路徑是不存在的,這種情況下,只需要最后測試是否還能夠對路徑進行松弛,如果不能則找到最短路徑;如果可以則存在負環
算法分析:復雜度 O(VE)
由于算法無差別地循環V-1次,每次需要遍歷每條邊,所以相對于Dijkstra算法而言,其效率更差。
SPFA 優化的Bellman-Ford算法
SPFA wiki
該算法使用一個隊列保存所有產生了更新的節點,算法的每一步是從隊列中取出一個點,然后更新其鄰接點。
該算法的最壞情況和Bellman-Ford算法一樣,O(VE).
如果存在負環,則算法永遠不會終止。因此,需要保證圖G沒有負環。
實際上,一個點加入到隊列中最多等于它的鄰接點個數,如果超過這個次數,說明該點存在負環。
4.Dijkstra最短路徑
關鍵點:每次使用優先隊列選擇最短的路徑進行松弛,松弛過程中還會調整優先隊列
求解有向圖非負權重的單源最短路徑問題。
注:圖的表示,V表示所有的點, G.adj[u]表示u的所有鄰接點
偽代碼:
算法中優先隊列有兩個操作: 1.extract-min操作 2.decrease-key操作(即將路徑松弛后,調整元素在隊列中的位置)
復雜度取決于優先隊列的實現:
1.如果優先隊列使用平常的數組,則每次查找最大值都需要O(V)時間,總的復雜度是O(V^2).
2.如果是稀疏圖,可以使用二叉堆來實現優先隊列,extract-min=O(logV),decrease-key=O(logV), 總的運行時間 O((V+E)logV)
3.使用斐波那契堆,extract-min和decrease-key的攤還操作是O(logV),總復雜度是O(VlogV + E). 實際上,斐波那契堆的改進就來源于迪杰斯特拉算法中decrease-key操作遠比extract-min操作頻繁而來。
使用數組編寫
public class Dijkstra{public int dijkstra(int[][] G,int s){int n = G.length;int[] d = new int[n];// 使用done[]標記已經完成的點boolean[] done = new boolean[n];for(int i=0;i<n;++i){d[i]=-1;}d[s] = 0;done[s]=true;for(int i=0;i<n;++i){// extract-min操作int min = -1;for(int j=0;j<n;++j){if(!done[j] &&d[j]!=-1&& (min==-1 || d[j]<d[min])){min = j;}}done[min]=true;for(int k=0;k<n;++k){if(G[min][k]>0){// relaxif(d[k]==-1 || d[k] > d[min] + G[min][k]){d[k] = S[min] + G[min][k];}}}}} }關鍵點:使用done數組標記已經完成的點避免重復查找;extract-min操作需要排除已經完成的點
2.TSP旅行商問題(TSP)
題目:有n個城市,任意兩個城市之間距離為d[i,j]。如果希望從城市0出發走遍每一個城市且每個城市僅路過一次,最后回到城市0,求最小路徑。
解:該問題實際上可以抽象為下面的子問題:設當前處于i城市,未訪問的城市集合是V,求從i出發,經過V的所有節點,返回城市0的最小路徑。
記為f(i,V), 則
f(i,V) = min{ div + f(v,V-{v}) }, 當V為空時, f(i,V) = di0
限定V的數量為20個,則可以使用一個int類型和二進制來記錄。
使用數組記錄,內存在M級別。
強連通分量(component)
定義:在有向圖中,強連通分量是指圖的一個子圖,該子圖中,任意兩個節點都能夠相互到達。
可以將圖拆分成若干個強連通分量,將這些連通分量視為一個點,圖就收縮為一個有向無環圖。
算法:對于圖G, G’表示G的轉置,也就是將G中的每一邊都反轉。
STRONGEST-CONNECTED-COMPONENT(G):dfs(G), get u.f for each vertex u in Gdfs(G') in decreasing order of u.f:then each tree in dfs(G') is a strongest-connected-component第一步DFS先找到所有點的拓撲排序,然后第二步DFS根據拓撲排序的倒序結果,尋找所有點的起點,最終獲得連通分量。
3.最小生成樹(MST)
定義:在一個無環圖中,尋找一條線將圖中的所有點都連接起來,并且權重和最小。
因為這些點連起來以后必然形成一棵樹(沒有回邊),從而將這個這棵樹稱為最小生成樹。
通用MST算法:
GENERIC-MST(G,w):A = {}while A is not a spanning tree:find and edge (u,v) that is safe for AA.add({u,v})return A循環不變式:每次循環開始時,A是某棵最小生成樹的子集
因為每次循環只加入了一條安全邊,所以加入的安全邊仍然構成了一個最小生成樹。算法的關鍵是尋找一條安全邊。
Kruskal算法和Prim算法符合該基本算法。
Kruskal算法:維護一個生成樹的集合,每次選擇連接兩棵生成樹的最小邊,然和合并這兩棵樹。
使用并查集:
Prim算法:從某個節點開始生成一顆最小生成樹A,維護一個尚未加入A中的集合Q,Q的所有點記錄了它們與A連接的最小值。每次循環時,從Q中取出連接權重最小的那個,然后更新其他Q中相鄰的點的權重。
該算法和dijkstra算法類似,每次從未解決的點中選擇一條最小的邊加入最小路徑,然后更新相鄰的點。
MST-PRIM(G,w,r):for v in G.V:u.key = INFu.parent = NILr.key = 0Q=G.Vwhile Q is not empty:u = EXTRACT-MIN(Q)for v in G.adj[u]:if v in Q and w(u,v) < v.key:v.parent = uv.key = w(u,v)不能存在負向環
算法
1.一系列數據中有一個數出現了k次,求這個數
2.并查集
并查集可以快速判斷兩個
關鍵點:1.支持union和find兩個操作 2.每個點使用屬性p表示歸屬的集合,根節點的p屬性等于其自身
偽代碼
帶有秩優化的find操作:
find(x):if(x.p==x)return x;return x.p=find(x);矩陣中的并查集問題(TODO 提交)
題目:778. Swim in Rising Water
在一個矩陣中G中, G[i][j]表示該點的海拔,在t時刻下雨,G[i][j]的高度是 max{t, G[i][j]}. 現在你處于G[0][0],你可以在0時刻內游到任何高度不超過t的格子中, 經過最少多少時間后可以游到G[n-1][n-1]?
解:判斷在給定t時間內,矩陣是否能夠從G[0][0]到大G[n-1][n-1],由于t的最小值是1,最大值是N^2,所以使用二分查找來確定t的最小值。
Number of Islands
題目:200. Number of Islands(Medium)
給定一個矩陣G[n][m], G[i][j]=1表示島嶼,G[i][j]=0表示水,求島嶼數目
解:此題可使用傳統的并查集來解決,如下:
class Solution {int[] prev;int c;int find(int x){if(x==prev[x])return x;return prev[x]=find(prev[x]);}void union(int x,int y){int px=find(x);int py=find(y);if(px!=py){--c;prev[px]=py;}}public int numIslands(char[][] grid) {int n=grid.length;if(n==0){return 0;}int m =grid[0].length;prev = new int[m*n];c=0;for(int i=0;i<n;++i){for(int j=0;j<m;++j){prev[i*m+j] = i*m+j; if(grid[i][j]=='1'){++c;}}}for(int i=0;i<n;++i){for(int j=0;j<m;++j){if(grid[i][j]=='1'){if(i-1>=0 && grid[i-1][j]=='1'){union(i*m+j,(i-1)*m+j);}if(j-1>=0 && grid[i][j-1]=='1'){union(i*m+j,i*m+j-1);}}}}return c;} }但是更快的方法是直接在數組中修改集合關系,每當我們遇到1時,我們就清除周圍的1,直到結束。因為1是連續的,我們往四個方向清除之后,保證這個集合的1不會再被訪問,因此每次遇到1都是集合數量。這種方法的復雜度是O(NM).
public class Solution {public int numIslands(char[][] grid) {int count = 0;for (int i = 0; i < grid.length; i++) for (int j = 0; j < grid[i].length; j++)if (grid[i][j] == '1') {count++;clearRestOfLand(grid, i, j);}return count;}private void clearRestOfLand(char[][] grid, int i, int j) {if (i < 0 || j < 0 || i >= grid.length || j >= grid[i].length || grid[i][j] == '0') return;grid[i][j] = '0';clearRestOfLand(grid, i+1, j);clearRestOfLand(grid, i-1, j);clearRestOfLand(grid, i, j+1);clearRestOfLand(grid, i, j-1);} }遞歸解法還可用于此題: 695. Max Area of Island(Medium)
class Solution {int[][] grid;public int maxAreaOfIsland(int[][] grid) {this.grid = grid;int max = 0;for(int i=0;i<grid.length;++i){for(int j=0;j<grid[i].length;++j){if(grid[i][j]==1){max = Math.max(findClearAll(i,j),max);}}}return max;}int findClearAll(int i,int j){if(i<0||i>=grid.length||j<0||j>=grid[i].length||grid[i][j]==0)return 0;grid[i][j]=0;return 1+findClearAll(i-1,j) + findClearAll(i+1,j) + findClearAll(i,j-1) + findClearAll(i,j+1);} }Friend Cycle
題目: 547. Friend Circles(Medium)
給定一個關系矩陣G[n][n], G[i][j]=1表示i和j存在朋友關系, 如果i,j存在朋友關系, j,k存在朋友關系,則i,k存在間接朋友關系。請找出所有的朋友圈,每個朋友圈中的兩個人要么是朋友關系,要么是間接朋友關系
解:這道題說白了還是考察并查集的內容,因為i要在一個朋友圈中,i必然與朋友圈中的一個人是直接朋友關系,所以只需要把所有具有朋友關系的人并在一起即可。
實際上,間接朋友關系對本題沒有任何影響。
class Solution {int[] prev;int c;int find(int x){if(prev[x]==x)return x;return prev[x]=find(prev[x]);}void union(int x,int y){int px=find(x);int py=find(y);if(px!=py){--c;prev[px]=py;}}public int findCircleNum(int[][] M) {// in a cycle, person A must have a direct friend.// c=N// for each person A// makeSet(A)// for each person A// for each friend B:// if A,B not the same// union(A,B)// --c// return cint n=M.length;prev = new int[n];c=n;for(int i=0;i<n;++i){prev[i] = i;}for(int i=0;i<n;++i){for(int j=i+1;j<n;++j){if(M[i][j]==1){union(i,j);}}}return c;} }3.整數的平方
4.背包問題
題目1: 給定體積下的放置方式 鏈接
給定一個背包容量為w, 共有n個物品,每個物品的體積是v[i],求總體積不超過w的情況下,有多少種放置物品的方法?(體積為0也是一種方法)
解:f(A,w) = sum { f(A - {i}, w - v[i]) + f(A -{i},w) }
由于子問題無論如何都會去掉i,因此我們可以將問題簡化為求 [i,n-1]的區間的背包問題
f(i,w) = f(i+1, w-v[i]) + f(i+1,w)
緩存使用map記錄w對應的值。
回溯法
0.一般應用背景
棋盤,或者數組規模小于等于9時
1.八皇后問題:棋盤上八個皇后的布局
對每一個格子(x,y), 如果該點被占用,則上它標記了縱線,正斜線和反斜線三條線上的格子不能用
因此進入下一層時所有的點只需要判斷它的縱線,正斜線和反斜線是否未被占用即可。
計算(x,y)的正斜線和反斜線的位置
| |\ | \ |_ _ _ _ _ _ _ _ _左斜線直線組: y = -x + T, T從0到2*(n-1)
右斜線直線組:y = x + T, T從-(n-1)到(n-1)
所以,左斜線的序號: y+x, 右斜線的序號:y-x + (n-1)
注意事項:1.代碼中以int search(i)為原型,表示當前狀態下搜索第i層產生的解,注意i==n時返回1
代碼:
vertical = new boolean[n]; left = new boolean[2*n - 1]; right = new boolean[2*n - 1];public static int search(int i){if(i==n){return 1;}int count = 0;for (int j = 0; j < n; j++) {if(vertical[j] || left[i+j] || right[i - j + n - 1]){continue;}vertical[j] = left[i+j] = right[i-j +n-1] = true;count += search(i+1);vertical[j] = left[i+j] = right[i-j +n-1] = false;}return count; }2.迷宮問題
題目:給定一個二維數組,其中0表示通道,1表示圍墻,指定起點和終點,尋找一條通道。
解:路徑是不存在重復的,因此如果一個點已經走過,它不可能重新成為路徑的一部分。所以搜索時需要將路徑上的點標記為已訪問。
當前點如果不是終點,則將鄰接的可用的點加入到棧中,依次進行搜索。
剪枝:如果一個點已經走過,則它的兩個相鄰點只能有一個成為解的一部分。因為如果兩個點都在路徑上,則選擇路徑靠后的一個,前面的點就被消除了。所以在回溯時,無需考慮將鄰接點的狀態從已用轉化為可用。因此下面的代碼中,首先將所有的可用鄰接點標記收集起來,然后標記為已用,再進行回溯。這樣能夠避免路徑上出現同一個點的兩個鄰接點。非常重要的一點是:在鄰接點遍歷完畢后,應當將所有的點重新標記為可用,因為它們只是在鄰接點期間不可用。
偽代碼:
# t 終點 solution = {s} search(s):if s==t:print(solution)returnavlAdj = []for q in s.adj:if q.used==0:continueq.used = 1avlAdj.add(q)for q in avlAdj:solution.add(q)search(q)solution.removeLast()# 為了找出所有的解,這里需要進行恢復for q in avlAdj:q.used = 0java代碼,打印所有的路徑
import java.util.ArrayList; import java.util.List; import java.util.Scanner;public class Solution {private static int n;private static int m;private static int[][] arr;private static int[] start;private static int[] end;public static void main(String[] args) {Scanner scanner = new Scanner(System.in);n = scanner.nextInt();m = scanner.nextInt();start = new int[] { scanner.nextInt(),scanner.nextInt()};end = new int[] { scanner.nextInt(),scanner.nextInt()};arr = new int[n][m];for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {arr[i][j] = scanner.nextInt();}}/*5 50 44 40 0 1 0 00 0 0 0 00 0 0 1 01 1 0 1 10 0 0 0 0*/// [down, left, left, down, down, down, right, right]// [left, down, left, down, down, down, right, right]// trueSolution solution = new Solution();System.out.println(solution.hasPath(arr,start,end));}class Pack{int[] data;String dir;public Pack(int x,int y,String dir) {this.data = new int[]{x,y};this.dir = dir;}}List<String> solution = new ArrayList<>();public boolean hasPath(int[][] maze, int[] start, int[] destination) {if(start[0]==destination[0] && start[1]==destination[1]){System.out.println(solution);return true;}List<Pack> points = new ArrayList<>(4);if (start[0]>0 && maze[start[0]-1][start[1]]==0){points.add(new Pack(start[0]-1, start[1],"up"));}if(start[0]<maze.length-1 && maze[start[0]+1][start[1]]==0){points.add(new Pack(start[0]+1, start[1],"down"));}if(start[1]>0 && maze[start[0]][start[1]-1]==0){points.add(new Pack(start[0], start[1]-1,"left"));}if(start[1]<maze[0].length-1 && maze[start[0]][start[1]+1]==0){points.add(new Pack(start[0], start[1]+1,"right"));}for (Pack point : points) {//visitedmaze[point.data[0]][point.data[1]]=1;}boolean once = false;for (Pack point : points) {solution.add(point.dir);once |= hasPath(maze,point.data,destination);solution.remove(solution.size()-1);}for (Pack point : points) {maze[point.data[0]][point.data[1]]=0;}return once;} }題目鏈接:
https://leetcode.com/articles/the-maze/
https://leetcode-cn.com/problems/word-search/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china
3.推箱子
鏈接:https://leetcode.com/problems/minimum-moves-to-move-a-box-to-their-target-location/
題目:在一個NxN的地圖上,有一個箱子,一個目的地,一個人,以及一些障礙物,計算出最少需要多少步將,人能夠將箱子推到目的地。
解1:該問題實際上比迷宮的問題復雜。將人和箱子的坐標視為狀態 (xp,yp), (xb,yb),則最終狀態就是xb=終點坐標,yb=終點坐標。從圖論的角度來看,節點與節點之間的距離是0或者1,而且有多個終點。可以使用dijkstra算法求解最短路徑,然后選擇其中箱子位置與目標位置相同的節點的最小值即可。實際上如果是求最短路徑,則第一個出現的狀態就是目標狀態。
基于Dijkstra算法,如果使用數組則需要遍歷很多無用狀態,因此需要使用隊列。隊列中存儲狀態,不存儲解,解使用其他的map存儲。
偽代碼:
注意:必須使用visited限定move的節點。
分析:dijkstra算法使用數組實現會比使用普通隊列慢,但是復雜度增長是一樣的; 因此在推箱子的問題中,雖然dijkstra算法易于實現,但是效果不佳;除非能夠使用斐波那契數堆。
解2:優化的Dijkstra算法
因為我們知道所有可能的路徑介于0到n*m之間(路徑最多包含所有的格子),因此在記錄最小路徑時,可以使用一個Tree來存儲路徑i的所有狀態。則每次從Tree中取出最小的路徑i以及對應的點集Q,則點集Q的所有點的路徑都已經確定了,將它們加入到visited中
對點集Q的每一個相鄰點P,如果P不在visited中,則進行下面的步驟:
因為相鄰點的距離只有0和1,如果是0,則將P加入visited中;如果是1,將P放到Tree的路徑為i+1的集合中
所以,實際上整個問題變成:尋找最短路徑=i (0 <= i <= n*m)對應的所有點集。
由于最短路徑的下一個狀態要么是0,要么是1,所以只需要維護一個集合即可,不需要visited等狀態。
循環不變式:queue中保存所有已解決的路徑為i的點
所有可能移動的點可能是:前一層已訪問的點(忽略),同一層已訪問的點(忽略),同一層未訪問的點,加入到當前層中;下一層未訪問的點,加入到下一層。
注意,當我們將一個點加入到下一層時,實際上就已經決定了它的距離至少是i+1。那么,是否存在一個點違反這個性質呢?由于隊列中所有的狀態都是唯一的,因此所有狀態的能夠推動箱子的狀態是唯一的。
解3:對狀態圖進行BFS,并使用隊列存儲產生更新的點
循環不變式:每次循環時,隊列中包含所有已產生距離更新的節點
為了避免重復更新,同一個節點不能被加入隊列中兩次,所以需要一個狀態記錄是否在隊列中(是否產生了更新)
當到達t節點后,無需將t節點放入隊列中,因為如果其他節點能夠更新t的距離,則刪除重復的路徑,總能獲得更短的距離。
循環終止條件:隊列中沒有新產生更新的節點時,則已求得最小距離
注意,該算法同樣適用于求最短路徑。
上面算法的運行時間實際上會比較長,因為一個點會被反復加入到隊列中更新直到它的路徑不能夠再松弛位置。
因此我們也可以將算法描述為:找到每個可到達的點的所有松弛路徑中的最小值。
首先是確保每個點都會被訪問到,通過move函數保證
然后確保所有松弛路徑,也是通過move函數的四個相鄰狀態保證的。
解4:減小BFS的狀態數,分別對人和箱子進行BFS
人的位置需要BFS到箱子的4個相鄰位置,只需要判斷是否能夠進行即可,每個箱子的位置有4個狀態,表示人所處的位置。箱子則需要在4個狀態下,搜索到目標的4個狀態之一。
復雜度分析:O(V^2)
4.魔方
題目:給一個2x2的迷你魔方每個面編號,賦予一個數字;最多轉動這個魔方n次,求魔方的每一面4個數字的積的總和的最大值。
解:使用回溯法,主要是狀態轉化有點麻煩,純手寫就行。
5.俄羅斯方塊
6.拼圖游戲
7.五子棋
題目:判斷五子棋哪一方贏棋
解:五子棋需要8個方向是否滿足贏棋規則,因此需要統計一個棋子的8個方向的累積和。8個方向的累積和各有具有迭代性質,因此,我們遍歷某個方向時,需要保證下一個點的累積數據一定能夠根據之前的累積數據直接計算出來。
我們使用的遍歷方法: 左上角 --> 右下角:解決左,左上,上的統計, 右上角-->左下角:解決右,右上的統計, 以及左下角->右上角和右下角-->左上角的統計。
此外,我們可以初始化一個nn8的數組,一次遍歷解決,只需要在一次遍歷中執行8個更新操作即可。
或者我們執行n*n次循環,單獨維護四個方向的遍歷即可。
其他還需要注意,黑棋和白棋至多相差一個子,且累積的數目至多是5。
5.數獨
題目:數獨是一個9x9的宮格里,每一行,每一列,每個3x3的格子(一共9個)中1-9只能出現一次
鏈接:https://leetcode-cn.com/problems/sudoku-solver/
解法1:使用回溯法依次判斷每一個空格。
優化方案:減少無效的重試,當一個宮格使用之后,該行的所有格子,該列的所有格子,該3x3宮格的格子可使用候選就減少了。一共有9行,9列,9個3x3格子,每一個格子都有其所屬的行,列和3x3格子下標。
使用一個3x9的數組,記錄行、列、3x3格子的占用情況。
解法2:使用Dancing Links
Knuth提出了解決精確覆蓋問題的方案,所謂精確覆蓋是指給定多個限制條件和選擇范圍,選擇出所有的解。
首先選擇一列,然后選擇選擇相交的行,移除這個行的所有已選擇的列,同時移除關聯這些列的行。
https://github.com/rafalio/dancing-links-java/blob/master/www-cs-faculty.stanford.edu/~uno/papers/dancing.ps.gz
精確覆蓋問題,考慮下面的矩陣,找出其中能夠覆蓋所有列都有1的所有行。比如1,4,5行。
AlgorithmX:對于一個矩陣A
1.如果A是空的,則已經產生了解
2.如果A非空,則選擇一列c
3.選擇這一列中的一行r, 是的A[r,c]=1
4.對行r的每一列j, 如果A[r,j]=1
從矩陣A中刪除列j
對j的每一行i, 如果A[i,j]=1
從矩陣A中刪除行i
5.在減小的矩陣上繼續應用該算法
矩陣的每一個點使用4個節點連接
使用數組來存儲矩陣,令U[x],D[x],L[x],R[x]分別表示上鄰接點,下鄰接點,左鄰接點,右鄰接點。C[x]表示該點的列頭,S[x]表示該列的大小,N[x]表示該列的名稱。
列頭的L,R鏈接了所有仍需解決的列(約束條件)。
一個特殊的點:h,表示頭部,僅L[h], R[h]有效。
偽代碼
search(k):if h.R == h:print solutionreturnc = choose_column()cover(c)r=c.Dwhile r!=c:O[k] = rj=r.Rwhile j!=r:cover(j)j = j.Rsearch(k+1)r=O[k]c=r.Cj=r.Lwhile j!=r:uncover(j)j = j.Luncover(c)choose_column的實現,可以選擇根節點h的右節點,也可遍歷選擇右節點中最少的一個:
choose_column():j=h.Rs=∞while j!=h:if s>j.S:c=js=j.Sreturn ccover和uncover操作分別從矩陣中移除或恢復列c以及相關的行,以及這些行對應的列
cover(c):c.R.L = c.Lc.L.R = c.Ri=c.Dwhile i!=c:j=i.Rwhile j!=i:j.D.U = j.Uj.U.D = j.Dj.C.S--j = j.Ri=i.D # 注意uncover其實就是cover的逆轉操作 uncover(c):i=c.Uwhile i!=c:j=i.Lwhile j!=i:j.C.S++j.D.U = jj.U.D = jj=j.Li=i.Uc.R.L=cc.L.R=c我們使用一個boolean函數w構造矩陣:
init(n,m,w):h = new Node()leftHead = NILleft = NIL# 按行遍歷,需要記錄每一列的最近upup = new Node[m]for i=0 to n:for j=0 to m:if w(i,j)==0:continuet = new Node()# 更新最近的左節點if leftHead == NIL:left = leftHead = tleft.R = tt.L = leftleft = t# 更新最近的頭部if up[j] == NIL:up[j].C = up[j] = tup[j].D = tt.U = up[j]t.C = up[j].Cup[j]=tif leftHead != NIL:if i==0:h.R = leftHeadh.L = leftleftHead.L = leftleft.R = leftHeadleftHead = left = NIL數獨問題轉化為精確覆蓋問題:列是約束條件,數獨游戲共有4類約束條件
1.每個位置只能放一個數字
2.每一行必須放置滿所有的數字
3.每一列必須放置滿所有的數字
4.每個3*3的區域必須放置滿所有的數字
這些約束使用列約束表示就是:
1.第i個位置放置一個數,共有9*9個位置,所以有9*9列
2.第i行出現j 9行,9個數字,共9*9列
3.第i列出現j,9列,9個數字,共9*9列
4.第i個區域出現j,9個區域,9個數字,共9*9列
所以總共有9*9*4=324列
行是解的狀態,即第i行j列放置k,共9行,9列,9個數字,所以總共9*9*9行,即729行。
初始化:從給定的數獨狀態中,已經有一些固定的行狀態為true,所以分別將4個約束條件對應的列置1。然后構造矩陣。
尋找解:將矩陣中所有已經確定的解移除,剩余一個尚未解決的矩陣;目標是移除所有的列,最終獲得解,因此,對每一行,嘗試將其加入解;然后遞歸解決這個問題即可。
建議:使用dancing links解決過于復雜,但是該思想值得借鑒。
5.貪吃蛇
題目:拼圖游戲
字符串
1.KMP查找算法
復雜度O(N+P), N是字符串長度,P是模式長度
2.模式匹配
3.簡單正則表達式
12.字符串交換的最大連續字符數
問題:字符串S由小寫字母構成,長度為n。定義一種操作,每次都可以挑選字符串中任意的兩個相鄰字母進行交換。詢問在至多交換m次之后,字符串中最多有多少個連續的位置上的字母相同?
解:我們知道最終的最大連續的字符肯定是26個小寫字母中的一個,我們分別求出26個字母的最大值進行比較即可。
現在來看求字符v的最大值,顯然字符串中v可以分為n個區間,每個區間包含長度信息len以及區間之間的間隔d。
由于需要連續字符,所以顯然將所有字符往某個位置移動是最好的策略,因為如果不這樣,一部分字符往a處移動,另一部分往b出移動,兩個策略產生的值一定小于全部往a處移動或全部往b處移動。因為將一部分a移動到b總能產生比b更大的連續數,將一部分b往a移動也總能產生比a更大的連續數。
可以采用遍歷的方法求得最大值。
5.判斷兩個字符串是否具有相同的組成
由于字符串的元素的個數有限,可以統計每個字符出現的次數,然后比較即可。
6.簡單模式字符串匹配問題
問題:模式是包含.和*的字符串,待匹配的字符串不包含* ,判斷模式與字符串是否匹配
f(i,j)表示模式[i,m-1]與字符串[j,n-1]匹配的問題。
如果模式P[i]不是.*,c*的模式,則問題轉化為 f(i+1,j+1)
如果模式P[i]是.*, 則問題轉化為 f(i+1,j) 和 f(i+1,j+2), 如果模式是c*,則問題轉化為 f(i+1,j+2), 以及當A[i]=c時, f(i+1,j)
二位坐標
1.包圍點
題目:給定二維平面上的點,尋找所有的包圍點。包圍點即該點的右上方沒有其他的點(右上方是指 (x,y)均大于該點的范圍)。
解法:我們考察包圍點的性質,發現最左側的包圍點,一定是y最大的;第二個包圍點,一定是除了第一個包圍點之外最大的。
因此,首先將點集按y降序,x升序排序;使用xmin記錄當前最左側的點,然后依次驗證每一個點:如果x<xmin,則該點不是解;否則,該點是解,加入解集,同時更新xmin=x
2.矩形覆蓋問題
問題1:391. Perfect Rectangle
給定n個矩形,判斷這些矩形是否精確地構成一個完整的矩形,其中不存在重疊和遺漏
解:對矩形按左下標排序,當左下標x相同時,按y排序;如果存在兩個相同的左下標,則不能構成完美矩形。將排序結果加入到優先級隊列Q中,當每次取出Q頭部x坐標相同的,判斷它們的左邊是否覆蓋完整矩形的左邊。然后,將其中寬度最小的矩形用于切割其他矩形,將剩下的矩形重新加入隊列中。
給定n個矩形,判斷重疊最多的點(邊界不算重疊)。
矩形的基本操作
判斷給定的兩個坐標是否是一個有效的矩形
只需要第一個下標嚴格地位于第二個下標的左下角
isValidRectange(x0,y0,x1,y1):return x0<x1 && y0<y1求重疊部分
求出交叉部分,判斷是否有效
findCover(x00,y00,x01,y01,x10,y10,x11,y11):xm0 = max(x00,x10)ym0 = max(y00,y10)xm1 = min(x01,x11)ym1 = min(y01,y11)if isValid(xm0,ym0,xm1,ym1):return (x0=xm0,y0=ym0,x1=xm1,y1=ym1)return NIL切割
首先,我們考慮被矩形內部的一個部分切割的情況
// 注意cover必須完全在矩形內 // cover最多將矩形分成4個部分 // ................. // . . // . ..... . // . . . . // . ..... . // . . // ................. cutByCover(x0,y0,x1,y1,cover):subRectangles = {(x0,y0,cover.x0,y1),(cover.x0,y0,x1,cover.y1),(cover.x0,cover.y1,x1,y1),(cover.x1,y0,x1,cover.y1) }for e in subRectangles:if not isValidRectange(e):remove e from subRectanglesreturn subRectangles然后兩個矩形的切割,就是兩個矩形與cover部分的切割:
cutRectange(x00,y00,x01,y01,x10,y10,x11,y11):cover = findCover(x00,y00,x01,y01,x10,y10,x11,y11)if cover == NIL:return [(x00,y00,x01,y01),(x10,y10,x11,y11)]return cutByCover(x00,y00,x01,y01,cover) + cutByCover(y01,x10,y10,x11,y11,cover)排列組合
1.把數組排成最小的數
問題:一個正整數數組A,將其所有數字拼接成一個數,求拼接的最小值
解:這個拼成的數據最終的位數是固定的,我們考慮最高位,一定是最高位最小的數字填充。但是最高位最小的數字長度不一,會限制第二個高位。我們可以證明一定是選擇順序最低的那位。對于長度相同的數字顯然是,對于長度較短的數字,則優先選擇長度較短的那個。因為次高位可填的數字必然頭部必然還是最低的。
因此最終的方案就是講數組轉換成字符串數組,比較函數是:nm和mn誰更小。
2.第k個排列組合
問題:每個單詞都包含n個’a’和m個’z’, 并且所有單詞按照字典序排列,找出第k個單詞是什么。
鏈接:https://www.nowcoder.com/practice/12b1b8ef17e1441f86f322b250bff4c0?tpId=98&tqId=32838&tPage=1&rp=1&ru=/ta/2019test&qru=/ta/2019test/question-ranking
解:一般性的問題,給定一個字符集合,求這些字符排列的第k字典序的值。
假定我們已經確定了某個排列A,如果兩個字典序的前綴相同,則順序取決于后綴的順序。因此,設A的下一個排列是A’, 設它們的最長公共前綴是T, 后綴是F和F’,則顯然F’是F的下一個排列。
因此,A’等于A的所有后綴中,能夠產生最相鄰排列中的最短的那個。只要一個排列還沒有達到逆序,則它就能產生下一個排列。
next(A) = for s = A的長度為2到n的后綴,如果next(s)存在,則返回 (A-s) + next(s)
由于next(s)同樣會遍歷后綴,因此最終的結果就是尋找最短后綴中,尚未達到逆序的那個排列,因為該后綴最短,所以處理前面的兩個數字是遞增的,其他數字都是遞減的。
也就是具有下面的結構:
交換A[i]與[i+1,n-1]中大于A[i]的第一個元素,從而第i位遞增,但是此時[i+1,n-1]并不處于最小的狀態,因為最小的狀態是單調遞增。所以,將數組后綴元素逆序,就構成了單調遞增狀態。
next(A):n=A.lengthi=n-2while i>=0 && A[i] >= A[i+1]:--iif i<0:return NIL# 交換 A[i] 與 A[i]右側的第一個大于A[i]的元素# 使用二分查找,查找右側第一個比,因為右側是排序的# a b c d# c b a d# d b c a# c和d進行比較,當c!=d時,選擇較小的一個,也就是最后一個大于a的元素# 當c==d時,顯然交換c,也就是相同元素最左側的那個j = binary-search-with-min-bigger(A,A[i],i+1,n-1)exchange A[i],A[j]reverse(A,i+1,n-1)return A # 返回處的元素必須滿足: A[r]>m, 且A[r-1] > A[r] binary-search-with-min-bigger(A,m,i,j):while i<j:# r必須是中間偏右側的位置,因為后面還需要進行逆序操作r=(i+j+1)/2if A[r] <= m:j = r - 1else:i = rreturn i reverse(A,i,j):while i<j:int tmp=A[i]A[i++]=A[j]A[j--]tmp算法總結:每一步操作O(N),如果需要查找第K個,需要O(KN)
解2:由于題目中只有’a’,'z’兩種數字,所以考慮f(i,j)為包含i個’a’和j個’z’的的問題,可以劃分為子問題f(i-1,j)和f(i,j-1)。
dp = new int[n+1][m+1]f(i,j,m):d = g(i-1,j)if d==m:return 'a' + 'z'*j + 'a'*(i-1)elif d>m:return 'a' + f(i-1,j,m)h = g(i,j-1)if h + d == m:return 'z'*j + 'a'*ielif h + d > m:return 'z' + f(i,j-1,d-m)return NILg(i,j):if i<=0 && j<=0:return 0elif i==0||j==0:return 1if dp[i][j]!=-1:return dp[i][j]return dp[i][j] = g(i-1,j) + g(i,j-1)解3:由于問題中只包含兩個字母,因此排列的問題轉化成組合的問題,其本質就是求C(n+m, m)
3.所有排列
如果不要求排列有序,實際上我們可以使用下面的遞歸方程獲得所有排列:
f(S,i) = “a” + f( S - {a}, i+1)
其中a是屬于S中的任意元素,且子遞歸中,a的數量應當減一;i表示排列長度
數字
1.最大的三個數乘積
題目:給定一個無序數組,包含正數、負數和0,要求從中找出3個數的乘積,使得乘積最大,要求時間復雜度:O(n),空間復雜度:O(1)
鏈接:https://www.nowcoder.com/practice/5f29c72b1ae14d92b9c3fa03a037ac5f?tpId=90&tqId=30776&tPage=1&rp=1&ru=/ta/2018test&qru=/ta/2018test/question-ranking
解的分析:
// 沒有正數時,選擇最大的三個數相乘 // 含有1個正數時,選擇正數和最小的兩個數相乘 // 含有2個正數時,如果只有3個元素, 則三個數相乘即可;否則,選擇較大的正數和 非正數中絕對值最大的其他兩個數相乘 // 含有3個以上正數時, 選擇下列元素中最大的值: // 三個最大的正數 // 最大的正數 和 非正數中絕對值最大的兩個數其中,絕對值大的兩個非負數,其實就是最小的兩個數(當數組中非負數含有至少2個時)。
因此,最終的結果只與5個數有關:最大的3個數和最小的2個數。
2.排列的數字是否被3整除
題目:數列按f(i) = f(i-1) + string(i) 的方式展開: 1, 12, 123,…12345678910,1234567891011…,給定i,j,試計算i到j之間能夠被3整除的數的個數。
鏈接:https://www.nowcoder.com/practice/51dcb4eef6004f6f8f44d927463ad5e8?tpId=98&tqId=32825&tPage=1&rp=1&ru=/ta/2019test&qru=/ta/2019test/question-ranking
解:我們知道能夠被3整除的數,其各位數字之和一定能夠被3整除。因此,問題的關鍵就是計算第i到j位,每一個數字的數字和與3的模。
實際上,i的各位之和與3的模,本身就等于i與3的模。這是因為數字每遞增1,它的各位之和與3的模也保持遞增1。
而且,這個模是存在周期的:
1 2 0 1 2 0 … 1 2 0
令k=i%3,k可取0,1,2
所以實際上 f(i) = i%3
也就是說,數列中的第i項與3的模,等于i與3的模k,加上小于k大于0的所有數字和的模。因此,如果i%3==0, 則f(i)=0, 如果i%3==1,則f(i)=1;如果i%3==2,則f(i)=0.
因此,僅當i%3==1時不能被整除,其他情況下都可以。
對于區間i,j,考察i和j所處的位置,j-i+1表示[i,j]的數字總數,令k=j-i+1 / 3, r=j-i+1%3, s=r這一部分的能被3整除的數目
則k表示其中不能被3整除的數目,r則表示余下的數字,如果j%i==1,則s=r-1;如果j%i==0,則s=r;如果j%i==2,則s=r-1
關鍵點:模3的數字和等于該數模3.
3.乘方冪的第K項
題目 鏈接
已知一個正整數n,(3 <= n <= 15),將所有n的乘方冪以及所有n的乘方冪(有限個且互不相等)之和組成一個遞增序列。例如,當n為4時,該序列為:1, 4, 5, 16, 17, 20, 21……
(40, 41, 40+41, 42, 40+42, 41+42, 40+41+42……)
請求出該序列的第K項
解
實際上,我們構建出一個三角形:
0
1 1,0
2 2,0 2,1 2,1,0
3 3,0 3,1 3,1,0 3,2 … 3,2,1,0
我們發現,每一層都是前面所有層的相加。
實際上,這個排列中,第K個數等于K所在的高度的起始值以及差值。
設h(K)為其高度, d(K)為差值,則
f(K) = nh(K) + f(d(K)), 顯然最終f(K) = ni0 + ni1 + … + nik
而ik就是K的二進制表示中所有為1的位,因為h(K)就是K的最高位,h(d(K))就是次高位,依次下去。
從另外一個角度看,我們可以考慮特殊的2進制數: A0*n0+A1*n1 + A2*n2 + … + Ai*ni,其中Ai==0或1,顯然A0 A1 A2 … Ai組合成的二進制數就是該二進制數空間的順序,因此只需要將K表示為二進制數,然后賦予A0…Ai,即可求得f(K).
cur = n^0 c = 0 while K>0:if K & 0b1 == 1:c += curcur *= nK >>>= 1 return c4.丑數
題目:1201 Ugly Number III, 264 Ugly Number II, 313 Super Ugly Number
一個丑數是僅能被a,b或c整除的數,給定a,b,c, 和n,求第n個丑數解:丑數能夠用表示為 ax * by * cz。
考慮我們已知前k個丑數,如何生成第k+1個丑數?
我們使用隊列Q表示所有已生成的k個丑數,則第k+1個丑數必然是a,b或者c的乘積,如果Sk+1 = Sk * a,則Sk = Sk+1/a必然屬于已經生成的丑數,同理對Sk+1/b,Sk+1/c也是如此。
所以,第k+1個丑數必然是隊列Q中的某一個丑數與a,b或c的乘積,且是大于已知最大丑數Sk的最小值。
所以我們只需要將隊列的值從小到大依次與a,b,c相乘并取滿足大于丑數Sk的最小值即可。
for e in Q:s = min(e*a,e*b,e*c)if s>Sk:Q.add(s)break優化:我們注意到求第k和第k+1個丑數時,都需要對a*e進行重復計算并求最小值,而這個最小值實際上就是上一個元素,因此,我們可以記錄上次計算最小值的最后元素,后面循環時繼續使用
i=1,j=1,k=1 s = min(Q[i]*a, Q[j]*b,Q[k]*c) if s == Q[i]*a:++i elif s== Q[j]*a:++j else:++k上面的代碼使用i,j,k來保存a,b,c最后一次相乘能夠產生大于序列Q中所有元素下標。
循環不變式:Q[j],Q[j],Q[k]是與a,b,c相乘產生的結果的不在隊列中的最小值,這是它們能夠產生隊列下一個元素的關鍵條件。
生成序列問題
所謂生成序列問題,是指在給定一個現有隊列Q,一個候選元素集合C,選擇Q與C中的兩個元素生成下一個Q中的元素。每次生成元素時,C與Q會生成一個候選集合S,選擇S中的最優元素加入Q即得下一個元素。
實際上,丑數問題的候選元素集合C={a,b,c}與隊列Q的生成值可以刻畫為3個有序數組:
[0]: a*e1,a*e2,a*e3,... [1]: b*e1,b*e2,b*e3,... [2]: c*e1,c*e2,c*e3,...我們知道S中的最小元素就是[0],[1],[2]中的最小元素,并且要求該元素未被加入到集合中。
所以,實際上整個生成過程就是對S進行3路歸并排序的過程。
上面討論的生成序列問題都是在候選元素C與隊列中Q中的每一個元素結合都是公平的,也就是說S是一個矩陣
下面的問題時,給定一個正整數n, 表示n^0, n^1, n^0 + n^1, ...的序列,我們分析發現S在每次生成時并不都是公平的,因為對每個元素ei,要排除ei中已經出現過的元素,因此不能使用上面的序列生成算法。
Q = e1 e2 e3 C = { n^0 n^1 n^2 ... n^k} S = [0]: e1+n^0 e1+n^1 ... [1]: e2+n^0, e2+n^1 ... ...超級丑數
題目:313 Super Ugly Number
給定一個排序的包含k個素數的數組,尋找第n個僅由該數組元素組成的數
此問題區別于一般的丑數問題,因為一般丑數問題都保證給定丑數小于不會出現 a*b > c的情況;此外,1是第一個丑數。所以,每次遇到下一個元素和最后一個元素相同時,總是跳過這個元素。
也可以在遇到相同元素時,跳過這些元素。
主要,3個素數時,同樣需要手動跳過這些元素。
上面的node節點也可以換成數組,使用鏈表的主要原因是即時回收內存,避免一次性分配過多元素。
5.平方組合
題目 279. Perfect Squares
給定一個數n,n可以被表示為多個數的平方,比如10 = 32 + 12, 求n的平方表示中最少數目
本題是普通的動態規劃: f(n) = min { f(n-X) + 1 } , X是小于n的所有平方數。
為什么不考慮 f(n) = min { f(n-i) + f(i) } 這種普通的分割呢?因為最終所有的分割都會表達成某個數的平方。
設計模式
1.抽象工廠模式
網絡
1.DNS協議
數據庫
中間件
總結
以上是生活随笔為你收集整理的【OfferX】常见题目的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET 三层架构+MVC+EF实现对数
- 下一篇: libvirt零知识学习1 —— lib