1. 程式人生 > 其它 >2021.1.25-2021.1.31

2021.1.25-2021.1.31

技術標籤:菜鳥

by wjl

2021.1.25-2021.1.31

021.1.25-2021.1.31)

2021.1.25

某大學ACM實驗室寒假新生培訓Day3:演算法基礎一(模擬、列舉、遞推、遞迴)

什麼是程式
演算法+資料結構=程式
什麼是演算法
做飯是演算法,把大象放進冰箱是演算法,為了完成某一件事,需要執行的步驟
什麼是資料結構
雞蛋可以認為是一種母雞封裝好的資料結構,即一些資料的集合
常見演算法:模擬、列舉、遞推、遞迴、二分、貪心、深度優先搜尋(dfs)、廣度優先搜尋(bfs)

模擬

顧名思義,就是寫程式來模擬題目中所要求的操作,只需要按照題面的意思來寫即可。

例題:迴文串

題目描述
輸入一個字串,判斷這個字串是不是迴文串,如果是,輸出“YES”,否則輸出“NO”
“迴文串”是一個正讀和反讀都一樣的字串,比如“level”或者“noon”等等就是迴文串,而“windows”不是

輸入
只有一行,包含一個字串,長度不超過1000
輸出
“YES”或“NO”
樣例輸入
level
樣例輸出
YES
下面就是AC的程式碼了

#include<bits/stdc++.h>
using namespace std;//std::cin>>s;
int main()
{
    string s;
    cin>>s;
    int n=s.length();
    bool ok =true;
    for(int i=0;i<n;i++){
        if(s[i]!=s[n-1-i]){
            ok=false;
        }
    }
    cout<<(ok?"YES":"NO")<<"\n";
//三目運算子  判斷?輸出一:輸出二  如果判斷值為真,輸出一,如果判斷值為假,輸出二
    return 0;
}

三目運算子用法詳解

例題:旋轉吧!雪月花

題目描述
這天,夜夜撿到了一個長為 nn 的陣列,貪玩的她把陣列首尾拼接了起來,並想從任意位置開始遍歷陣列一週。
輸入
第一行給出陣列 aa 的大小 n。
第二行給出 n個數。
第三行給出遍歷的起點 b 。
(1≤n≤100, 1≤ai≤1000, 1≤b≤n)
輸出
從起點遍歷陣列一週的結果。
樣例輸入

5
2 4 3 1 5
2

樣例輸出

4 3 1 5 2

下面就是最開始AC的程式碼了

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int a[n]{};
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    int b;
    cin>>b;//第b個數,下標是b-1
    --b;//轉換下標
    for(int i=b;i<n;i++){//遍歷b到n-1
        cout<<a[i]<<" ";
    }
    for(int i=0;i<b;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}

下面是優化之後的程式碼,用到了取餘的思路

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int a[n]{};
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    int b;
    cin>>b;//第b個數,下標是b-1
    --b;//轉換下標
    for(int i=0;i<n;i++){
        cout<<a[(b+i)%n]<<" ";//當b+i=n之後,就又會從0遍歷到b
    }
    return 0;
}

列舉

指依據題意,列舉所有可能的狀態,並用題目中給定的條件來判斷哪些是需要的,哪些是不需要的。
列舉是初等題目中最常用的思想。

例題:百錢買百雞

題目描述
我國古代數學家張丘建在《算經》一書中提出的數學問題:雞翁一值錢五,雞母一值錢三,雞雛三值錢一。百錢買百雞,問雞翁、雞母、雞雛各幾何?

下面是最開始寫出來的AC程式碼

#include<bits/stdc++.h>
using namespace std;
int main()
{
    for(int i=0;i<=100;i++){//公雞的個數
        for(int j=0;j<=100;j++){//母雞的個數
            for(int k=0;k<=100;k++){//小雞的個數,小雞的只數一定是三的倍數
                if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){
                    cout<<i<<' '<<j<<' '<<k<<"\n";
                }
            }
        }
    }
    return 0;
}
/*
總計算次數:100*100*100=10^6
<=10^8
*/

下面就是逐步優化程式碼的過程了
減少列舉範圍
公雞個數不會超過100/5=20只
母雞個數不會超過100/3=33只
小雞個數不會超過300只,又因為是買百雞,則不會超過100只

#include<bits/stdc++.h>
using namespace std;
int main()
{
    for(int i=0;i<=20;i++){//公雞的個數
        for(int j=0;j<=33;j++){//母雞的個數
            for(int k=0;k<=100;k++){//小雞的個數,小雞的只數一定是三的倍數
                if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){
                    cout<<i<<' '<<j<<' '<<k<<"\n";
                }
            }
        }
    }
    return 0;
}
/*
總計算次數:20*33*100=60000
<=10^8
*/

減少列舉變數
小雞的個數=100-i-j

#include<bits/stdc++.h>
using namespace std;
int main()
{
    for(int i=0;i<=20;i++){//公雞的個數
        for(int j=0;j<=33;j++){//母雞的個數
            int k=100-i-j;
            if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){
                cout<<i<<' '<<j<<' '<<k<<"\n";
            }
        }
    }
    return 0;
}
/*
總計算次數:20*33=660
<=10^8
*/

進一步減少列舉變數
i+j+k=100
5i+3j+k/3=100
所以
15i+9j+k=300
14i+8j=200即7i+4j=100

