1. 程式人生 > 其它 >演算法資料結構中的奇淫技巧

演算法資料結構中的奇淫技巧

技術標籤:演算法演算法

1.尤拉篩法選質數

1.1 演算法原理

一個合數總是可以分解成若干個質數的乘積,那麼如果把質數(最初只知道2是質數)的倍數都去掉,那麼剩下的就是質數了。

1.2 步驟

(1)先把1刪除(1既不是質數也不是合數)

(2)讀取佇列中當前最小的數2,然後把2的倍數刪去

(3)讀取佇列中當前最小的數3,然後把3的倍數刪去

(4)讀取佇列中當前最小的數5,然後把5的倍數刪去


剩下沒刪去的數就是 n 以內的質數

1.3 實現

問題:給一個數n,求出比n小的所有的質數有多少個

思路:用一個bool陣列,儲存n個數的狀態,初始化都為true,然後從2開始,如果2的狀態為true,就開始遍歷比n小的所有的2的倍數,將其全部置為false。把2的倍數遍歷完後,繼續往下找下一個狀態為true的數,即3,遍歷比n小的所有的3的倍數(按3乘以3,3乘以4,3乘以5這樣遍歷,注意不需要從3*2開始了)。…最後剩下的狀態為true的數全為質數。

程式碼

int countPrimes(int n) {
        vector<bool> vec_flag(n,true);
        vec_flag[0] = false;
        vec_flag[1] = false;
        
        for (int i = 2; i < sqrt(n);i++){
            if(vec_flag[i]){
                for(int j = i * i;j < n;j += i){
                    vec_flag[j] = false;
                }
            }
        }
        return count(vec_flag.begin(),vec_flag.end(),true);
    }

Eratosthenes篩選法雖然效率高,但是Eratosthenes篩選法做了許多無用功,一個數會被篩到好幾次,最後的時間複雜度是O(nloglogn),對於普通素數演算法而言已經非常高效了,但尤拉篩選法的時間複雜度僅僅為O(n).

尤拉演算法是一種空間換時間的演算法。

注意,尤拉篩法並不是一次性將某質數的所有倍數一次性刪去。

prime陣列中的素數是遞增的,當 i 能整除 prime[j],那麼 i*prime[j+1] 這個合數肯定可以被 prime[j] 乘以某個數篩掉。
因為i中含有prime[j], prime[j] 比 prime[j+1] 小。接下去的素數同理。所以不用篩下去了。

在第一次滿足i%prme[j]==0這個條件時,prime[j]必定是prime[j]*i的最小因子。

尤拉篩法程式碼

#include <iostream>
using namespace std;
 
const int MAXN = 3000001;
int prime[MAXN];//儲存素數 
bool vis[MAXN];//初始化 
int Prime(int n)
{
	int cnt = 0;
	memset(vis, 0, sizeof(vis));
	//篩選與Eratosthenes不同,並不是按照順序篩選,但每一個合數都等於一個數字乘以它的最小素因子,所以遍歷每個數字 i 乘以小於i(若大於i,則i為最小素因子)的所有素因子可以保證,每個合數都被遍歷到
	for (int i = 2; i< n; i++)   //遍歷 n 以內的數
	{
		if (!vis[i])
			prime[cnt++] = i;
		for (int j = 0; j < cnt && i * prime[j] < n; j++)	//遍歷質數陣列
		{
			vis[i*prime[j]] = 1;//i*prime[j]這個數的最小質因子就是prime[j]
			if (i%prime[j] == 0)//關鍵  每一個篩選數,只被一個數乘以它的最小素因子,如果i % prime[j] == 0,則證明 i中含有prime[j]這個素因子,所以prime[j + 1] 至 prime[prime.size()-1]都不是最小素因子
				break;
		}
	}
	return cnt;//返回小於n的素數的個數 
}

2.質數應用於字串問題

2.1 問題

假設有一個長字串和一個短字串。從演算法上講,什麼方法能最快的查出所有小字串裡的字母在大字串裡都有?

比如,如果是下面兩個字串:
  String 1: ABCDEFGHLMNOPQRS
  String 2: DCGSRQPOM
答案是true,所有在string2裡的字母string1也都有。如果是下面兩個字串:
  String 1: ABCDEFGHLMNOPQRS
  String 2: DCGSRQPOZ
