程式設計實踐考試的入門模板

mMingfunnyTree發表於2018-11-23

這個部落格不再更新,新部落格地址請戳

程式設計實踐考試的入門模板

前言

其實從大二開始就在整理有關如何學習C語言以及如何應對程式設計實踐(和C語言考試)的經驗和相關模板,由於各種原因,這件事情也沒有一個很好的進展。前不久鄒大佬提起這事兒的時候,突然覺得是應該好好整理一份類似於參考資料的東西了。

我打算先由自己整理出來這份模板,主要面向應對程式設計實踐考試的同學。
本文當中可能會存在一些錯誤和遺漏的東西,還請指正。(email 1278683056@qq.com

使用這份模板之前,你需要學會最基本的C語言(C++)語法,所以關於語法部分如果還不是很熟悉,這份模板對你而言沒有任何幫助。

在信工院程設掛科率奇高的大環境下,我覺得整理出一份適合於入門者使用的模板很有必要,希望能夠幫助到大家。


第一章 關於程式設計入門

- 1.online judge

oj指的是線上評測系統,程式設計實踐考試在oj上進行,所以首先我們需要對oj有一個大致的瞭解。

1.1 根據測試,xtuoj 1秒鐘大約能夠執行3e7次,這一點在避免得到TLE很重要,學會計算時間複雜度和空間複雜度是資料結構課程的內容,在此不贅述。

1.2 介紹幾種常見錯誤的原因,以便於對症下藥。

型別 原因 解決方案
WA(答案錯誤) 程式輸出跟標程輸出不一致,演算法設計上有錯誤,或存在邏輯錯誤 改進演算法,檢查邏輯問題
TLE(超時) 程式未能在限定時間內結束,演算法複雜度過高,或存在死迴圈 檢查是否存在死迴圈,判斷演算法時間複雜度是否可行,如果確認複雜度可行,有可能是被卡常
RE(執行錯誤) 除0,棧溢位,記憶體訪問越界等 ①檢查除法運算的地方是不是除0了 ②如果使用了遞迴演算法,判斷是不是爆棧了 ③ 下標超過範圍,陣列開小,會訪問越界
MLE(記憶體超限) 申請的記憶體超過了題目限制範圍,一般是陣列開大了,也可能是因為在死迴圈裡不停地申請記憶體 改進演算法,計算空間複雜度
PE(格式錯誤) 答案對了,但是輸出格式有問題 仔細檢查換行,空格等問題,距離AC很接近了

在此解釋一下何為卡常
卡常指的是,程式演算法本身複雜度符合題目要求,按理說是能夠AC的,但可能由於自己程式碼寫了很多不必要的東西,導致超時。當然,不排除會有出題人故意卡常。解決方法是儘量避免不必要的額外運算,另外,在輸入輸出上能通過使用外掛從而加速執行。外掛會在接下來的模板中給大家貼出。

何為爆棧:
遞迴層數太多,導致棧溢位。(這類似於死迴圈,但是程式還沒超時就因為爆棧而終止執行了。)如果確實是因為層數太多,也可以手動模擬棧(stack),或者改為佇列(queue)。

- 2.分析題型

程設考試一般6題,對於絕大多數人而言,通過2題意味著考試及格,當然也有少部分人可以1題及格。

一:暴力,所謂的簽到題
二:執行
三:貪心
四:模擬
五:資料結構
六:圖論
七:動態規劃
八:數學相關

對於以上題型,一到四項沒有什麼很好的模板可供參考,更多的是平時的積累和練習,然而在考試時這些題相對後面的題型來說,屬於簡單題;針對五到八項,接下來我會整理出一些適合的模板。


第二章 數學相關

- 1 素數相關

1.1單個數n的判定,時間複雜度O(sqrt(N))

bool isprime(int n){
	if(n<2)return 0;
	if(n<4)return 1;
	for(int i=2;i*i<=n;i++){
		if(n%i==0)return 0;
	}
	return 1;
}

解釋:
素數的因子只有1和它本身,那麼如果從1到sqrt(n)都沒有數字是n的因子,那麼n一定是質數。
可以發現,一個數的所有因子,一定均等地分佈在sqrt(n)的左右兩邊。
比如數字9的因子{1,3,9},左邊是{1,3},右邊是{3,9}。

1.2素數表,時間複雜度O(N)

const int maxn = 1e5+10;
bool notprime[maxn];
void getprime(){
	notprime[0]=notprime[1]=1;
	for(int i=2;i<maxn;i++){
		if(notprime[i]==0){
			for(long long j=1LL*i*i;j<maxn;j+=i){
				notprime[j]=1;	
			}
		}
	}
}

解釋:
notprime[i]==1表示i不是素數,反之表示i是素數。
對於一個素數a,它的倍數一定都不是素數,所以我們可以對於遇見的每個素數,都把它的倍數標記為非素數,以上程式碼就是實現這一過程的。
由於i*i可能會溢位,為了避免溢位,j使用long long型。j從i^2開始,因為小於i倍的部分都已經被修改過了,不需要重複修改。

1.3 合數分解(值域為int的)
把一個合數a分解為 a = 1 * p1^x1 * p2^x2 * … *pn^xn 的形式

const int maxn = 1e5;
int p[100],x[100];
void getheshu(int n){
	int cnt=0;
	for(int i=2;i<maxn&&n>1;i++){
		if(n%i==0){
			p[++cnt]=i;
			while(n%i==0){
				n/=i;
				x[cnt]++;
			}
		}
	}
	if(n>1){
		p[++cnt]=n;
		x[cnt]=1;
	}
}

解釋:
呼叫這個函式後,n的分解結果儲存在p陣列和x陣列中,表達形式如上述。
如果能夠分解出一個質數p,那麼迴圈分解出的p的最高次冪。
最後剩下的“尾巴”如果大於1,說明這個數字一定是個質數。

- 2 最大公約數

gcd和lcm
最大公約數主要用到的是輾轉相除法

int gcd(int a,int b){
	int c;
	while(b){
		c=a;
		a=b;
		b=t%b;
	}return a;
}

當然我們可以直接使用庫函式__gcd(,)它是內部已經實現好了的函式,所以可以省去上面的程式碼,請注意該函式前面有兩條下劃線。

至於a和b的最小公倍數,等於a*b/gcd(a,b)
我們可以實現函式:

int lcm(int a,int b){
	return 1LL*a*b/gcd(a,b);//避免32位整型溢位
}

- 3 組合數

3.1組合數打表

對於較小的組合數,我們一般採用打表的方式儲存答案,主要有以下兩種方法:

int dp[30][30];
for(int i=0;i<30;i++){
	dp[i][0]=dp[0][i]=1;
}
for(int i=1;i<30;i++){
	for(int j=1;j<30;j++){
		dp[i][j]=dp[i-1][j]+dp[i][j-1];
	}
}
/**解釋:dp[i][j]表示從i+j個物品中選擇i個物品,不選擇j個物品,
那麼它可以由dp[i-1][j]和dp[i][j-1]轉移得到,滿足加法定理。
C(n,k)對應dp[n-k][k]
**/

第一種方法是我喜歡的寫法,不過以下第二種方法可能更加方便。

int dp[30][30];
for(int i=0;i<30;i++){
	c[i][0]=c[i][i]=1;
	for(int j=1;j<i;j++){
		dp[i][j]=dp[i-1][j]+dp[i-1][j-1];
	}
}
/**解釋:這種寫法的C(n,k)對應的值是dp[n][k]
**/

以上打表的演算法,時間複雜度都是O(n^2)的,所以當複雜度過高時,請使用盧卡斯定理。
另外,根據資料範圍調整32位整型和64位整型,如果要求取模,記得每次運算都要取模。

下面我們介紹盧卡斯定理。

3.2 盧卡斯定理

具體原理可以自行百度學習,這裡還牽涉到了乘法逆元的知識點,初學者不妨把它當作黑箱子來使用。

typedef long long ll;
const int maxn = 1e5+10;
const int mod  = 1e9+7;

ll qpow(ll a,ll n){
    ll ret=1;
    while(n){
        if(n&1)ret=ret*a%mod;
        a=a*a%mod;
        n>>=1;
    }
    return ret;
}

//除以一個數x,等同於乘以x的逆元,x的逆元 = x^(mod-2)%mod
//所以有p/q%mod = p*qpow(q,mod-2)%mod成立

ll fac[maxn];

void init(){
    fac[0]=1;
    for(int i=1;i<maxn;i++){
        fac[i]=fac[i-1]*i%mod;
    }
}

//C(n,m) = n!/(m!*(n-m)!)

ll C(ll n,ll m){
    if(n<m)return 0;
    return fac[n]*qpow(fac[m],mod-2)%mod*qpow(fac[n-m],mod-2)%mod;
}

小結:至於其他的數學知識,暫時還沒有怎麼見過,主要還是素數篩法考來考去。


第二章 資料結構

  • 1 排序
    在資料結構裡,我們學習了很多種各有特色的排序演算法,但在這裡我只介紹一種最方便的排序工具:STL裡的sort()函式
    什麼是STL?這個問題在我另一篇很久沒更新的部落格上可以找到答案:點我,我是傳送門
    上面的連結裡有很多可用的STL介紹,在此不贅述。
    講到排序,不妨再深入學習一下其他的STL工具。
    這裡列舉可以深入瞭解的STL工具:
    stack
    queue
    vector
    map
    set
    以及一大堆好用的函式

  • 2 雙指標尺取法
    之所以把這個東西單獨拿出來講,是因為eric多次出過這種型別的題,O(nlogn)過不去,而這個O(n)的演算法可以過題。
    舉個例子:給個序列,序列中每個值都是正數,序列長度為百萬級別的,問有多少個區間[L,R],使得區間累加和為k。
    樸素做法是對於每個點都以它為起點,暴力掃描一遍,複雜度O(n²)。
    可以把樸素做法用二分搜尋優化到O(nlogn)。
    最快做法如下(這個連結的D題題解就是雙指標原題):

ans=0;//答案初始為0
int l=1,r=1;//定義雙指標的初始位置
while(l<=n&&r<=n){//當雙指標都在序列範圍內時
	if(pre[r]-pre[l-1]==s){//pre[]是字首和,這一段的值如果滿足條件,答案累加
		ans++;
		l++;r++;
	}
	else if(pre[r]-pre[l-1]>s){//如果超過,那麼拿掉左邊的一個
		l++;
	} else {//如果不足,從右邊新增一個
		r++;
	}
}
  • 3 字首和
    字首和是一種可以用來快速查詢區間和的資料結構,常見題型有“區間內素數個數”,“序列區間和”等。
const int maxn = 1e5+10;
int a[maxn],pre[maxn];
for(int i=1;i<=n;i++){
	pre[i]=pre[i-1]+a[i];//從1到i的累加和,等於從1到(i-1)的累加和,再加上a[i]的值。
}
//那麼我們要查詢區間[L,R]的累加和,只需要用pre[R]-pre[L-1]便可。
  • 4 樹狀陣列
    既然字首和可以處理區間和,那如果需要對單點進行修改操作呢?我們可以藉助樹狀陣列來實現。由於這個知識點需要很多前置知識,所以在這裡只貼模板。
#define maxn 1000060
int a[maxn],c[maxn];
int lowbit(int x){
    return x&(-x);
}
int n;
int sum(int i){
    int s=0;
    while(i) {
        s+=c[i];
        i-=lowbit(i);
    }
    return s;
}

void add(int i,int v){
    while(i<=n) {
        c[i]+=v;
        i+=lowbit(i);
    }
}

//如果需要在結點i處加上x,那麼只需要呼叫函式add(i,x)
//如果需要查詢區間[L,R]的區間和,這個值會等於sum(R)-sum(L-1)

時間複雜度分析:修改操作和查詢操作,複雜度均為O(logn)。

  • 5 指標實現二叉樹
    二叉排序樹是一個很經典的資料結構,程式碼如下:
struct node{
    bool used;      //標記該結點是否有值
    int v;
    node *l,*r;     //左右子節點
    node():used(false),l(NULL),r(NULL){};//構建函式
};
node* root;
node* newnode(){return new node();} //構建新結點
void build(node* u,int val){
    if(val < u->v){
        if(u->l!=NULL)
            build(u->l,val);
        else
            u->l = new node(val);
    }
    else{
        if(u->r!=NULL)
            build(u->r,val);
        else
            u->r = new node(val);
    }
}

//至於查詢操作,由於樹的結構是有特點的,所以訪問到某個點的時候,只需要判斷應該往左還是往右走便可,
//程式碼跟addnode操作是差不多的。

戳我,這篇部落格的E題是一個二叉排序樹的考試原題

戳我,這篇的E題也是一個二叉排序樹的考試原題,學會這兩題就能掌握BST了

  • 6 dfs
    我發現其實很多人都不太會深度優先搜尋(depth first search),所以貼一個原理和解釋吧。
//問題:有一個100*100的迷宮,告訴你起點終點,以及障礙物的位置,問你至少需要幾步從起點走到終點。
這裡我們用dfs解決(也可以用bfs解決,bfs是廣度優先搜尋)。
void dfs(int step,int x,int y){
    if((x,y)是終點,那麼step就是最小的答案);
    vis[x][y]=1;
    /**
    這裡寫往4個方向搜尋的程式碼,如果那個點以前沒有訪問過,那麼就往下搜
    if(......)dfs(step+1,x+1,y);如果這個點不是障礙物,並且以前沒有到達過,那麼我們就往下走。
    if(......)dfs(step+1,x-1,y);
    if(......)dfs(step+1,x,y+1);
    if(......)dfs(step+1,x,y-1);
    **/
}

這裡貼一個dfs例題題解的連結,裡面有完整程式碼,可作為模板使用。

  • 7 bfs
    我們可以認為dfs的實現過程像是一個棧,而bfs的實現過程像是一個佇列。
    以下程式碼也是以迷宮為原型的bfs
struct node{
    int x,y,step;
    node(int x,int y,int step):x(x),y(y),step(step){}
};
void bfs(){
    int vis[100][100]={0};
    queue<int> q;
    q.push(node(起點座標));
    while(!q.empty()){
        node now = q.front();q.pop();
        vis[now.x][now.y]=1;
        if(now這個點的座標是終點座標){ 答案就是now.step; }
        if(......)q.push();如果這個點不是障礙物,並且以前沒有到達過,那麼我們就壓入佇列,等待訪問。
        if(......)q.push();
        if(......)q.push();
        if(......)q.push();
    }
    如果佇列訪問空了都沒有找到答案,說明無解。
}

按理說,“搜尋”應該放在演算法一類講,不過這樣分類也無大礙,接下來是一些最基礎的搜尋原題和程式碼。

//題意:
//定義一個二維陣列: 
//它表示一個迷宮,其中的1表示牆壁,0表示可以走的路,只能橫著走或豎著走,不能斜著走,
//要求程式設計序找出從左上角到右下角的最短路線。

#include <cstdio>
#include <cstring>
#include <queue>
#include <iostream>
using namespace std;

int mp[5][5];
int ans[30][2];
int vis[5][5];

int fx[]={0,0,1,-1};
int fy[]={1,-1,0,0};//這裡是表示(+1,0),(-1,0),(0,+1),(0,-1)的四個不同方向

void print(int cnt)
{
    for(int i=0;i<=cnt;i++)
        printf("(%d, %d)\n",ans[i][0],ans[i][1]);
}

bool check(int x,int y)
{
    if(x<0||y<0||x>4||y>4||vis[x][y]||mp[x][y])//如果要訪問的點超出邊界,或者以前訪問過,或者是牆壁,則不能走
        return 0;
    vis[x][y]=1;//把這個點標記為已經訪問過
    return 1;//否則能走
}

void dfs(int x,int y,int cnt)
{
    ans[cnt][0]=x;
    ans[cnt][1]=y;//記錄路徑
    if(x+y==8)//如果到達終點,直接輸出答案
    {
        print(cnt);
        return;
    }
    for(int i=0;i<4;i++)//遍歷四個方向
    {
        if(check(x+fx[i],y+fy[i]))//如果能走
        {
            dfs(x+fx[i],y+fy[i],cnt+1);//dfs下去,步數+1
        }
    }
}

int main()
{
    for(int i=0;i<5;i++)
        for(int j=0;j<5;j++)
        scanf("%d",&mp[i][j]);
    dfs(0,0,0);
    return 0;
}
  • 8 二分搜尋
    使用二分的先決條件是,二分物件滿足單調性。
    比如:有一個序列a[],你想知道x在這個序列中的排名,假使這個序列有序,那麼我們就可以對序列進行二分。
const int maxn = 1e5+5;
int a[maxn];
void binary_search(int len,int x){
	sort(a+1,a+1+len);//如果無序,先對它排序
	int low = 0,high = len+1;
	while(low+1<high){
		int mid=low+high>>1;
		if(x>a[mid])low=mid;//如果x大於a[mid],說明x比任何在mid前面的數字都要大,所以我們要找的位置在後面區間
		else high=mid;//否則在前面區間裡
	}
	return low;
}

二分搜尋可以做一個經典的題型就是,二分答案。
思路是:如果答案滿足單調性,那麼我們可以先二分一個答案,然後檢查這個答案是否可行,
然後不斷地縮減區間,最後就能找到最終的答案了。
這裡貼一個原題的程式碼:

Alice是遊戲中的一名工匠,遊戲中最近“惡魔手套”很熱,她準備做一批,正好可以賺一筆。 
製作一件“惡魔手套”需要n種原材料,第i種原料需要ai份,Alice已經有第i種原料bi份。 
Alice還有k份兌換券,兌換券可以去商店兌換任意的原料,但一份兌換券只能兌換一份。
請問Alice最多可以製作多少件“惡魔手套”。
([題目來源](http://202.197.224.59/exam/index.php/problem/read/id/1269))

#include <bits/stdc++.h>
using namespace std;
#define ll __int64
ll n,k;
ll a[1005],b[1005];

bool check(ll num){
    ll sy=k;
    for(int i=1;i<=n;i++){
        if(b[i]<a[i]*num){
            sy -= a[i]*num - b[i];
        }
        if(sy<0)return 0;
    }return 1;
}

int main(){
    int t;scanf("%d",&t);
    while(t--){
        scanf("%I64d%I64d",&n,&k);
        for(int i=1;i<=n;i++)scanf("%I64d",a+i);
        for(int i=1;i<=n;i++)scanf("%I64d",b+i);
        ll l=0,r=2e9+5;
        while(l<r){
            ll mid=(l+r)>>1;
            if(check(mid))l=mid+1;
            else r=mid-1;
        }
        if(!check(l))l--;
        printf("%I64d\n",l);
    }
    return 0;
}


三 圖論

  • 1 建圖
    在瞭解這一個知識點之前,你需要前置知識《離散數學.圖論》和《資料結構》。
    對於稠密圖,我們直接用鄰接矩陣儲存。也就是說,圖中有多少個點,我們就開個多大的二維陣列
int mp[maxn][maxn];
//那麼,從點u到點v的距離就是mp[u][v]了,非常方便。

對於稀疏圖,我們採用鄰接表儲存,這裡有兩種儲存方式

1.1 vector存圖
1.2 前向星

struct edge{
	int to,cost;
	edge(int to,int cost):to(to),cost(cost){}
}
const int maxn = 1e5+10;
vector<edge> mp[maxn];

void addedge1(int u,int v,int w){
	mp[u].push_back(edge(v,w));
}

個人並不喜歡前向星建圖,認為在當下很少有人會卡常數,而且編譯器能夠把常數優化掉,
所以面向萌新的你們還是不貼前向星了,有興趣的可以自己去學習。
  • 2 單源最短路之dijkstra
//鄰接矩陣版本
const int INF=0x3f3f3f3f;
const int maxn=1200;

int dist[maxn],g[maxn][maxn],N;
bool vis[maxn];

void dijkstra()
{
    for(int i=1;i<=N;i++)
        dist[i]=(i==1)?0:INF;
    memset(vis,0,sizeof(vis));

    for(int i=1;i<=N;i++)
    {
        int mark=-1,mindis=INF;
        for(int j=1;j<=N;j++)
        {
            if(!vis[j]&&dist[j]<mindis)
            {
                mindis=dist[j];
                mark=j;
            }
        }
        vis[mark]=1;

        for(int j=1;j<=N;j++)
        {
            if(!vis[j])
            {
                dist[j]=min(dist[j],dist[mark]+g[mark][j]);
            }
        }
    }
}

// 堆優化dijkstra
//這份模板用到的建圖方式是前向星

#include<bits/stdc++.h>
using namespace std;
int head[100001],ne[200001],to[200001],w[200001],edgenum=0;
int dis[100001];
bool vis[100001];
int inf;
struct node{
    int pos,val;
    bool operator <(const node &a)const {return a.val<val;}
};
priority_queue<node> que;
inline void addedge(int f,int t,int co)
    {
        ne[++edgenum]=head[f];
        head[f]=edgenum;
        to[edgenum]=t;
        w[edgenum]=co;
    }

inline int read()
    {
        int x = 0, w = 0; char ch = getchar();
        for(;!isdigit(ch);w |= (ch == '-'), ch = getchar());
        for(;isdigit(ch);x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar());
        return w ? -x : x;
    }

inline node make_node(int x, int y)
    {
        node a;
        a.pos = x, a.val = y;
        return a;
    }

void Dijkstra(int s)
    {
        memset(dis,0x3f,sizeof(dis));
       // inf = dis[0];
        dis[s]=0;
        que.push(make_node(s, dis[s]));
        while(!que.empty())
            {
                node x=que.top();que.pop();
                int u = x.pos;
                if(x.val > dis[u]) continue; //這一步就相當於是刪除了那些不夠優的節點
                vis[u]=true;
                for(int i=head[u];i;i=ne[i])
                    {
                        int v=to[i];
                        if(vis[v])    continue;
                        if(dis[v]>w[i]+dis[u])
                            {
                                dis[v]=w[i] + dis[u];
                                que.push(make_node(v, dis[v]));
                            }
                    }
            }
    }
int main()
{
    int n = read(),m = read(),s = read(),x,y,l;
    for(int i=1;i<=m;i++)
        {
            x = read(), y = read(), l = read();
            addedge(x,y,l);
        }
    Dijkstra(s);
    for(int i=1;i<=n;i++)  printf("%d ",dis[i]);
    printf("\n");
    return 0;
}
  • 3 多源最短路之floyd
    floyd的過程像是一個動態規劃,複雜度O(n3)
#define MAX 500
#define INFE 1<<20
int N; 
int map[MAX][MAX],b[MAX],path[MAX][MAX];  //path[i][j]記錄路徑
void init(){
       int i,j;
       for(i=1;i<=N;i++)
              for(j=1;j<=N;j++) {
                     map[i][j]=INFE;
                     path[i][j]=j;
              }
}
void floyd(){
       int i,j,k;
       for(k=1;k<=N;k++)
              for(i=1;i<=N;i++)
                     for(j=1;j<=N;j++)
                            if(map[i][j]>map[i][k]+map[k][j]) {
                                   map[i][j]=map[i][k]+map[k][j];
                                   path[i][j]=path[i][k];
                            }
}
//最後點u和點v的距離就是map[i][j]了,如果map[i][j] == INFE,說明不存在(u,v)的路徑。
  • 4 最小生成樹之克魯斯卡爾
    前置知識:並查集。(不會也沒關係,照抄就是了)
    最小生成樹:給出一個無向圖,有n個點和m條邊,要你從中選出n-1條邊,
    使得這個圖變成一棵樹,並且邊權之和最小。
Description
求一個非負權邊的無向連通圖的最小生成樹,如果這個無向圖存在兩個或兩個以上的最小生成樹,
就輸出Not Unique,否則輸出最小生成樹的邊的權值和。
輸入:
第一行是一個整數K,表示有多少個測試用例,以後每個測試用例佔m+1行。
每個測試用例的第一行為兩個整數n,m(3<=n<=100),表示圖的頂點數和邊數,
從第二行開始每行為三個整數i,j,w,表示從i到j頂點的權值。
輸出:
每行輸出一個測試用例的結果。如果這個無向圖存在兩個或兩個以上的最小生成樹,
就輸出Not Unique,否則輸出最小生成樹的邊的權值和。
(這個題目就是202.197.224.59/exam上的1045)

#include<bits/stdc++.h>
using namespace std;
#define r(t) scanf("%d",&t)
int f[101];
int flag[101*101];
struct dis
{
    int x,y,d;
    friend bool operator < (dis A,dis B)
    {
        return A.d<B.d;
    }
}dis[101*101];
int t,n,m;

int find(int a)
{
    return a==f[a]?a:f[a]=find(f[a]);
}

void check(int a,int b)
{
    int fa=find(a),fb=find(b);
    if(fa!=fb)
    {
        f[fa]=fb;
    }
}

int main()
{
    r(t);
    while(t--)
    {
        int ans=0;
        int ok=0;
        memset(flag,0,sizeof(flag));

        r(n);r(m);
        for(int i=1;i<=n;i++)f[i]=i;
        for(int i=1;i<=m;i++)
        {
            r(dis[i].x);r(dis[i].y);r(dis[i].d);
        }
        sort(dis+1,dis+1+m);

        for(int i=1;i<=m;i++)
        {
            if(find(dis[i].x)!=find(dis[i].y))
            {
                check(dis[i].x,dis[i].y);
                flag[i]=1;
                ans+=dis[i].d;
            }
        }
        for(int q=1;q<=m;q++)
        {
            if(flag[q]==1)
            {
                int cnt=0;
                for(int i=1;i<=n;i++)f[i]=i;
                for(int i=1;i<=m;i++)
                {
                    if(q==i)continue;
                    if(find(dis[i].x)!=find(dis[i].y))
                    {
                        check(dis[i].x,dis[i].y);
                        cnt+=dis[i].d;
                    }
                }
                if(cnt==ans)ok=1;
            }
        }
        if(ok)cout<<"Not Unique\n";
        else cout<<ans<<endl;
    }
    return 0;
}

四 動態規劃

動態規劃是一個龐大的知識塊,它有很多種解決不同型別問題的變形,所以我只貼一些原題的程式碼。
1303

n(1≤n≤60)個整陣列成的序列(a1,a2,…,an),1≤ai≤8。
每次你可以從序列的頭部或者尾部取一個數,第i(i=1,2,…,n)輪取數為ak,那麼其代價為ak×2i−1。 
求將序列取完的最小代價。

#include <bits/stdc++.h>
using namespace std;
#define ll unsigned long long
int a[64];
int t;
int n;
ll dp[64][64];

ll dfs(int l,int r)
{
    if(dp[l][r])return dp[l][r];
    dp[l][r]=min(dfs(l+1,r)*2+a[l],
                 dfs(l,r-1)*2+a[r]);
    return dp[l][r];
}

int main()
{
    cin>>t;
    while(t--)
    {
        memset(dp,0,sizeof(dp));
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",a+i);
            dp[i][i]=a[i];
        }
        cout<<dfs(1,n)<<"\n";
    }
    return 0;
}

某個模擬考試E題

給一個整數序列{a1,a2,…,an},存在這樣的子序列{ai1,ai2,…,aim}∣1≤i1<i2<…<im≤n,
使得ai1<ai2<…<aik−1<aik>aik+1>…>aim。求最長的滿足條件的子序列的長度。

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

int main(){
    int a[10005];
    int pre[1005];
    int suf[1005];
    int t;cin>>t;
    while(t--){
        memset(pre,0,sizeof pre);
        memset(suf,0,sizeof suf);
        int n;scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%d",a+i);
            for(int j=1;j<i;j++){
                if(a[i]>a[j])
                    pre[i]=max(pre[i],pre[j]+1);
            }
        }
        for(int i=n;i>=1;i--){
            for(int j=n;j>i;j--){
                if(a[i]>a[j]){
                    suf[i]=max(suf[i],suf[j]+1);
                }
            }
        }
        int ans=0;
        for(int i=1;i<=n;i++){
            if(pre[i]&&suf[i]){
                ans=max(ans,pre[i]+suf[i]+1);
            }
        }
        cout<<ans<<endl;
    }
    return 0;
}

三的倍數,原題

思路:

1.我們知道,一個數字是3的倍數,當且僅當這個數字的數位和能整除3,那麼我們只需要找出那些使得數位和能整除3的種類。

2. 
dp[i][j]表示以第i個數字為數字的開頭,能構成多少種符合條件的數字(此時不考慮前導0) 
那麼有遞推式: 
①:當前位模3餘0: 
dp[i][0]=dp[i+1][0]<<1|1; 
dp[i][1]=dp[i+1][1]<<1; 
dp[i][2]=dp[i+1][2]<<1;

②:當前位模3餘1: 
dp[i][0]=dp[i+1][0]+dp[i+1][2]; 
dp[i][1]=dp[i+1][1]+dp[i+1][0]+1; 
dp[i][2]=dp[i+1][2]+dp[i+1][1];

③: 
dp[i][0]=dp[i+1][0]+dp[i+1][1]; 
dp[i][1]=dp[i+1][1]+dp[i+1][2]; 
dp[i][2]=dp[i+1][2]+dp[i+1][0]+1;

此時,對於這個數字,dp[0][0]就是要求的答案。

3.問題在於如何去除含有前導0的種類數: 
對於s[i]==0: 
它的種類數應該是dp[i+1][0]+1; 
(當前位是0,後面要湊成3的倍數,共有dp[i+1][0]種,然後後面全部不選,只選第i也滿足,故種類數是dp[i+1][0]+1)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod = 1e9+7;
int main()
{
    char s[10000];
    int v[10000];
    ll dp[10000][3];
    while(scanf("%s",s)!=EOF)
    {
        int len=strlen(s);
        for(int i=0;i<len;i++)
            v[i] = (s[i]-'0')%3;
        dp[len][0]=dp[len][1]=dp[len][2]=0;
        for(int i=len-1;i>=0;i--)
        {
            if(v[i]==0)
            {
                dp[i][0]=dp[i+1][0]<<1|1;
                dp[i][1]=dp[i+1][1]<<1;
                dp[i][2]=dp[i+1][2]<<1;
            }
            else if(v[i]==1)
            {
                dp[i][0]=dp[i+1][0]+dp[i+1][2];
                dp[i][1]=dp[i+1][1]+dp[i+1][0]+1;
                dp[i][2]=dp[i+1][2]+dp[i+1][1];
            }
            else
            {
                dp[i][0]=dp[i+1][0]+dp[i+1][1];
                dp[i][1]=dp[i+1][1]+dp[i+1][2];
                dp[i][2]=dp[i+1][2]+dp[i+1][0]+1;
            }
            dp[i][0]%=mod;dp[i][1]%=mod;dp[i][2]%=mod;
        }
        ll ans=dp[0][0];
        for(int i=1;i<len;i++)
        {
            if(s[i]=='0')
            {
                ans = ans-dp[i+1][0]-1;
                ans%=mod;
            }
        }
        cout<<(ans+mod)%mod<<"\n";
    }
    return 0;
}

另外再補充一些簡單動態規劃模板吧。

最長公共子序列:
對於兩個序列,找到他們最長公共子序列的長度

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

#define maxn 1111
string s1,s2;
int dp[maxn][maxn]={0};
int main()
{
    while(cin>>s1>>s2)
    {
        int l1 = s1.length();
        int l2 = s2.length();

        memset(dp,0,sizeof(dp));

        for(int i=0;i<l1;i++)
        {
            for(int j=0;j<l2;j++)
            {
                if(s1[i]==s2[j])
                {
                    dp[i+1][j+1]=max(dp[i][j]+1,dp[i+1][j+1]);
                }
                else
                {
                    dp[i+1][j+1]=max(dp[i][j+1],dp[i+1][j]);
                }
            }
        }
        cout<<dp[l1][l2]<<endl;
    }
    return 0;
}

最長上升子序列:
對於一個序列,找到最長上升的子序列長度

#include<iostream>
#include<cstring>
using namespace std;
int a[1000];
int dp[1000];
int BinarySearch(int x, int len)//二分查詢dp[]裡面第一個大於等於x的數
{
    int left=1, right=len, mid;
    while(left<=right)
    {
        mid=(left+right)/2;
        if(x==dp[mid])
            return mid;
        else if(x>dp[mid])
            left=mid+1;
        else if(x<dp[mid])
            right=mid-1;
    }
    return left;
}
int main()
{
    int N;
    while(cin>>N)
    {
        memset(dp, 0, sizeof(dp));
        for(int i=1; i<=N; i++)
        {
            cin>>a[i];
        }
        dp[1]=a[1];
        int len=1;//當前已經求出來的最長序列長度
        int j;//dp[]的下標
        for(int i=2; i<=N; i++)
        {
            //if(a[i]<dp[1])
               // j=1;
            if(a[i]>dp[len])//如果a[i]>dp[len]   len +1
                j=++len;
            else//反之, 更新j
                j=BinarySearch(a[i], len);
            dp[j]=a[i];//把a[i]更新入dp[]陣列
 
        }
        cout<<len<<endl;
    }
    return 0;
}

01揹包:
有一個容量為V的揹包,有很多體積不等和價值不等物品,問最多能裝多少價值的物品。

#include<iostream>
#include<string.h>
using namespace std;

int dp[1001];
int vo[1001];
int va[1001];

int main()
{
    int n,all;
    int t;
    cin>>t;
    while(t--)
    {
        memset(dp,0,sizeof(dp));

        cin>>n>>all;
        for(int i=1;i<=n;i++)
            cin>>va[i];
        for(int i=1;i<=n;i++)
            cin>>vo[i];
        for(int i=1;i<=n;i++)
        {
            for(int j=all;j>=0;j--)
            {
                if(j-vo[i]>=0)
                dp[j]=max(dp[j],dp[j-vo[i]]+va[i]);
            }
        }
        cout<<dp[all]<<endl;
    }
    return 0;
}

五 其他

  • 1 重定向輸入輸出
    freopen("a.in","r",stdin);//從a.in這個檔案讀取
    freopen("a.out","w",stdout);//輸出到a.out這個檔案
  • 2 輸入掛,輸出掛
    用了輸入掛輸出掛就別用普通的scanf和printf,容易出錯。
inline char nc(){
    static char buf[100000],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
template<class T>
inline void read(T &sum){
    char ch=nc();sum=0;
    while(!(ch>='0'&&ch<='9'))ch=nc();
    while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=nc();
}

template<class T>
inline void print(T x){
    if(x>9)print(x/10);putchar(x%10+'0');
}

讀入一個整數int a 就是 read(a);
輸出一個整數int a 就是 print(a);

結語:

2018.12.5更新:
今天把四個主要內容補充完,其中很多程式碼都是直接手寫的,沒有檢驗過正確性,如果有錯誤,請指出。
部分模板來自網路,部分 原題來自exam。
希望能夠幫到那些初學者,以及被eric折磨得死去活來 的無辜群眾們。

因為寫這個面向的是小朋友嘛,所以作為一個學長,希望你們能夠好好學習程式設計,切身去感受一下演算法的魅力,有興趣的話可以加一下eric帶的 那個實驗室,
叫什麼名字來著?湘潭大學ACM集訓隊

相關文章