#include<bits/stdc++.h>
using namespace std;
int main()
{
    for(int i=0;i<=20;i++){//公雞的個數
        int j=(100-7*i)/4;
        int k=100-i-j;
        if((100-7*i)%4==0 and j>=0 and k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){//判斷條件多了,注意不要少條件
            cout<<i<<' '<<j<<' '<<k<<"\n";
        }
    }
    return 0;
}
/*
總計算次數:20
<=10^8
*/

遞增的倍數增加
由公雞和母雞的只數關係可以知道,公雞的只數一定是四的倍數

#include<bits/stdc++.h>
using namespace std;
int main()
{
    for(int i=0;i<=20;i+=4){//公雞的個數
        int j=(100-7*i)/4;
        int k=100-i-j;
        if((100-7*i)%4==0 and j>=0 and k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){
            cout<<i<<' '<<j<<' '<<k<<"\n";
        }
    }
    return 0;
}
/*
總計算次數:6
<=10^8
*/

例題:This Year‘s Substring

一個字串能否去掉中間某一段,使得餘下的首尾相接後剛好為 2021 。
如:200021 去掉中間的 00 後可以得到 2021
原題連結 這兩個題有些許不同

#include<bits/stdc++.h>
using namespace std;
int main()
{
    string s;
    cin>>s;
    int n=s.length();
    bool ok = false;//列舉去掉的字串的起點和終點
    for(int i=0;i<n;i++){//去掉的起點
        for(iny j=i;j<n;j++){//去掉的終點
            //去掉字串的下標範圍為[i,j]
            string t=s.suber(0,i)+s.suber(j+1);//suber的用法在1.24有提到
            if(t=="2021"){
                ok=true;
            }
        }
    }
    cout<<(ok?"Yes":"No")<<"\n";
    return 0;
}

優化後

#include <bits/stdc++.h>
using namespace std;
int main() {
    string s;
    cin >> s;
    int n = s.length();
    bool ok = false;
    for (int i = 0; i <= 4; i++) { //列舉餘下的首端的長度
        string t = s.substr(0, i) + s.substr(n - (4 - i));
        // s.substr(0, 1) + s.substr(n - 3);
        // 0 1 ... n - 3  n - 2  n - 1
        if (t == "2021") {
            ok = true;
        }
    }
    cout << (ok ? "Yes" : "No") << "\n";
    return 0;
}

/*
    i   j
    0   0 ~ n - 1   n
    1   1 ~ n - 1   n - 1
    ...
    n - 1  n - 1 ~ n - 1   1
    總計算次數:4
*/

例題:校園活動

原題連結
將一個數字字串分為連續的一些組,使得每個組內的數字之和相等,最多可以分為多少組。
如:31113 最多可以分為 3 組:3 、 111 、 3 。
求和:3 +1+1+1+ 3 = 9 ,9 為 3 的倍數
列舉剩下多少組,範圍為:[1, n]

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    string s;
    cin>>s;
    int sum=0;
    for(int i=0;i<n;i++){//對字串進行求和
        sum+=s[i]-'0';
    }
    for(int i=n;i>=2;i--){
        if(sum%i==0){
            bool ok=true;//能否恰好分為i組
            int ave=sum/i;//每組的和
            int cur=0;//當前組的和
            for(int j=0;j<n;j++){
                cur+=s[j]-'0';
                if(cur>ave){
                    ok=false;
                }else if(cur==ave){
                    cur=0;
                }
            }
            if(ok){
                cout<<i<<"\n";
                return 0;
            }
        }
    }
    cout<<-1<<"\n";
    return 0;
}

遞推和遞迴

遞推

通常用於序列計算,序列中的每一項依賴於前面一個或多個項的值。

遞迴

一個函式自己呼叫自己即是遞迴。

兩者的區別
二者對問題的求解方式本質上是一樣的,不同的是求解次序:
從前往後求解是遞推,從後往前求解是遞迴;
由已知到未知是遞推,由未知到已知是遞迴。

例題:階乘

計算20內某個數的階乘

遞推的寫法

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    long long fac[n+1]={};
    fac[0]=fac[1]=1;
    for(int i=2;i<=n;i++)
        fac[i]=fac[i-1]*i;
    cout<<fac[n]<<"\n";
    return 0;
}

遞迴的寫法

#include<bits/stdc++.h>
using namespace std;

long long fac(int n){
    if(n==0)
        return 1;
    return n*fac(n-1);
}

int main(){
    int n;
    cin>>n;
    cout<<fac(n)<<"\n";
    return 0;
}

例題:兔子 兔子

開始時有一對小兔,小兔出生後第三個月起每個月都會再生一對小兔,輸入n,問第n個月有多少對兔子?
第一個月:1對
第二個月:1對
第三個月:2對
第四個月:3對
第五個月:5對
……類似於斐波那切數列
f[n] = f[n-1] + f[n-2]
//第 n 個月的兔子數 = 上一個月的兔子數(f[n-1]) + 新生的兔子數(f[n-2])
f[n] //第 n 個月有多少對兔子

遞推的解法

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    long long f[n+1]{};
    f[1]=f[2]=1;
    for(int i=3;i<=n;i++)
        f[i]=f[i-1]+f[i-2];
    cout<<f[n]<<"\n";
    return 0;
}

這是遞迴的寫法

#include<bits/stdc++.h>
using namespace std;
long long f(int n){
    if(n==1 or n==2)
        return 1;
    return f(n-1)+f(n-2);
}

int main()
{
    int n;
    cin>>n;
    cout<<f(n)<<"\n";
    return 0;
}