答案是false,因為第二個字串裡的Z字母不在第一個字串裡。

2.2 思路

假設我們有一個一定個數的字母組成字串——給每個字母分配一個素數,從2開始,往後類推。這樣A將會是2,B將會是3,C將會是5,等等。現在遍歷長字串和短字串,把每個字母代表的素數相乘,會得到兩個很大的整數。用長字串的整數除以短字串的整數,如果不能整除說明有不匹配的字母。

3.位運算

3.1 問題一

有一個 n 個元素的陣列,除了一個元素只出現一次外,其他元素都出現兩次,讓你找出這個只出現一次的元素是幾,要求時間複雜度為 O(n) 且不再開闢新的記憶體空間。

思路

將所有元素做異或運算,即 a[1] xor a[2] xor a[3] xor a[4]…xor a[n],所得到的結果就是那個只出現一次的數字,時間複雜度為 O(n)

3.2 問題二

有一個 n 個元素的陣列,除了兩個元素只出現一次外,其他元素都出現兩次,讓你找出這兩個只出現一次的元素分別是幾,要求時間複雜度為 O(n) 且再開闢新的記憶體空間固定(與 n 無關)。

思路

首先,和前面的演算法思路一樣,先把所有元素異或,得到的結果就是那兩個只出現一次的元素異或的結果。

然後,重點來了,因為這兩個只出現一次的元素一定是不相同的,所以這兩個元素的二進位制形式肯定至少有某一位是不同的,即一個為0,另一個為1,找到這一位。(找到一位不同的就行,不用全部找出來)

可以根據前面異或得到的數字找到這一位,怎麼找呢?稍加分析就可以知道,異或得到這個數字二進位制形式中任意一個為1的位都是我們要找的那一位,找到這一位就可以了(這很容易)。
再然後,以這一位是1還是0為標準,將陣列的n個元素分成了兩部分,將這一位為0的所有元素做異或,得出的數就是隻出現一次的數中的一個;將這一位為1的所有元素做異或,得出的數就是隻出現一次的數中的另一個。從而解出題目。忽略尋找不同位的過程,總共遍歷陣列兩次,時間複雜度為O(n)。

舉個例子說:
以 1,1,2,3,3,4為例,寫成二進位制是001,001,010,011,011,100,將這6個數做異或運算後得6,二進位制為110。根據異或運算的性質,得到從左起的第二位,這兩個只出現一次的數在這一位是不同的,根據我們的例子也可以看出來,010和100在左起第二位分別是1和0,然後將所有左起第二位為1的做異或,那麼哪些是左起第二位為1的數呢?在我們的例子中,有011、011、010,顯然,異或結果就是010;同樣的,將左起第二位為0的所有數做異或,即001、001、100,顯然結果為100。
最後一段其實將問題劃歸為只有一個數單獨出現一次的情形,也就是分成了兩個問題一。

3.3 問題三

如何不使用語言自帶的 + 來計算 a+b

思路

假設有兩個二進位制的數,比如3(011)和 5(101),按照每一位對應異或的結果便是:110(最低位沒有進位,其他位正常相加),所以如果要實現一個加法的話,可以考慮用異或的方式解決,但是需要手動考慮一下進位的問題。

那什麼時候該進位呢?肯定是兩個地方都是 1 的地方,所以我們需要一個與(&)運算來確定,如果兩個都是 1 ,那麼 & 的結果一定是 1 ,否則是 0 ,有了這樣的思路之後我們就可以寫出如下程式:

int add(int a, int b){
	while(b!=0){
    int tmp_a=a ^ b;
    int tmp_b=(a & b) << 1; //關鍵思路
    a=tmp_a;
    b=tmp_b;
   }
   return a;
}

3.4 問題四

交換兩個數 x 和 y,不能使用額外的輔助變數

程式碼:

x=x^y (1)
y=x^y (2)
x=x^y (3)

思路:

我們知道,兩個相同的數異或之後結果會等於 0,即 n ^ n = 0。並且任何數與 0 異或等於它本身,即 n ^ 0 = n。所以,解釋如下:

把(1)中的 x 帶入 (2)中的 x,有

