《算法筆記》閱讀筆記


這是之前刷PAT時看胡凡的《算法筆記》時做的一點筆記,后來沒時間,就沒看完。

emmm,剛開始看的內容都很基礎。現在想想這樣從頭到尾地學確實沒必要,可能當時的目標也和現在不一樣吧。

第一章 如何使用本書

在線評測系統

PAT是“單點測試”。

常見的評測結果

整理常見的測評結果

第二章 C/C++快速入門

cincout消耗的時間比scanfprintf多得多,很多題目可能輸入還沒結束就超時了。……。請不要同時在一個程序中使用coutprintf,有時候會出問題。

頭文件的hhead的縮寫

C++向下兼容C

整理一下向下兼容、向上兼容、向前、向后

基本數據類型

int\(-2^{31}\sim+(2^{31}-1)\),大致范圍是\(-2\times10^9\sim2\times10^9\),絕對值在\(10^9\)以內的整數都可以定義成int型。

long long\(-2^{63}\sim+(2^{63}-1)\),大致范圍是\(-9\times10^{18}\sim9\times10^{18}\)

浮點型都用double來存儲。

小寫字母比大寫字母的ASCII碼大32。

\0代表空字符NULL,其ASCII碼為0,請注意\0不是空格。

整型常量在賦值給布爾型變量時會自動轉換為true(非零)和false(零)。

宏定義是直接將對應的部分替換,然后才進行編譯和運行。

輸入和輸出

類似於13:45:20這種hh:mm:ss的時間需要輸入,可以使用下邊的代碼:

int hh,mm,ss;
scanf("%d:%d:%d",&hh,&mm,&ss);

scanf的雙引號內的內容其實就是整個輸入,只不過把數據換成它們對應的格式符並把變量的地址按次序寫在后面而已。

除了%c以外(scanf%c格式是可以讀入空格和換行的),scanf對其他格式符(如%d)的輸入是以空白符(即空格、Tab)為結束判斷標志的,字符數組使用%s讀入的時候以空格和換行為讀入結束的標志。

double輸出格式為%f,輸入格式為%lf

%.mf保留\(m\)位小數,這個“保留”使用的是精度的“四舍六入五成雙”規則。

getchar可以識別換行符。

常用數學函數

round函數,四舍五入。

整理常用數學函數

條件判斷

條件判斷中n!=0可以改為nn==0可以改為!n

循環語句

do...while會先執行循環體一次,然后才去判斷循環條件是否為真,這就使得do...while語句的實用性遠不如while,因為用戶碰到的大部分情況都需要能處理在某些數據下不允許進入循環的情況。

C語言中不允許隨時定義臨時變量,C++可以。

一維數組

數組大小必須是整數常量,不可以是變量。

如果只初始化了一維數組的一部分元素,后面未被初始化的元素將會由不同編譯器內部實現的不同而被賦以不同的初值,而一般情況默認初值為0。所以可以通過下面的代碼實現整個數組賦初值0

int a[10]={};
int a[10]={0};

遞推可以分為順推和逆推兩種。

冒泡排序的本質在於交換,即每次通過交換的方式把當前剩余元素的最大值移動到一端,當剩余元素數量減少為0時,排序結束。代碼如下:

// 定義含5個元素的數組
int a[]={3,1,4,5,2};
int n=5;

// 冒泡排序(從小到大)
for(int i=1;i<n;++i){	//5-1趟
    for(int j=0;j<n-i;++j){
        if(a[j]>a[j+1]){	// 交換
            int temp=a[j];
            a[j]=a[j+1];
            a[j+1]=temp;
        }
    }
}

// 輸出數組元素
for(int i=0;i<n;++i){
    printf("%d ",a[i]);
}

二維數組

可以把二維數組當作一維數組的每一個元素都是一個一維數組。

二維數組初始化(下面的代碼中未被初始化的元素一般默認為0):

int a[5][6]={{3,1,2},{8,4},{},{1,2,3,4,5}};