例題:碎夢

窗臺上有 n 堆紙飛機,數目分別為 1 , 2 , … , n 。
每次可以選擇一個數 x ,然後從所有紙飛機數目大於等於 x 的堆中各擲出 x 只紙飛機。
問最少要選擇多少次才能將所有紙飛機擲出?
原題連結

遞迴解法

/*
    n = 5
    1 2 3 4 5
    x = 1   0 1 2 3 4
    x = 2   1 0 1 2 3
    x = 3   1 2 0 1 2
    x = 4   1 2 3 0 1
    x = 5   1 2 3 4 0

    n = 6
    1 2 3 4 5 6
    x = 1   0 1 2 3 4 5 
    x = 2   1 0 1 2 3 4
    x = 3   1 2 0 1 2 3
    x = 4   1 2 3 0 1 2
    x = 5   1 2 3 4 0 1
    x = 6   1 2 3 4 5 0
*/

#include <bits/stdc++.h>
using namespace std;

int solve(int n) {
    if (n == 1) {
        return 1;
    }
    if (n % 2 == 1) {
        return 1 + solve((n - 1) / 2);
    } else {
        return 1 + solve(n / 2);
    }
}

int main() {
    cout << solve(5) << "\n";
    return 0;
}

/*
    n = 3   1 + solve(2)
    n = 2   1 + solve(1)
*/

2021.1.26

某大學ACM實驗室寒假新生培訓Day4:演算法基礎二(二分)

引子

給n個數,m次詢問,每次詢問給一個數a,找到n個數中比a小的最大的數,資料保證這樣的數存在。
不會二分的人
!陣列操作 每次詢問的時候掃一遍原陣列 時間複雜度O(n×m).
! 排序離線,只需要把陣列排序,查詢值也排序,就可以掃一遍! 時間複雜度O(nlogn)。
!stl(set)…….

二分講解

我們先將陣列進行排序,對於每次詢問假設答案的下標在k,那麼k之前的所有數必定滿足其小於a。K之後的所有數必定滿足k大於等於a。 我們把滿足 小於a 這個作為條件,如果滿足,就定義其為合法(1),不滿足就定義其為非法(0),那麼序列應該是這個樣子
111……111000……000
我們的目的就是找到最大的1所在的位置。

體現在程式碼上,可以先寫一個check函式

bool check(int id)
{
    if(w[id]<a)return 1;
    return 0;
}

如果n很大的話,通過列舉找最後一個1的過程就很慢,但我們可以通過二分來進行查詢。

void work1()
{
    int ls =1,rs=n+1;
    while(ls+1<rs){
        int mid=(ls+rs)/2;
        if(!check(mid))rs=mid;
        else ls=mid;
    }
    printf("%d\n",w[ls]);
}

簡單來說,維護一個區間使答案必定在這個區間內,然後繼續維護,使得左端點必定合法(check為1),右端點必定不合法(check為0)然後不斷通過取左右端點的中點的方式來每次將這個區間縮短一半,這樣當ls+1=rs的時候,左端點就是答案。
其實就是一個左閉右開的區間。
簡單來說,如果我們已經實現了check函式,那麼無非是下列兩種情況
11111110000000 讓你求最大的1的位置
00000001111111 讓你求最小的1的位置
第一個就是上述方法(左閉右開)
第二個只需要維護ls永遠非法,rs永遠合法即可(左開右閉)。
然而二分的難點其實在於check函式的實現,即怎樣判斷當前位置的數是否滿足題目要求。
這個大家做題的時候應該會有所感悟。

例題:跳石頭

數軸上有n個石子,第i個石頭的座標為Di,現在要從0跳到L,每次條都從一個石子跳到相鄰的下一個石子。現在FJ允許你最多移走M個石子,問移走這M個石子後,相鄰兩個石子距離的最小值最大是多少。
(N<=50000,L<=1e9)
題解
據說“最小值最大”和“最大值最小”是基本可以判斷二分法的
如果一個距離滿足題意,則更小的距離肯定滿足,所以答案滿足單調性,考慮二分答案。
那麼我們在跳躍距離[0,L]之間二分列舉一個最小跳躍距離作為答案mid進行判斷,看是否符合最多移走m個的限制,如果符合,那麼mid可能就是答案,但也可能還存在更大的答案,所以要向右二分查詢。如果不行,那肯定大了,向左二分查詢。
判定直接O(N)的貪心即可
總時間複雜度為NlogL
在網上找到的題解連結

例題:借教室

N天內有m份借教室的訂單,學校第i天有ri個教室可借,第i每份訂單顯示從第si~ti天需要借di個教室,請你按照訂單先後順序分配教室,如果能夠完成分配輸出0,否則輸出第一個無法滿足的訂單編號。
1≤n,m≤1e6,0≤ri,dis≤1e9
題解
首先考慮暴力方法:列舉每個訂單,對訂單內的每天更新剩餘教室數量,如果出現負數,即表示該訂單不可行。N×M
對於會線段樹的人來說這就是一道送分題,但不幸的是只能90分。
而且線段樹在12年的時候屬於高階演算法,沒幾個人會
且發現訂單滿足單調性,可以二分,判定可以利用訂單的區間更新特性,求出每天需要教室數的差分陣列,求字首和即可判定出教室是否夠用。MlogN。

例題:尋找段落

給定一個長度為n的序列a_i,定義a[i]為第i個元素的價值。現在需要找出序列中最有價值的段落。段落的定義是長度必須大於s的連續序列。價值=段落總價值/段落長度。
n<=100000,1<=S<=T<=n,-10000<=價值指數<=10000。
題解
考慮二分一個平均值mid,我們將a全部減去mid,問題轉化為判斷是否存在一個長度在s~t範圍內的區間,它的和為正,如果有說明還有更大的平均值。
即求sum[i]-min(sum[i-s]…sum[0])是否大於0

2021.1.27

某大學ACM實驗室寒假新生培訓Day5:數學入門

前置知識

先定義一些符號

⌊x⌋表示x向下取整
⌈x⌉表示x向上取整
[esp]其中exp代表一個表示式,如果表示式為真,值為1,如果表示式為假,值為0
gcd(x,y)表示x與y的最大公約數
lcm(x,y)表示x與y的最小公倍數
d|n表示d整除n,n是d的倍數
a ≡ b(mod n)表示a,b在模n的意義下同餘

快速冪

求a^b mod m的值,且b<=10^18

求a1,a2,a4,a8,a^2n
把b進行二進位制分解
若b=5,則b=101B,a5=a1 × a^4

//b=5
int quick_pow(int a,int b){
    int ret=1;
    while(b){//滿足b不為0
        if(b&1)ret=ret*a%mod;//a^1 ret*=a^1
        b>>=1;
        a=a*a%mod;//之後a^2  a^4 ret*=a^4,之後b就是0了,a^8,所以是a^1+a^4
    }
    return ret;
}

a+b %m=(a%m+b%m)%m
a×b %m=(a%m×b%m)%m
a^b %m=a%m^(b%m)

pow進行的是浮點運算

歐幾里得演算法

歐幾里得演算法,就是求gcd(x,y)。在輾轉相除法上進行改進。
假設gcd(x,y)=d,且d|x,d|y等價於d|(x-y),d|x。所以gcd(x,y)=gcd(x-y,y)=gcd(x-2×y,x)=gcd(x-k×y,x)=gcd(y,x%y)。顯然每次取模後數字的大小至少折半,所以複雜度是低於O(log max(x,y))級別的
gcd(x,0)=x

int gcd(int x,int y){
	if(!y)return x;//函式的終止條件是y=0
    return gcd(x%y,x);
}

關於取整的經典題目

在這裡插入圖片描述

即求:n/1+n/2+n/3+……+n/n

//超時程式碼
for(int i=1;i<=n;++i)ans+=n/i;

n/i (i<=n)取整,最多一共有根號n種取值
sqrt(n)<n
情況一:當i<=sqer(n)
n/1,n/2,n/3,n/4……n/sqrt(n)(最多有sqrt(n)種)
情況二:當i>sqrt(n)
可得i/sqrt(n)<sqrt(n) i/aqrt(n),i/(sqrt(n)+1)……i/n
n/i<sqrt(n) n<sqrt(n)×i sqrt
n/sqrt(n) n/sqrt(n)+1 n/nsqrt[1,sqrt(n)]

則總共有sqrt(n)種
每種取值的i一定是一個連續的區間

/l和r是左區間端點右區間端點
for(int l=1,r=1;l<=n;l=r+1){
    r=n/(n/l);
    ans+=n/l*(r-l+1);
}

如何找左區間端點,右區間端點?
左區間端點=上一個區間右區間端點+1
著重於找右區間端點
首先求出n/l=1 ,再次,x=n/l找r使n>=x×r,r=n/x向下取整

取整與取模的轉換

在這裡插入圖片描述

cin>>n;
long long ans = 0;
for(int l=1,r=1;l<=n;l=r+1){
    r=n/(n/l);
    ans+=(r-l+1)*(r+l)/2*n/l*(r-l+1);//l,l+1,l+2……r
}
cout<<n<<endl;

2021.1.28

某大學ACM實驗室寒假新生培訓Day6:資料結構入門

資料結構是組織資料的結構。用來方便我們的操作管理資料。陣列是一種簡單的資料結構(順序儲存結構的線性表)。

生活中有大量遵循“後進先出”規則的結構,我們將之抽象為
棧(stack)支援如下操作:查詢棧頂,彈出棧頂,向頂部壓入元素
模擬盤子、模擬電梯,這兩種任務只需要用棧就能完成。

//定義棧
#include<bits/stdc++.h>
using namespace std;
struct node{
    int x,y;
};
strack<node>a;
int main()
{
    
    return 0;
}

理解的例題:洗盤子

大清洗期間,R 君每天要洗盤子。盤子總是堆成一疊,已知 R 君總是取出最頂端的盤子,拿去洗。
其他人總是把需要清洗的髒盤子,放在最頂端。
給所有盤子編號。現在考慮以下操作:放進1,放進2,清洗, 放進3,放進4,清洗,清洗,放進5,清洗,清洗。
問:遭到清洗的盤子順序。
2 4 3 5 1 ,就是簡單的棧的問題

頂端操作

永遠在頂端進行操作,在頂端加入,在頂端彈出,它的特性就是:後進先出

例題:電梯問題

盧比揚卡包吃包住,所以那裡的電梯經常人滿為患。
依據常識,當超載鈴響時,最晚進來的人應該退出。如果要模擬 這臺電梯(進入人、退出人),必然也會滿足後進先出規則。
因此,擠電梯和洗盤子遵循同一套規則,洗盤子的模擬程式也能 用來模擬盧比揚卡的電梯。
程式碼實現
起一個數組a,用來直接存放棧裡面的每一個元素。記錄tot(棧 裡面的元素個數)。
查詢棧頂:返回a[tot]
彈出棧頂:tot–
向頂部壓入元素:a[++tot] = x

#include<stack>
using namespace std;

void caic()
{
    stack<int>s;//定義一個棧,儲存int型
    for(int i=1;i<=s;i++)
    	s.push(i);//壓入棧
    while(!s.empty())//當站非空時
    {
        printf("%d\n",s.top());//查詢棧頂
        s.pop();//彈出
    }
}

STL 的棧支援其他各種資料型別、支援使用者自己的struct.
空間佔用是動態的;比起自己手寫的棧稍慢。

例題:平衡的括號

原題連結
思路
維護一個棧,初始為空,從左到右掃描括號序列:

  • 若為左括號,則壓入棧
  • 若為右括號,則判斷棧頂是否與之成對。如果成對,則彈出棧頂,否則報告錯誤。
    掃描完成後,若棧中還剩下元素,則報告錯誤,否則報告序列合法
#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;
int n,tot;
char s[maxn],a[maxn];
int main()
{
    scanf("%d",&n);
    while(n--){
        tot=0;
        scanf("%s",s+1);
        int le=strlen(s+1),ans=1;
        for(int i=1;i<=len;i++){
            if(s[i]=='('||s[i]=='['){
                ++tot;
                a[tot]=s[i];
            }
            if(s[i]==')'){
                if(a[tot]=='['||tot==0)ans=0;
                else
                	tot--;
            }
            if(s[i]==']'){
                if(a[tot]=='('||tot==0)ans=0;
                else
                	tot--;
            }
        }
        if(ans)printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

vj例題:A-Rails(stack)

原題連結
需要模擬一個棧,試圖讓它的出棧順序符合題目所給。
維護一個棧,初始為空。以變數tot 記錄“入過棧的最大數”。 依次考慮目標序列中的每一個元素x:
1 若x 小於tot。
如果棧頂不為x,則報告非法。否則,彈出棧頂。
2 若x 大於等於tot,則把[tot, x] 內的所有數依次壓進棧,再 彈出棧頂,tot 改為x。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int k,n,a[1005];
int p[1005],t;
bool ok;
int main(){
	while(cin>>n&&n!=0){
		abc://abc在這!
		while(true){
			memset(a,0,sizeof(a));
			memset(p,0,sizeof(p));//因為有多組資料,所以每次清零
			t=k=0;//防止影響下一組資料
			ok=false;
			cin>>a[1];
			if(a[1]==0)break;
			for(int i=2;i<=n;i++)
				scanf("%d", &a[i]);
			for(int i=1;i<=n;i++){
				while(k==0||k!=0&&p[k]!=a[i]&&t<n)
				p[++k]=++t;
				if(p[k]!=a[i]){
					cout<<"No"<<endl;
					goto abc;//跳轉到abc位置
				}
				else k--;
			}
			cout<<"Yes"<<endl;//注意大小寫
		}
		cout<<endl;//每組資料換行
	}
	return 0;
}

例題:排隊

今年有一堆人排隊槍斃。每個人想知道站自己前面的、比自己高的人中,離自己近的是誰。
例:
排隊者身高為[3,1,2,5,4,7,6]
則答案為[0,1,1,0,4,0,6]

維護一個棧,儲存“觀測者現在能看到的人”。
從左到右掃描佇列。對每個元素x 執行如下操作:
1 彈棧,直到棧尾比自己高。
2 將棧尾作為自己的答案。若棧空,則自己的答案為0.
來模擬一波樣例:[3,1,2,5,4,7,6]

單調棧

由於上文中的棧永遠單調遞減,故稱為“單調棧”。
複雜度O(n),因為每個元素頂多入棧一次、出棧一次。
模板題

#include<bits/stdc++.h>
#define maxn 3000010
using namespace std;
int n,z[maxn],a[maxn],b[maxn],tot,ans[maxn];
//z表示n個元素的值,a表示第一個棧是棧的元素的值,b表示第二個棧對應a的第i個值的位置
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&z[i]);
    for(int i=n;i>=1;i--){
        while(tot>0&&a[tot]<=z[i])tot--;//維護單調棧,棧頂元素要大於現在加入的值,棧元素個數大於零
        ans[i]=b[tot];//更新答案
        tot++;a[tot]=z[i];b[tot]=i;
    }
    for(int i=1;i<=n;i++)printf("%d ",ans[i]);
    printf("\n");
    return 0;
}

佇列

今有一群人排隊槍斃,每次加入人都是在隊尾,而每次取出人都是在隊首。
收銀臺的佇列、食堂恰飯的佇列……無不如此。我們把這種隊伍 抽象為資料結構“佇列”。
佇列是符合先入先出的結構。越早進入佇列的,越早排完隊出去。 對於所有的a,b 均有:若a 比b 早進入佇列,則a 比b 早離開 佇列。
佇列提供如下操作: 查詢隊首、彈出隊首、向尾部壓入元素
程式碼實現
搞一個數組來儲存元素。用fnt, end 分別記錄頭指標、尾指標。

int q[10005];
int fnt=1,end;
#define front (q[fnt])
#define pop   (fnt++)
#define push(x) (q[++end]=(x))

但是,q[fnt] 之前的所有空間都被浪費了!

#include<bits/stdc++.h>
#define maxn 3000010
using namespace std;
int q[maxn],l=1,r;
int main()
{
    cout<<q[l];//查詢隊首元素
    l++;//讓隊首出站
    q[++r]=x;//在隊尾加入一個x
    return 0;
}

迴圈佇列是指:在end 超出陣列大小之後,把資料溢位到陣列的頭部

#define SIZE 100000;
int q[SIZE+5];
int fnt=1,end;

注意要保證SIZE一定比佇列最長長度大!
STL 提供了佇列,需要包含queue 庫。
#include<queue>//c++
用法與stack 類似,但隊首用front() 訪問。

#include<queue>
using namespace std;
void caic()
{
    queue<int> s;//定義一個佇列,儲存int型
    for(int i=1;i<=5;i++)
    	s.push(i);//壓入佇列
    while(!s.empty())//當佇列非空時
    {
        printf("%d\n",s.front())//隊首
        s.pop();//彈出
    }
}

顯然,佇列的三個操作——front, push, pop 時間複雜度全都是 O(1) 的。
STL 佇列為動態空間。手寫樸素佇列空間會浪費大量空間(不停 地入隊、出隊);迴圈佇列節省空間,但如果空間估算不足,會造成資料錯誤。

例題:約瑟夫問題

原題連結
起一個佇列,初始為[1,2,3…n].
每輪迴圈:
1 隊首出隊,然後重新入隊。執行m-1 次。 2 隊首出隊輸出。
直到佇列空為止。

#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;
int d[maxn],l=1;r,n,m;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)d[++r]=i;
    for(int i=1;i<=n;i++){
        if(m!=1)for(int j=1;j<m-1;j++){
            d[++r]=d[l];
            l++;
        }
        if(i!=n)printf("%d ",d[i]);
        else printf("%d\n",d[l]);
        l++;
    }
    return 0;
}

例題:求細胞數量

原題連結
掃描每一個點。我們可以通過如下方式走遍所在細胞中的每一個點:
先把這個點加入佇列。當佇列非空:
1.取出隊首
2.標記隊首四周沒有走過的的細胞點,並把它們全都加進佇列
佇列空了之後,我們就標記完了這個細胞中所有的點。
上述手段稱為bfs(寬度優先搜尋)

單調佇列

單調佇列,即單調遞減或單調遞增的佇列。使用頻率不高,但在有些程式中會有非同尋常的作用。
模板題

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int all, k;
struct node { int n, m; }a[maxn];
deque<node> q;
void BIG(int all, int k) {
	for (int i = 1; i <= all; ++i) {
		while (!q.empty() && a[i].m >= q.front().m )q.pop_front(); // 求最小的
		q.push_front(a[i]);
		while (q.back().n <= i - k) q.pop_back();
		if (i >= k) printf("%d ", q.back().m);
	}
	q.clear();
}
void LOW(int all, int k) {
	for (int i = 1; i <= all; ++i) {
		while (!q.empty() && a[i].m <= q.front().m) q.pop_front(); // 求最大的
		q.push_front(a[i]);
		while (q.back().n <= i - k) q.pop_back();
		if (i >= k) printf("%d ", q.back().m);
	}
	q.clear();
}
int main() {
	scanf("%d %d", &all, &k);
	for (int i = 1; i <= all; ++i) scanf("%d", &a[i].m), a[i].n = i;
	LOW(all, k);
	puts("");
	BIG(all, k);
	return 0;
}

並查集

剛剛實現的資料結構就是並查集(union-find)。並查集是用來維護不相交集合的資料結構,支援兩個操作:
ask(x, y) 查詢x,y 是否在同一個集合。
union(x, y) 將x,y 所在的集合合併。
帶路徑壓縮的並查集,單次操作平均複雜度接近 O(1)

例題:家族問題

相關連結
今有n 個人,初始時互相沒有關係。要支援多次操作:
1,查詢x,y 是不是親戚。
2,讓x,y互相成為親戚
一開始把每一個人的祖先設定為0
查詢操作:
查詢x,y 是不是親戚時,找到x,y 各自的祖先。 如果相同,則返回true;如果不同,則返回false.
認親操作:
如果x,y 已經在同一家族,則忽略。否則,將x 的祖先設為y

int anc(int x)//找到x的祖先
{
    if(dad[x])return anc(dad[x]);
    return x;
}
void ask(int x,int y)//查詢是否為親戚
{
    return anc(x)==anc(y);
}
void uni(int x,int y)//連線想,y
{
    x=anc(x),y=anc(y);
    if(x!=y) dad[x]=y;
}

容易發現,複雜度的瓶頸在於“找到某人的祖先”。最壞情況下, 1 是2 的父親,2 是3 的父親……子子孫孫無窮匱也,最後每次 找祖先要付出O(n) 的複雜度。
有辦法加速嗎?提示:我們自始至終只關心“x 的祖先是誰”,不 關心“x 的父親是誰”。

路徑壓縮優化:確定了x 的祖先之後,直接把dad[x] 設為他的 祖先。正確性顯然。

int anc(int x)//找到x的祖先
{
    if(dad[x])
    	return dad[x]=anc(dad[x]);
    return x;
}
#include <bits/stdc++.h>
#define maxn 20010
using namespace std;
int fa[maxn],n,m,q,a1,a2;
int find(int p){
    if(fa[p])return fa[p]=find(fa[p]);
    return 0;
}
int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&a1,&a2);
        int f1=find(a1),f2=find(a2);
        if(f1!=f2)fa[f2]=f1;
    }
    for(int i=1;i<=q;i++){
        scanf("%d%d",&a1,&a2);
        int f1=find(a1),f2=find(a2);
        if(f1==f2)printf("Yes\n");
        else peintf("No\n");
    }
    return 0;
}