y = x^y = (xy)y = x(yy) = x^0 = x。 x 的值成功賦給了 y。

對於(3),推導如下:

x = x^y = (xy)x = (xx)y = 0^y = y。

3.5 問題五

找出不大於N的最大的2的冪指數

思路

例如 N = 19,那麼轉換成二進位制就是 00010011(這裡為了方便,我採用8位的二進位制來表示)。那麼我們要找的數就是,把二進位制中最左邊的 1 保留,後面的 1 全部變為 0。即我們的目標數是 00010000。那麼如何獲得這個數呢?相應解法如下:

1、找到最左邊的 1,然後把它右邊的所有 0 變成 1(同或)
在這裡插入圖片描述

2、把得到的數值加 1,可以得到 00100000即 00011111 + 1 = 00100000。

3、把 得到的 00100000 向右移動一位,即可得到 00010000,即 00100000 >> 1 = 00010000。

3.6 小技巧

巧用 x & (x-1) 消去 x 最後一位的 1

x = 1100
x - 1 = 1011
x & (x - 1) = 1000

3.6.1 問題1

檢測一個數是否是2的冪次

思路

通過二進位制的方法來看,可以發現一些特徵,2的冪次對應的二進位制只有最左邊位的一個1,其他位都是0,程式如下:

bool check(int n){
	return n > 0 && (n & (n-1)) == 0;
}

3.6.2 問題2

計算在一個32位的整數的二進位制式中有多少個1

思路:

不斷使用 x & (x-1) 消去x最後一位的1,計算總共消去了多少次即可。

程式碼:

public int NumberOf12(int n) {
        int count = 0;
        int k = 1;
        while (n != 0) {
            count++;
            n = (n - 1) & n;
        }
        return count;

4.數學問題

4.1 給定一個整數 n ,返回 n!結果尾數中零的數量
示例:
輸入:5
輸出:1
解釋:5!= 120,尾數中有一個0
說明:演算法的時間複雜度應為 O(logn)

思路:

可以發現,9 個數字中只有 2(它的倍數) 與 5 (它的倍數)相乘才有 0 出現。

所以,現在問題就變成了這個階乘數中能配 多少對 2 與 5。

可以發現,一個數字進行拆分後 2 的個數肯定是大於 5 的個數的,所以能匹配多少對取決於 5 的個數。(好比現在男女比例懸殊,最多能有多少對異性情侶取決於女生的多少)

那麼問題又變成了 統計階乘數裡有多少個 5 這個因子。

需要注意的是,像 25,125 這樣的不只含有一個 5 的數字的情況需要考慮進去。

比如 n = 15。那麼在 15! 中 有 3 個 5 (來自其中的5, 10, 15), 所以計算 n/5 就可以 。

但是比如 n=25,依舊計算 n/5 ,可以得到 5 個5,分別來自其中的5, 10, 15, 20, 25,但是在 25 中其實是包含 2個 5 的,這一點需要注意。

所以除了計算 n/5 , 還要計算 n/5/5 , n/5/5/5 , n/5/5/5/5 , …, n/5/5/5,/5直到商為0,然後求和即可。

public class Solution {
    public int trailingZeroes(int n) {
        return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
    }
} 

4.2 你有 4 張寫有 1 到 9 數字的牌。你需要判斷是否能通過 *,/,+,-,(,) 的運算得到 24。

示例 1:

輸入: [4, 1, 8, 7]
輸出: True
解釋: (8-4) * (7-1) = 24
示例 2:

輸入: [1, 2, 1, 2]
輸出: False

注意:
1.除法運算子 / 表示實數除法,而不是整數除法。例如 4 / (1 - 2/3) = 12 。
2.每個運算子對兩個數進行運算。特別是我們不能用 - 作為一元運算子。例如,[1, 1, 1, 1]作為輸入時,表示式 -1 - 1 - 1 - 1 是不允許的。
3.你不能將數字連線在一起。例如,輸入為 [1, 2, 1, 2] 時,不能寫成 12 + 12 。

程式碼

class Solution:
  def judgePoint24(self, nums):
    bad = '對撒剘劥圞剜劏哱掶桺泛揋掵従剟剣彫寣汙愨壛梄甏咍哲汭剤堧點卋嬞勆叛汬泐塵棟劚嚮咃寵吖剗楗囧力桻攋壯劯嗏桹劙剢剚焧啫栕炸栫棲嚲彳剛撐烴洿宋汷彲剙揁妷埻撧汢吩壙劇剭埼呂剝汣敯憇勇剝咎囻匓'
    return chr(int(''.join(map(str, sorted(nums)))) + 19968) not in bad

原理

因為在 24點 遊戲中,四個數字,每個數字的取值區間為 [ 1 - 9 ], 無重複組合總數為 495 組,其中以下 92 組為無解組合:

1111, 1112, 1113, 1114, 1115, 1116, 1117, 1119, 1122, 1123, 1124, 1125, 1133, 1159, 1167, 1177, 1178, 1179, 1189, 1199, 1222, 1223, 1299, 1355, 1499, 1557, 1558, 1577, 1667, 1677, 1678, 1777, 1778, 1899, 1999, 2222, 2226, 2279, 2299, 2334, 2555, 2556, 2599, 2677, 2777, 2779, 2799, 2999, 3358, 3388, 3467, 3488, 3555, 3577, 4459, 4466, 4467, 4499, 4779, 4999, 5557, 5558, 5569, 5579, 5777, 5778, 5799, 5899, 5999, 6667, 6677, 6678, 6699, 6777, 6778, 6779, 6788, 6999, 7777, 7778, 7779, 7788, 7789, 7799, 7888, 7899, 7999, 8888, 8889, 8899, 8999, 9999

所以只需要將這 92 種情況進行 Unicode 編碼,然後對於給定輸入,排序轉為字串後查詢是否在這 92 種情況的編碼中。

4.3 給定一個整數,寫一個函式來判斷它是否是 3 的冪次方。

進階:你能不使用迴圈或者遞迴來完成本題嗎?

思路
3 的冪次的質因子只有 3。題目要求輸入的是 int 型別,正數範圍是 0 - 2^31,在此範圍中允許的最大的 3 的次方數為 3^19 = 1162261467 ,那麼只要看這個數能否被 n 整除即可。

class Solution {
    public boolean isPowerOfThree(int n) {
        return n > 0 && 1162261467%n == 0;
    }
}

5.現實生活問題

5.1 扔雞蛋

經典的動態規劃問題,題設是這樣的:
如果你有2顆雞蛋,和一棟36層高的樓,現在你想知道在哪一層樓雞蛋剛好被摔碎,應該如何用最少的測試次數對於任何答案樓層都能夠使問題得到解決。

----如果你從某一層樓扔下雞蛋,它沒有碎,則這個雞蛋你可以繼續用
----如果這個雞蛋摔碎了,則你可以用來測試的雞蛋減少一個
----所有雞蛋的質量相同(都會在同一樓層以上摔碎)
----對於一個雞蛋,如果其在樓層i扔下的時候摔碎了,對於任何不小於i的樓層,這個雞蛋都會被摔碎
----如果在樓層i扔下的時候沒有摔碎,則對於任何不大於i的樓層,這顆雞蛋也不會摔碎
----從第1層扔下,雞蛋不一定完好,從第36層扔下,雞蛋也不一定會摔碎。

問題的關鍵之處在於,測試之前,你並不知道雞蛋會在哪一層摔碎,你需要找到的是一種測試方案,這種測試方案,無論雞蛋會在哪層被摔碎,都至多隻需要m次測試,在所有這些測試方案中,m的值最小。

思路:

對於只有1顆雞蛋的情況,我們別無選擇,只能從1樓開始,逐層向上測試,直到第i層雞蛋摔碎為止,這時我們知道,會讓雞蛋摔碎的樓層就是i(或者直到頂層,雞蛋也沒有被摔碎),其他的測試方案均不可行,因為如果第1次測試是在任何i>1的樓層扔下雞蛋,如果雞蛋碎了,你就無法確定,i-1層是否也會令雞蛋摔碎。所以對於1顆雞蛋而言,最壞的情況是使雞蛋摔碎的樓層數i>=36,此時,我們需要測試每個樓層,總共36次,才能找到最終結果,所以1顆雞蛋一定能解決36層樓問題的最少測試次數是36.

對於2個雞蛋,36層樓的情況,你可能會考慮先在第18層扔一顆,如果這顆碎了,則你從第1層,到第17層,依次用第2顆雞蛋測試,直到找出答案。如果第1顆雞蛋沒碎,則你依然可以用第1顆雞蛋在27層進行測試,如果碎了,在第19~26層,用第2顆雞蛋依次測試,如果沒碎,則用第1顆雞蛋在32層進行測試,……,如此進行(有點類似於二分查詢)。這個解決方案的最壞情況出現在結果是第17/18層時,此時,你需要測試18次才能找到最終答案,所以該方案,解決36層樓問題的測試次數是18.

相較於1顆雞蛋解決36層樓問題,測試次數實現了減半,但是18並不是確保解決2顆雞蛋,36層樓問題的最小值(實際的最小值是8).

我們可以將這樣的問題簡記為W(n,k),其中n代表可用於測試的雞蛋數,k代表被測試的樓層數。對於問題W(2,36)我們可以如此考慮,將第1顆雞蛋,在第i層扔下(i可以為1~k的任意值),如果碎了,則我們需要用第2顆雞蛋,解決從第1層到第i-1層樓的子問題W(1,i-1),如果這顆雞蛋沒碎,則我們需要用這兩顆雞蛋,解決從i+1層到第36層的子問題W(2,36-i),解決這兩個問題,可以分別得到一個嘗試次數p,q,我們取這兩個次數中的較大者(假設是p),與第1次在i層執行測試的這1次相加,則p+1就是第一次將雞蛋仍在i層來解決W(2,36)所需的最少測試次數次數ti。對於36層樓的問題,第一次,我們可以把雞蛋仍在36層中的任何一層,所以可以得到36中解決方案的測試次數T{t1,t2,t3,……,t36},在這些結果中,我們選取最小的ti,使得對於集合T中任意的值tj(1<=j<=36,j!=i),都有ti<=tj,則ti就是這個問題的答案。用公式來描述就是W(n, k) = 1 + min{max(W(n -1, x -1), W(n, k - x))}, x in {2, 3, ……,k},其中x是第一次的測試的樓層位置。

其中W(1,k) = k(相當於1顆雞蛋測試k層樓問題),W(0,k) = 0,W(n, 0) = 0

所以在計算W(2,36)之前,我們需先計算出所有W(1,0),……,W(1,36),W(2,0),……,W(2,35)這些的值,可以用遞推的方法實現,程式碼如下:

unsigned int DroppingEggsPuzzle(unsigned int eggs, unsigned int floors)
{
	unsigned int i, j, k, t, max;
 
	unsigned int temp[eggs + 1][floors + 1];
 
	for(i = 0; i < floors + 1; ++i)
	{
		temp[0][i] = 0;
		temp[1][i] = i;
	}
 
	for(i = 2; i < eggs + 1; ++i)
	{
		temp[i][0] = 0;
		temp[i][1] = 1;
	}
 
	for(i = 2; i < eggs + 1; ++i)
	{
		for(j = 2; j < floors + 1; ++j)
		{
			for(k = 1, max = UINT_MAX; k < j; ++k)
			{
				t = temp[i][j - k] > temp[i - 1][k -1] ?  temp[i][j - k] : temp[i - 1][k -1];
 
				if(max > t)
				{
					max = t;
				}
			}
 
			temp[i][j] = max + 1;
		}
	}
 
	return temp[eggs][floors];
}

該演算法的空間複雜度是O(nk),時間複雜度是O(nk^2),對於規模較大的問題,無論是空間還是時間複雜度都很可觀。

這個演算法可以計算出W(2,36)問題的最少測試次數是8,但是卻不能給出用2顆雞蛋解決36層樓問題的具體方案,這裡我就給出一個測試方案:

用第一顆雞蛋分別在8,15,21,26,30,33,35層進行測試
如果雞蛋在某一層碎了(例如26層),則在前一測試點由下到上依次測試,例如(22,23,24,25),直到找到滿足條件的樓層為止
如果雞蛋在第35層的測試中也沒碎,則用該雞蛋在第36層再測試一次
該方案可以保證,無論滿足條件的樓層是多少,都可以在最多8次測試之後找到答案,例如目標樓層為28時,該方案的測試順序為8,15,21,26,30,27,28,總共測試7次。