程式設計實踐考試的入門模板
前言
其實從大二開始就在整理有關如何學習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題也是一個二叉排序樹的考試原題,學會這兩題就能掌握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;
}
給一個整數序列{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集訓隊。
相關文章
- 《Python程式設計:從入門到實踐》Python程式設計
- 2018上學期程式設計實踐考試總結程式設計
- Python 程式設計從入門到實踐5Python程式設計
- 函數語言程式設計入門實踐(一)函數程式設計
- python核心程式設計:入門Python程式設計的8個實踐性建議Python程式設計
- 函數語言程式設計入門實踐 —— Compose/Pipe函數程式設計
- 探索 TypeScript 程式設計的利器:ts-morph 入門與實踐TypeScript程式設計
- C語言開發入門與程式設計實踐pdfC語言程式設計
- 三週刷完《Python程式設計從入門到實踐》的感受Python程式設計
- Python專案實戰(一)《Python程式設計 從入門到實踐》Python程式設計
- python程式設計:從入門到實踐學習筆記-字典Python程式設計筆記
- 《python 程式設計從入門到實踐》序:學習目標Python程式設計
- 資源 | 小白必收!《Python程式設計 從入門到實踐》Python程式設計
- 《Python程式設計:從入門到實踐》第2章習題Python程式設計
- 湘潭大學2018年上學期程式設計實踐模擬考試3 參考題解程式設計
- 湘潭大學2018年上學期程式設計實踐模擬考試2 參考題解程式設計
- React本機模板設計入門(1)React
- python程式設計:從入門到實踐學習筆記-函式Python程式設計筆記函式
- python3入門與實踐(六):函數語言程式設計Python函數程式設計
- Spring Boot Docker入門模板與4個最佳實踐Spring BootDocker
- 入門程式碼程式設計程式設計
- 《Python程式設計:從入門到實踐》 筆記(一)基礎知識Python程式設計筆記
- Python程式設計入門Python程式設計
- Shell 程式設計入門程式設計
- SpringBoot 單元測試入門實踐Spring Boot
- 程式設計和網路程式設計入門程式設計
- 【Python程式設計從入門到實踐】 1 Linux搭建Python編譯環境Python程式設計Linux編譯
- Go程式設計實踐Go程式設計
- 通過 Socket 實現 TCP 程式設計入門TCP程式設計
- 通過 Socket 實現 UDP 程式設計 入門UDP程式設計
- Socket程式設計入門(基於Java實現)程式設計Java
- MongoDB實現問卷/考試設計MongoDB
- 微信小程式入門與實踐微信小程式
- 單元測試的入門實踐與應用
- Number 1 — 程式設計入門程式設計
- Flink DataStream 程式設計入門AST程式設計
- java Swing程式設計入門Java程式設計
- Python程式設計:從入門到實踐(第2版)第1章習題答案Python程式設計