模板題

模板題連線

例題:修復公路

原題連結

2021.1.29

某大學ACM實驗室寒假新生培訓Day7:動態規劃入門

01揹包

問題描述:有N種物品和一個容量是V的揹包。每件物品只能使用一次。
第i件物品的體積是v[i],價值是w[i]。
求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。
輸出最大價值。

先從數學的角度抽象理解:
設f(i,j)為只考慮前i個物品的前提下,每個物品只能拿一次,最多用j容量能獲得的最大價值。
顯然,f(0,0),f(0,1),f(0,2)…f(0,m)的值為0。
假設已知f(i-1,0),f(i-1,1)…f(i-1,V),考慮如何求出f(i,j)(0≤j≤V)。
f(i,j)和f(i-1,j)的區別是f(i,j)還可以考慮第i個物品。
也就是說,f(i,j)只會根據取與不取第i個物品從之前的狀態轉移過來:
①不拿第i個物品最優:f(i-1,j)
②拿第i個物品下最優:f(i-1,j-w[i])+v[i](需要保證j>=w[i])
綜上所述,f(i,j)=max(f(i-1,j-w[i])+v[i],f(i-1,j))
那麼,以f(0,0),f(0,1),f(0,2)…f(0,V)為基礎,我們可以依次求出f(i,j)(1≤i≤N,0≤j≤V)。
f(N,V)就是我們要求的答案。

考慮用一個二維陣列f記錄f(i,j)
程式碼如下:
時間複雜度O(N×V),空間複雜度O(N×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i];
    for(int i=1;i<=N;++i){
        for(int j=0;j<=V;++j){
            f[i][j]=f[i-1][j];
            if(j>=v[i])f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<f[N][V]<<endl;
    return 0;
}

狀態表示:
f[i][j]表示只考慮前i個物品的前提下,每個物品只能拿一次,最多用j容量能獲得的最大價值。

狀態轉移方程:
f[i][j]=max( f[i-1][j],f[i-1][j-w[i]]+v[i] )

完全揹包

問題描述:
有N種物品和一個容量是V的揹包。每種物品都有無限件可用。
第i件物品的體積是v[i],價值是w[i]。
求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。
輸出最大價值。
回顧01揹包的狀態表示和狀態轉移方程,考慮完全揹包。

01揹包:
狀態表示:
f[i][j]表示只考慮前i個物品的前提下,每個物品只能拿一次,最多用j容量能獲得的最大價值。

狀態轉移方程:
f[i][j]=max( f[i-1][j],f[i-1][j-w[i]]+v[i] )
					(j>=w[i])
完全揹包:
狀態表示:
f[i][j]表示只考慮前i個物品的前提下,每個物品能拿任意次,最多用j容量能獲得的最大價值。

狀態轉移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )
				(j>=w[i])
把i-1改成i就可以多次使用第i個物品了。

舉個例子:
狀態表示:
f[i][j]表示只考慮前i個物品的前提下,每個物品能拿任意次,最多用j容量能獲得的最大價值。
N=2(物品數),V=5(揹包容量)

狀態轉移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )
			(j>=w[i])

f[0][1],f[0][2],f[0][3],f[0][4],f[0][5]=0
f[1][0]=0,S=∅
f[1][1]=max(f[0][1],f[1][0]+1)=1,S={1}
f[1][2]=max(f[0][2],f[1][1]+1)=2,S={1,1}
f[1][3]=max(f[0][3],f[1][2]+1)=3,S={1,1,1}
f[1][4]=max(f[0][4],f[1][3]+1)=4,S={1,1,1,1}
f[1][5]=max(f[0][5],f[1][4]+1)=5,S={1,1,1,1,1}
f[2][0]=f[1][0]=0,S=∅
f[2][1]=f[1][1]=1,S={1}
f[2][2]=max(f[1][2],f[2][0]+3)=3,S={2}
f[2][3]=max(f[1][3],f[2][1]+3)=4,S={1,2}
f[2][4]=max(f[1][4],f[2][2]+3)=6,S={2,2}
f[2][5]=max(f[1][5],f[2][3]+3)=7,S={1,2,2}

考慮用一個二維陣列記錄f(i,j)
程式碼如下:
時間複雜度O(N×V),空間複雜度O(N×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i];
    for(int i=1;i<=N;++i){
        for(int j=0;j<=V;++j){
            f[i][j]=f[i-1][j];
            if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    }
    cout<<f[N][V]<<endl;
    return 0;
}

狀態表示:
f[i][j]表示只考慮前i個物品的前提下,每個物品能拿任意次,最多用j容量能獲得的最大價值。

狀態轉移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )

優化

01揹包優化
時間複雜度O(N×V),空間複雜度O(V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i];
    for(int i=1;i<=N;++i){
        for(int j=V;j>=v[i];--j){
            f[j]=max(f[j],f[j-v[i]+w[i]]);
        }
    }
    cout<<[V]<<endl;
    return 0;
}

完全揹包:
時間複雜度O(N×V),空間複雜度O(V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i];
    for(int i=1;i<=N;++i){
        for(int j=v[i];j<=V;++j){
            f[j]=max(f[j],f[j-v[i]+w[i]]);
        }
    }
    cout<<[V]<<endl;
    return 0;
}

多重揹包

問題描述:
有 N 種物品和一個容量是 V 的揹包。
第 i 種物品最多有 si 件,每件體積是 vi,價值是 wi。
求解將哪些物品裝入揹包,可使物品體積總和不超過揹包容量,且價值總和最大。
輸出最大價值。

solution1:

對於每種物品i,將它的si件單獨拆開考慮,那麼就變成了01揹包。時間複雜度為O(Σ(s[i])×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[105];int w[105];;int s[105];
int f[105];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=N;++i){
        for(int ith=1;ith<=s[i];++ith){
            for(int j=V;j>=v[i];--j){
            f[j]=max(f[j],f[j-v[i]+w[i]]);
        	}
        }
    }
    cout<<[V]<<endl;
    return 0;
}

一個個拆開太笨,如果對於一種物品s[i]為一億,那我們就要拆開成一億份,DP一億次,成本太大。
是否有更好的拆法,使得拆出來的份數少一點?且它們可以且僅可以組合成0~s[i]間的任意數呢?
可以用二進位制拆分的方法,依次按照1,2,4,8,…來拆分,直到不足時剩下的來當最後一份。

舉個例子:
拆分8(1000b):
第一次拆一份大小為20的,剩餘7(111b)
第二次拆一份大小為21的,剩餘5(101b)
第三次拆一份大小為22的,剩餘1(1b)
第四次由於1<23而結束演算法,直接將剩下的1個為1份。

證明二進位制拆法的正確性:
回顧問題:是否有更好的拆法,使得拆出來的份數少一點?且它們可以且僅可以組合成0~s[i]間的任意數呢?
根據演算法可知,我們最終拆分出來的大小分別為20,21,…2k,x。
證明僅可以:20+21+…+2k+x=s[i],不管怎麼組合都不會超出s[i],能組合出來的數必然在0~s[i]之間。
證明可以:
①我們可以知道x<2k+1,也就是說x≤20+21+…+2k。
②證明可以組成0~x的任意數:
由二進位制的性質,我們可知20,21,…2k可以組合成0~20+21+…+2k任意數。
又因為x≤20+21+…+2k。所以我們可以組成0~x任意數。
③證明可以組成x+1~s[i]的任意數:
先固定一個x,也就是說我們只需要用20,21,…2k組成1~s[i]-x的任意數。
因為20+21+…+2k=s[i]-x且20,21,…2k可以組合成0~20+21+…+2k任意數。
所以我們可以組成x+1~s[i]的任意數

solution2:

按照二進位制優化的方法拆分。
時間複雜度為O(Σ(log2(s[i]))×V)

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int N;int V;
int v[1005];int w[1005];;int s[1005];
int f[2005];
int main()
{
    cin>>N>>V;
    for(int i=1;i<=N;++i)cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=N;++i){
        for(int num=1;s[i];num<<=1){
        	num=min(num,s[i]);
            for(int j=V;j>=num*v[i];--j){
            f[j]=max(f[j],f[j-num*v[i]+num*w[i]]);
        	}
        	s[i]-=num;
        }
    }
    cout<<[V]<<endl;
    return 0;
}

2021.1.30-2021.1.31

某大學ACM實驗室寒假新生培訓Day6:資料結構入門(補充)

堆(Heap)是電腦科學中一類特殊的資料結構的統稱。堆通常是一個可以被看做一棵完全二叉樹的陣列物件。

  • 堆中某個節點的值總是不大於或不小於其父節點的值;
  • 堆總是一棵完全二叉樹。

二叉樹

二叉樹(Binary tree)是樹形結構的一個重要型別。許多實際問題抽象出來的資料結構往往是二叉樹形式,即使是一般的樹也能簡單地轉換為二叉樹,而且二叉樹的儲存結構及其演算法都較為簡單,因此二叉樹顯得特別重要。
二叉樹特點是每個結點最多隻能有兩棵子樹,且有左右之分 。
陣列儲存二叉樹
將1設定為樹的根
節點標號i的父親標號為i/2(向下取整)
節點標號i的左兒子標號為i×2
節點標號i的右兒子標號為i×2+1

堆的操作

push:往堆中加入一個元素;
top:從堆中取出堆頂元素;
pop:從堆中刪除堆頂元素;

push

(1)在堆尾加入一個元素,並把這個結點置為當前結點。
(2)比較當前結點和它父結點的大小,
如果當前結點小於父結點,則交換它們的值,並把父結點置為當前結點。轉(2)。
如果當前結點大於等於父結點,則轉(3)。
(3)結束。

top

直接查詢即可

pop

(1)取出堆的根結點的值。
(2)把堆的最後一個結點(len)放到根的位置上,把根覆蓋掉。把堆的長度減一。
(3)把根結點置為當前父結點pa。
(4)如果pa無兒子(pa>len/2),則轉(6);否則,把pa的兩(或一) 個兒子中值最小的那個置為當前的子結點son。
(5)比較pa與son的值,
如果pa的值小於或等於son,則轉(6);
否則,交換這兩個結點的值,把pa指向son,轉(4)。
(6)結束。

模板和程式碼

網上有很多關於堆的模板,由於STL中有priority_queue
模板連結
在資訊學一系列競賽中,由於堆比較難打(其實也沒多難),一般都用pririty_queue來代替,但這並不代表學習關於堆的基礎知識沒用!