如果數組大小較大(大概\(10^6\)級別),則需要定義在主函數外邊。原因是函數內部申請的局部變量來自系統棧,允許申請的空間較小。而函數外部申請的全局變量來自靜態存儲區,允許申請的空間較大。

memset

一般用函數memsetfill給數組中每一個元素賦相同的值。

使用memset需要使用頭文件string.h,使用fill需要使用STL中的頭文件algorithm

memset格式如下:

memset(數組名,值,sizeof(數組名));

只建議初學者使用memset賦0和-1,因為memset是按字節賦值,即對每個字節賦同樣的值,0的二進制補碼為全0,-1的二進制補碼為全1,不容易弄錯。如果要對數組賦其它值,請使用fill函數(但memset的執行速度快)。

字符數組

可以使用字符串對字符數組進行初始化,但也僅限於初始化。

%c能夠識別空格和換行並將其輸入,%s`通過空格或換行來識別一個字符串的結束。

gets識別換行符作為輸入結束(並會將其讀取走),因此scanf完一個整數后,如果要用gets,需要用getchar接受整數后的換行符。

一維字符數組的末尾都會有一個\0,表示字符串的結束。使用getsscanf時會自動在字符串后邊添加\0putsprintf也通過識別\0作為字符串的結尾來輸出字符串。

string.h

  • strlen(字符數組1)

    得到字符數組\0前邊的字符數

  • strcmp(字符數組1,字符數組2)

    按照字典序比較兩個字符串的大小,返回一個整數(負數,0,正數),符號和字符串1-字符串2相同。

  • strcpy(字符數組1,字符數組2)

    把字符串2(第二個參數)賦值給字符串1(第一個參數)

  • strcat(字符數組1,字符數組2)

    把字符數組2接到字符數組1后邊

  • sscanf(字符數組,"%d",&n)

    從字符數組中讀

  • sprintf(字符數組,"%d",n)

    往字符數組中寫

數組作為函數參數

數組作為參數時,參數中數組的第一維不需要填寫長度(如果是二維數組,那么第二維需要填寫長度),實際調用時

指針

指針是一個unsigned類型的整數

int* p1,p2;	//p1是int型指針 p2是int型變量
int* p1,*p2;	//p1和p2都是int型指針

cin

char str1[100];
cin.getline(str1,100);

string str2;
getline(cin,str2);

浮點數的比較

由於計算機采用有限位的二進制編碼,因此浮點數在計算機的存儲並不總是精確的,於是需要引入一個極小的EPS來對這種誤差進行修正。

待補充:通過EPS進行浮點數的比較

復雜度

  • 時間復雜度

    在時間復雜度中,高等級的冪次會覆蓋低等級的冪次。當有些算法實現較為復雜時,其常數會比較大,這時即便時間復雜度(一般講時間復雜度是不帶系數的)相同,其性能也會有較大差距。

    對一般的OJ系統來說,1秒能承受的運算次數大概是\(10^7\sim10^8\),因此\(O(n^2)\)的算法當n的規模是1000時是可以承受的,而當n的規模為100000時則是不可承受的。

  • 空間復雜度

    在一般的應用中,一般來說空間都是足夠使用的(只要不開好幾個\(10^7\)以上的數組即可,例如int A[10000][10000]的定義就是不合適的)。

    \(O(1)\)的空間復雜度指的是算法消耗的空間不隨數據規模的增大而增大。

    考慮到空間一般夠用,因此常常采用以空間換時間的策略。

  • 編碼復雜度

    編碼復雜度是一個定性的概念,並沒有什么量化的標准。

黑盒測試

黑盒測試是指:系統后台會准備若干組輸入數據,然后將其輸到提交的程序中,如果輸出的結果與正確答案完全相同(字符串意義上的比較),那么就稱通過了這道題的黑盒測試,否則會根據錯誤類型而返回不同的結果。

根據黑盒測試是否對每組輸入數據都單獨測試或是一次性測試所有測試數據,又可以分為單點測試多點測試

整理多點測試的幾種類型

第三章 入門篇(1)——入門模擬

第四章 入門篇(2)——算法初步

簡單選擇排序

選擇排序是最簡單的排序算法之一,本節介紹眾多選擇排序方法中最常用的簡單選擇排序

將數組分為前后兩部分:有序部分和無序部分。遍歷數組,每次選擇最小的值放在數組前邊(有序部分),時間復雜度為\(O(n^2)\)

// 定義含5個元素的數組
int a[]={3,1,4,5,2};
int n=5;

// 選擇排序(從小到大)
for(int i=0;i<n;++i){
	int k=i;	// 默認當前待排序的第一個值是最小值
    for(int j=i+1;j<n;j++){	// 遍歷之后的待排序的值,尋找最小值
        if(a[j]<a[k]){	// 更新待排序的最小值的下標
            k=j;
        }
    }
    
    // 交換
    int temp=a[i];
    a[i]=a[k];
    a[k]=temp;
}

// 輸出數組元素
for(int i=0;i<n;++i){
    printf("%d ",a[i]);
}

插入排序

插入排序也是最簡單的一類排序方法,本節主要介紹眾多插入排序方法中最直觀的直接插入排序

把序列分為左右兩部分:有序(左)和無序(右),從無序的部分取出元素,插入有序序列對應位置。

// 定義含5個元素的數組
int a[]={3,1,4,5,2};
int n=5;

// 直接插入排序(從小到大)
for(int i=1;i<n;++i){
    
    // 記錄要插入的值
	int temp=a[i],j=i;
    
    // 有序元素后移
    while(j>0&&temp>a[j-1]){
        a[j]=a[j-1];
        j--;
    }
    
    // 插入
    a[j]=temp;
}

// 輸出數組元素
for(int i=0;i<n;++i){
    printf("%d ",a[i]);
}

散列的定義和整數散列

散列(hash)是常見的算法思想之一,在很多程序中都會有意無意地使用到。

若給出\(N\)個正整數,再給出\(M\)個正整數,問這M個數中的每個數分別是在\(N\)個數出現過,其中\(N,M\leq10^5\),且所有正整數均不超過\(10^5\)

對這個問題,最直接的思路是:對每個欲查詢的正整數\(x\),遍歷\(N\)個數,看是否有一個數與x相等。這種做法的時間復雜度是\(O(NM)\),當\(N\)\(M\)都很大(\(10^5\)級別)時,顯然是無法承受的。

不妨用空間換時間,即設定一個bool型數組hashTable[100010],其中hashTable[x]==true表示正整數\(x\)\(n\)個正整數中出現過。這樣就可以在一開始讀入\(N\)個正整數時就對hashTable進行賦值,於是對於\(M\)個欲查詢的數,就能直接通過hashTable判斷出每個數是否出現過。顯然這種做法的時間復雜度為\(O(M+N)\)

同樣的,如果題目要求統計次數而非是否出現,就把數組改成int型,這兩個問題的解法都有一個特點,那就是直接把輸入的數作為數組的下標來對這個數的性質進行統計(這種做法非常實用,請務必掌握)。這是一個很好的用空間換時間的策略,因為它將查詢的復雜度降到了\(O(1)\)級別。

但這個策略暫時還有一個問題——上面的題目中出現的每個數都不會超過\(10^5\),因此直接作為數組下標是可行的,但是如果輸入可能是\(10^9\)大小的整數,或者甚至是一個字符串,就不能將它們直接作為數組下標了。

這時可以使用散列。一般來說,散列可以濃縮成“將元素通過一個函數轉換為整數,使得改正數可以盡量唯一地代表這個元素”,其中把這個轉換函數稱為散列函數H,也就是說,如果元素在轉換前為key,那么轉換后就是一個整數H(key)

對於key是整數的情況來說,常用的散列函數有直接定址法、平方取中法、除留余數法。

H(key1)==H(key2),這種情況叫作沖突常用的解決沖突的方法有線性探查法、平方探查法和鏈地址法,其中前兩種都計算了新的hash值,又稱為開放定址法。

在寫代碼時,這種散列的功能可以用STL中的map代替。

字符串hash初步

一個點\(P\)的坐標\((x,y)\)可以用下面的散列函數進行處理:

\(H(P)=x\times Range+y\),這樣對數據范圍內的任意兩個整點\(P_1\)\(P_2\)\(H(P_1)\)都不會等於\(H(P_2)\)

字符串hash是指將一個字符串\(S\)轉換為一個整數,使得該整數可以盡可能地唯一地代表字符串\(S\)。本節只討論將字符串轉換為唯一的整數。

假設字符串\(S\)由大寫字母\(A \sim Z\)構成。在這個假設下,可以把26個大寫字母視為\(0\sim25\),進而轉換為二十六進制,再轉換為十進制,可得一唯一整數。只是\(S\)的長度並不可太長。

在上面的假設下,假如還可以由\(a\sim z\)組成,可以再把\(a\sim z\)對應為\(26\sim51\),進而轉換為五十二進制,再轉換為十進制。

分治

分治法(divide and conquer)將原問題划分成若干個規模較小而結構與原問題相同或相似的子問題,然后分別地解決這些子問題,最后合並子問題的解,即可得到原問題的解。

分治法的三個步驟:

  1. 分解

    將原問題分解為若干和原問題擁有相同或相似結構的子問題

  2. 解決

    遞歸求解所有子問題。如果存在子問題的規模小到可以直接解決,就直接解決它。

  3. 合並

    將子問題的解合並成原問題的解。

分治法分解出的子問題應當是相互獨立、沒有交叉的。如果存在兩個子問題有相交部分,那么不應當使用分治法解決。

廣義上來講,分治法分解成的子問題個數只要大於0即可。從嚴格的定義上講,一般把子問題的個數為1的情況稱為減治,而把子問題個數大於1的情況稱為分治。

分治法作為一種算法思想,既可以使用遞歸的手段去實現,也可以通過非遞歸的手段去實現。

遞歸

遞歸有兩個重要概念:

  1. 遞歸邊界
  2. 遞歸式

\(n\)的階乘體現了減治的思想,求\(Fibonacci\)數列的第\(n\)項體現了分治的思想。

一般把\(1\sim n\)\(n\)個整數按某個順序擺放的結果稱為這\(n\)個整數的一個排列,而全排列指這\(n\)個整數能形成的所有排列。

\(n\)皇后問題就可以使用解決全排列問題的方法去解決。

如果在到達遞歸邊界前的某層,由於一些事實導致已經不需要往任何一個子問題遞歸,就可以直接返回上一層。一般把這種做法稱為回溯法

簡單貪心

貪心法是求解一類最優化問題的方法,它總是考慮當前狀態下局部最優(或較優)的策略,來使全局的結果達到最優(或較優)。

可以使用貪心法的問題一定滿足最優子結構性質,即一個問題的最優解可以由它的子問題的最優解構造出來。

要獲得全局最優解,則要求中間的每步策略都是最優的,因此嚴謹使用貪心法來求解最優哈問題需要對采取的策略進行證明。證明的一般思路是使用反證法及數學歸納法,即假設策略不能導致最優解,然后通過一系列推導來得到矛盾,以此證明策略是最優的,最后用數學歸納法保證全局最優。不過對於平常來說,不太容易對想到的策略進行嚴謹的證明(貪心的證明往往比貪心本身更難),如果在想到某個似乎可行的策略,並且自己無法舉出反例,那就勇敢地實現它。

區間貪心

區間不相交問題和區間選點

二分查找

二分查找是基於有序序列的查找算法,二分查找的高效之處在於,每一步都可以去除當前區間中的一半元素,因此其時間復雜度是\(O(logn)\)

如果序列是嚴格遞增

  • 遞歸方法
#include <iostream>
using namespace std;

int binarySearch(int* arr,int left,int right,int key);

int main()
{
    // 定義含10個元素的升序數組
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查詢key
    int key=15;
    printf("%d\n", binarySearch(a, 0, n-1, key));

    system("pause");
    return 0;
}

// 在數組arr的[left,right]中尋找key,找到則返回key在數組中的位置,否則返回-1
// 初始區間一般是[0,n-1]
int binarySearch(int *arr, int left, int right, int key)
{
    // 區間正確
    if(left<=right){
        // 設置區間中間下標
        int mid = (left + right) / 2;

        // 找到
        if (key == arr[mid])
        {
            return mid;
        }
        // 左半區間查找
        else if (key < arr[mid])
        {
            return binarySearch(arr, left, mid - 1, key);
        }
        // 右半區間查找
        else
        {
            return binarySearch(arr, mid+1,right, key);
        }
    }

    // 區間錯誤
    return -1;
}
  • 非遞歸方法

在程序設計時,更多采用的是非遞歸的寫法。

#include <iostream>
using namespace std;

int binarySearch(int* arr,int left,int right,int key);

int main()
{
    // 定義含10個元素的升序數組
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查詢key
    int key=19;
    printf("%d\n", binarySearch(a, 0, n-1, key));

    system("pause");
    return 0;
}

// 在數組arr的[left,right]中尋找key,找到則返回key在數組中的位置,否則返回-1
// 初始區間一般是[0,n-1]
int binarySearch(int *arr, int left, int right, int key)
{
    int mid;

    //當可以形成區間時,進行查找
    while(left<=right){
        // 設置區間中間下標
        mid = (left + right) / 2;

        // 找到,返回對應位置
        if (key==arr[mid]){
            return mid; 
        }
        // key在左半區間,更新right
        else if(key<arr[mid]){
            right=mid-1;
        }
        // key在右半區間,更新left
        else{
            left=mid+1;
        }
    }

    // 未尋找到
    return -1;
}

如果序列是嚴格遞減

只需要把if(key<arr[mid])語句中的<改成>就好了。

如果序列是非嚴格遞增(即遞增,但元素可能重復)

如何求出序列中第一個大於等於key的元素的位置L和第一個大於key的元素的位置R,這樣元素key在序列中的存在區間就是左閉右開區間\([L,R)\)。顯然,如果序列中沒有key,那么LR可以理解為假設序列中存在keykey應該在的位置。

上面有兩個問題:

  • L
#include <iostream>
using namespace std;

int lowerBound(int *arr, int left, int right, int key);

int main()
{
    // 定義含10個元素的升序數組
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查詢key
    int key=4;
    printf("%d\n", lowerBound(a, 0, n, key));

    system("pause");
    return 0;
}

// 在非嚴格遞增數組arr的[left,right]中尋找第一個大於等於key的值的位置,如果不存在則返回該值應該在的位置(即最后一個元素后邊)
// 初始區間一般是[0,n]
int lowerBound(int *arr, int left, int right, int key)
{
    int mid;

    // 當left==right時,剛好求出大於等於key的第一個數字的位置
    while(left<right){
        // 設置區間中間下標
        //mid = (left + right) / 2;
        mid=left+(right-left)/2;    // 避免溢出

        // key在左半區間,更新right
        if(key<=arr[mid]){
            right=mid;
        }
        // key在右半區間,更新left
        else{
            left=mid+1;
        }
    }

    // 未尋找到
    return left;
}
  • R
#include <iostream>
using namespace std;

int upperBound(int *arr, int left, int right, int key);

int main()
{
    // 定義含10個元素的升序數組
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查詢key
    int key=4;
    printf("%d\n", upperBound(a, 0, n, key));

    system("pause");
    return 0;
}

// 在非嚴格遞增數組arr的[left,right]中尋找第一個大於key的值的位置,如果不存在則返回該值應該在的位置(即最后一個元素后邊)
// 初始區間一般是[0,n]
int upperBound(int *arr, int left, int right, int key)
{
    int mid;

    // 當left==right時,剛好求出大於等於key的第一個數字的位置
    while(left<right){
        // 設置區間中間下標
        //mid = (left + right) / 2;
        mid=left+(right-left)/2;    // 避免溢出

        // key在左半區間,更新right
        if(key<arr[mid]){
            right=mid;
        }
        // key在右半區間,更新left
        else{
            left=mid+1;
        }
    }

    // 未尋找到
    return left;
}

對比lowerBoundupperBound的代碼可知,upperBound函數只是把代碼中的if(key<=arr[mid])改成if(key<arr[mid]),其他完全相同,這啟發我們去尋找它們的共同點。

可以發現,兩者都在解決這樣一個問題:尋找有序序列中第一個滿足某條件C的元素的位置。這是一個非常重要且經典的問題,平常能碰到的大部分二分法問題都可以歸結於這個問題。顯然,所謂的條件C在序列中一定是從左到右先不滿足,然后滿足的。該類問題代碼模板如下:

int solve(int left,int right)
{
    int mid;
    while(left<right){
        mid=left+(right-left)/2;
        if(C成立){
        	right=mid;    
        }
        else{
            left=mid+1;
        }
    }
    return left;
}

若想尋找最后一個滿足條件C的元素的位置,則可以先求第一個滿足條件!c的元素的位置,然后將該位置減1即可。

如果目的是判斷有序序列中是否存在滿足某條件的元素,使用剛開始的二分最合適。

二分法的其他應用

上面講了二分查找,事實上二分法的應用遠不止如此。

估算\(\sqrt{2}\)

#include <iostream>
using namespace std;

int main()
{
    // 誤差
    const double eps=10e-5;

    // 
    double left=1,right=2;
    double mid;

    // 逼近2^0.5
    while(right-left>eps){
        // 計算mid
        mid = left+(right - left) / 2;

        if (2>mid * mid){
            left=mid;            
        }
        else if(2<mid*mid){
            right=mid;
        }
    }
    
    // 輸出結果
    mid = left+(right - left) / 2;
    printf("%f\n",mid);

    system("pause");
    return 0;
}

裝水問題

有一個側面看上去是半圓的儲水裝置,該半圓的半徑是\(R\),要求往里面裝入高度為\(h\)的水,使其在側面看去的面積與半圓面積的比例恰好為\(r\)。已知\(r\),求\(h\)

#include <iostream>
#include <cmath>
using namespace std;

int main()
{
    // 一些常量
    const double eps=10e-5;
    const double r=0.5;
    const double R=1;
    const double pi=3.1415926535;

    // 
    double left=0,right=R;
    double mid,alpha,L,S1,S2=pi*R*R/2;

    // 逼近
    while(right-left>eps){
        // 計算mid
        mid = left+(right - left) / 2;
        alpha=2*acos((R-mid)/R);
        L = 2 * sqrt(R * R - (R - mid) * (R - mid));
        S1 = alpha*R*R/2-L*(R-mid)/2;

        if (r>S1/S2){
            left=mid;            
        }
        else if (r < S1 / S2){
            right=mid;
        }
    }
    
    // 輸出結果
    mid = left + (right - left) / 2;
    printf("%f\n",mid);

    system("pause");
    return 0;
}

木棒切割

給出\(N\)根木棒,長度均已知,現在希望通過切割它們來得到至少\(K\)段長度相等的木棒(長度必須是整數),問這些長度相等的木棒最長能有多長。例如對於三根長度分別為10、24、15的木棒來說,假設\(K=7\),即至少需要7段長度相等的木棒,那么可以得到的最大長度為6(\((10+24+15)/7=7\)),在這種情況下,第一根木棒可以提供\(10/6=1\)段、第二根木棒可以提供\(24/6=4\)根、第三根木棒可以提供\(15/6=2\)根,達到了7根的要求。

對於這道題,我們可以注意到一個結論:如果長度相等的木棒的長度\(L\)越長,那么熱可以得到的木棒段數\(k\)越小。

P142


作者:@臭咸魚

轉載請注明出處:https://www.cnblogs.com/chouxianyu/

歡迎討論和交流!



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM