藍橋杯 C/C++A組國賽

一.深度優先搜索的剪枝

1.可行性剪枝
下面的算法用于從0~30個數中選取8個,使其和為200.每一個數有選與不選兩個枝,若選取得數字個數已經大于8個,將這個枝剪去,若數字和s已經大于200,則將這個枝剪去。
這種判斷解是否可行得剪枝稱位可行性剪枝。

#include <iostream>
using namespace std;
int n, k, sum, ans;
int a[40];
void dfs(int i, int cnt, int s) {
    if(cnt>k){
        return;
    }
    if(s>sum){
        return;
    }
    if (i == n) {
        if (cnt == k && s == sum) {
            ans++;
        }
        return;
    }
    dfs(i + 1, cnt, s);
    dfs(i + 1, cnt + 1, s + a[i]);
}
int main() {
    n = 30;
    k = 8;
    sum = 200;
    for (int i = 0; i < 30; i++) {
        a[i] = i + 1;
    }
    ans = 0;
    dfs(0, 0, 0);
    cout << ans << endl;
    return 0;
}

2.最優性剪枝
這是尋找地圖中起點到終點最短路徑的算法
ans用于存儲目前最小的步數,如果某一條路徑長度(step)已經大于ans了,就將這條枝剪去,這樣尋找最優解決辦法的剪枝叫做最優性剪枝。若到達終點時步數小于ans,則更新ans的大小。

#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
int n,m;
string maze[110];
bool vis[110][110];
int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
int ans=100000;
bool in(int x,int y){
    return 0<=x&&x<n&&0<=y&&y<m;
}
void dfs(int x,int y,int step){
    if(step>=ans){
        return;
    }
    if(maze[x][y]=='T'){
        ans=step;
        return;
    }
    vis[x][y]=1;
    for(int i=0;i<4;i++){
        int tx=x+dir[i][0];
        int ty=y+dir[i][1];
        if(in(tx,ty)&&maze[tx][ty]!='*'&&!vis[tx][ty]){
            dfs(tx,ty,step+1);
        }
    }
    vis[x][y]=0;
}
int main() {
    cin>>n>>m;
    for(int i=0;i<n;++i){
        cin>>maze[i];
    }
    int x,y;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(maze[i][j]=='S'){
                x=i,y=j;
            }
        }
    }
    dfs(x,y,0);
    cout<<ans<<endl;
    return 0;
}

3.重復性剪枝
在與順序無關的深搜中,我們最常用到重復性剪枝,例如從n個整數里選k個數使得和為s。這種與順序無關的dfs中我們常用重復性剪枝。

#include <iostream>
using namespace std;
int n, k, sum, ans;
int a[40];
bool xuan[40];
void dfs(int s, int cnt,int pos) {
    if(s>sum||cnt>k){
        return;
    }
    if (s == sum && cnt == k) {
        ans++;
    }
    for (int i = pos; i < n; i++) {
        if (!xuan[i]) {
            xuan[i] = 1;
            dfs(s + a[i], cnt + 1,i+1);
            xuan[i] = 0;
        }
    }
}
int main() {
    n = 30;
    k = 8;
    sum = 200;
    for (int i = 0; i < 30; i++) {
        a[i] = i + 1;
    }
    ans = 0;
    dfs(0, 0,0);
    cout << ans << endl;
    return 0;
}

4.奇偶性剪枝
本質上來說是一種可行性剪枝
從起點到終點,要求正好T步可以到達
設某一點得橫坐標與縱坐標之和為奇書就是白色,偶數就是黑色。易得如果起點與終點同色,則必須走偶數步,不同色則必須走奇數步??梢愿鶕@個奇偶性對結果進行剪枝。

#include <iostream>
using namespace std;
const int N = 10;
int n, m, T;
char mat[N][N];
bool vis[N][N];
int dx[4]={0,0,-1,1};
int dy[4]={1,-1,0,0};
bool ok;
void dfs(int x,int y,int t){
    if(ok)return;
    if(t==T){
        if(mat[x][y]=='D')ok=true;
        return;
    }
    vis[x][y]=true;
    for(int i=0;i<4;i++){
        int tx=x+dx[i];
        int ty=y+dy[i];
        if(tx<0||tx>=n||ty<0||ty>=m||mat[tx][ty]=='X'||vis[tx][ty])
            continue;
        dfs(tx,ty,t+1);
    }
    vis[x][y]=false;
}
int main() {
    cin >> n >> m >> T;
    for (int i = 0; i < n; ++i) {
        cin >> mat[i];
    }
    int sx,sy,ex,ey;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(mat[i][j]=='S')sx=i,sy=j;
            if(mat[i][j]=='D')ex=i,ey=j;
        }
    }
    if((sx+sy+ex+ey+T)%2!=0){
        cout<<"NO"<<endl;
    }else{
        ok=false;
        dfs(sx,sy,0);
        if(ok)
            cout<<"YES"<<endl;
        else
            cout<<"NO"<<endl;
    }
    return 0;
}

dfs剪枝習題

1.給一個數n,要求你找出一個只由0和1組成的得十進制數m,使這個正整數m可以被n整除。

#include <iostream>
#include <stdio.h>
using namespace std;
int n;
long long ans;
bool flag=false;
void dfs(long long num,int depth){
    if(flag||depth>18)return;//最優性剪枝
    if(num%n==0){
        ans=num;
        flag=true;
        return;
    }
    dfs(num*10,depth+1);
    dfs(num*10+1,depth+1);
}

int main(){
    cin>>n;
    dfs(1,1);
    cout<<ans<<endl;
    return 0;
}

2.全排列
給出一個數n,輸出n的全排列的排列個數,并按字母序輸出

#include <iostream>
#include <string.h>
#include <string>
#include <vector>
#define numToChar(x) (x+'0')
using namespace std;
int n;
bool vis[10];
vector<string> save;
void dfs(int num,string ans,int step){
    ans+=numToChar(num);
    if(step==n){
        save.push_back(ans);
        return;
    }
    for(int i=1;i<=n;++i){
        if(!vis[i]){
            vis[i]=true;
            dfs(i,ans,step+1);
            vis[i]=false;
        }
    }
}

int main(){
    memset(vis,0,sizeof(vis));
    cin>>n;
    for(int i=1;i<=n;++i){
        vis[i]=true;
        dfs(i,"",1);
        vis[i]=false;
    }
    cout<<save.size()<<endl;
    for(int i=0;i<save.size();++i){
        printf("%s\n",save[i].c_str());
    }
    return 0;
}

3.蒜頭君的旅游計劃

#include <iostream>
#include <cstring>
using namespace std;
int n,ans=150010;
int mat[16][16];
bool vis[16];
void dfs(int city,int num,int money){
    if(num==n){
        ans=min(ans,money+mat[city][0]);
        return;
    }
    if(money>=ans)return;//最優性剪枝
    for(int i=1;i<n;++i){
        if(!vis[i]){
            vis[i]=true;
            dfs(i,num+1,money+mat[city][i]);
            vis[i]=false;
        }
    }
}

int main(){
    memset(vis,0,sizeof(vis));
    cin>>n;
    for(int i=0;i<n;++i){
        for(int j=0;j<n;++j){
            cin>>mat[i][j];
        }
    }
    dfs(0,1,0);
    cout<<ans<<endl;
    return 0;
}

4.正方形

#include <iostream>
#include <cstring>
using namespace std;
int sum=0;
int n,mat[30];
bool vis[30];
bool flag=false;
void dfs(int index,int cnt,int len){
    if(flag||len>(sum/4))return;
    if(cnt==3){
        flag=true;
        return;
    }
    if(len==sum/4){
        len=0;
        cnt++;
        index=0;
    }
    for(int i=index;i<n;++i){
        if(!vis[i]){
            vis[i]=true;
            dfs(i,cnt,len+mat[i]);
            vis[i]=false;
        }
    }
}

int main(){
    memset(vis,0,sizeof(vis));
    cin>>n;
    for(int i=0;i<n;++i){
        cin>>mat[i];
        sum+=mat[i];
    }//搜索出四個長度相同的木棒
    bool f1=true;
    for(int i=0;i<n;++i){
        if(mat[i]>sum/4)f1=false;
    }
    if(sum%4==0&&f1)dfs(0,0,0);
    flag?cout<<"Yes":cout<<"No";
    return 0;
}

二,圖

當邊的個數e<nlogn時,我們稱其為稀疏圖,反之為稠密圖。無向圖中每一對頂點間相連叫完全圖,在有向圖中任何一對頂點間都有(u,v),(v,u)兩條邊,則稱為有向完全圖。

度的概念

在無向圖中,頂點的度指某個頂點連出的邊數。
把圖中所有頂點的排列成一個序列s,s稱位一個度序列。圖中的邊數為總度數的一半。
在有向圖中,和度對應的有入度和出度兩個概念,入度指以該頂點為終點的有向邊數量,出度指以該頂點為起點的有向邊數量。且總的入度數=總的出度數

一個度序列是否可圖,可以運用Havel-Hakimi定理:
s:d1,d2,d3,d4,d5....dn可圖,那么d2-1,d3-1,d4-1,d5-1,....d(d1)-1.....dn可圖....

證明:4 3 2 2 1可圖
--->2 1 1 0
--->0 0 0 所以4 3 2 2 1可圖

核心代碼:

bool Havel_Hakimi(){
    for(int i=0; i<n-1; ++i){
        sort(arr+i,arr+n,greater<int>());
        if(i+arr[i] >= n) return false;
        for(int j=i+1; j<=i+arr[i] ; ++j){
            --arr[j];
            if(arr[j] < 0) return false;
        }
    }
    if(arr[n-1]!=0) return false;
    return true;
}
圖的存儲方法

1.鄰接矩陣存儲圖的例題:找朋友
G[i][j]=1代表i到j存在一條有向邊

#include <iostream>
#include <cstring>
using namespace std;
int main() {
    int G[6][6];
    memset(G,0,sizeof(G));
    int m;
    cin>>m;
    for(int i=0;i<m;i++){
        int a,b;
        cin>>a>>b;
        G[a][b]=1;
    }
    for(int i=1;i<=5;i++){
        int sum=0;
        for(int j=1;j<=5;j++){
            if(G[i][j]==1&&G[j][i]==1){
                sum++;
            }
        }
        cout<<i<<" 有 "<<sum<<" 個真正的朋友。"<<endl;
    }
    return 0;
}

2.鄰接表
對于每一個頂點a,都用一個vector存儲a到b的有向邊
具體操作:G[a].push_back(b)
鄰接表的好處是存儲稀疏圖時比較省空間,但是查詢起來效率較低。
在稀疏圖中我們常用鄰接表,稠密圖我們常用鄰接矩陣。
3.帶權值的圖的存儲方法
1)鄰接矩陣 G[a][b]存儲a到b的邊權
2)鄰接表 用一個struct存儲

#include <iostream>
#include <vector>
using namespace std;
struct node{
    int v,w;
};
vector<node> G[11];
void insert1(int u,int v,int w){
    node temp;
    temp.v=v;
    temp.w=w;
    G[u].push_back(temp);
}
void insert2(int u,int v,int w){
    insert1(u,v,w);
    insert1(v,u,w);
}
void input(){
    int m;
    cin>>m;
    for(int i=0;i<m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        insert2(u,v,w);
    }
}
int main() {
    
    return 0;
}

基于鏈表的鄰接表:

const int M = 1000000;
const int N = 10000;
struct edge {
    int v, d, next;
} e[M];
int p[N], eid;
void init() {  // 初始化,在建圖之前必須進行
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int d) {  // 插入單向邊
    e[eid].v = v;
    e[eid].d = d;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int d) {  // 插入雙向邊
    insert(u, v, d);
    insert(v, u, d);
}
void output(int n) {  // 輸出整張圖中的所有邊
    for (int i = 0; i < n; i++) {
        for (int j = p[i]; j != -1; j = e[j].next) {  // 遍歷從 i 連出的所有邊
            cout << i << "->" << e[j].v << ", " << e[j].d << endl;
        }
    }
}

關系查詢:

#include <iostream>
#include <map>
#include <vector>
#include <string>
using namespace std;
int n,m;
map<string,vector<string>> friends;

int main(){
    cin>>n;
    for(int i=0;i<n;++i){
        string temp1,temp2;
        cin>>temp1>>temp2;
        friends[temp1].push_back(temp2);
        friends[temp2].push_back(temp1);
    }
    cin>>m;
    for(int i=0;i<m;++i){
        string temp1,temp2;
        cin>>temp1>>temp2;
        bool flag=false;
        for(int j=0;j<friends[temp1].size();++j){
            if(friends[temp1][j]==temp2){
                flag=true;
                break;
            }
        }
        flag?cout<<"Yes"<<endl:cout<<"No"<<endl;
    }
    return 0;
}

蒜頭君的旅行計劃

#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
int n,m;
struct node{
    int pos,w;
};
bool vis[50000];
vector<vector<node>> e;
void play(int pos){
    if(vis[pos]){
        return;
    }
    vis[pos]=true;
    printf("%d ",pos);
    int m=1000010,index=-1;
    for(int i=0;i<e[pos].size();++i){
        if(e[pos][i].w<m){
            index=e[pos][i].pos;
            m=e[pos][i].w;
        }else if(e[pos][i].w==m)index=min(index,e[pos][i].pos);
    }
    play(index);
}

int main(){
    int u,v,w;
    cin>>n>>m;
    e.resize(n+1);
    for(int i=0;i<m;++i){
        scanf("%d %d %d",&u,&v,&w);
        e[u].push_back({v,w});
        e[v].push_back({u,w});
    }
    play(1);
    return 0; 
}

圖習題

1.完全圖的判定

#include <iostream>
using namespace std;
int n,m;

int main(){
    cin>>n>>m;
    bool e[110][110];
    int cnt=0;
    for(int i=0;i<m;++i){
        int u,v;
        cin>>u>>v;
        if(!e[u][v]){
            e[u][v]=e[v][u]=true;
            cnt++;
        }
    }
    if(cnt==n*(n-1)/2)cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    return 0;
}

三、最短路

由于dfs解決最短路問題非常麻煩,而bfs只能解決權值為1的最短路,所以我們引入了最短路算法。

1.Dijkstra

Dijkstra用于解決單源最短路問題,特點是以起點為中心,逐層向外擴展,值得注意的是,這個算法不能處理負邊。(bfs+貪心)
算法步驟
首先初始化一個dist[v]集合,它代表起點s到頂點v的距離,將起點s的dist初始化為0,其他頂點置為inf
1.找出一個離源點最近的v,將v加入最短路集合U
2.用dist[v]和v連出來的邊更新不在集合U的頂點,這一步稱為松弛操作
3.不停重復1,2,直到V=U或者找不到新的點
如果V!=U,則說明找不到最短路徑,否則dist[i]代表s到i的最短路徑
算法演示:

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, fail;
    edge() {}
    edge(int _v, int _w, int _fail) {
        v = _v;
        w = _w;
        fail = _fail;
    }
} e[M << 1];
int head[N], len;
void init() {
    memset(head, -1, sizeof(head));
    len = 0;
}
void add(int u, int v, int w) {
    e[len] = edge(v, w, head[u]);
    head[u] = len++;
}
void add2(int u, int v, int w) {
    add(u, v, w);
    add(v, u, w);
}
int n, m;
int dis[N];
bool vis[N];
void dijkstra(int u) {
    memset(vis, false, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    dis[u] = 0;//初始化
    for (int i = 0; i < n; ++i) {
        int mi = inf;
        for (int j = 1; j <= n; ++j) {
            if (!vis[j] && dis[j] < mi) {
                mi = dis[u = j];//尋找最近的未加入集合U的頂點
            }
        }
        if (mi == inf) {
            return;
        }
        vis[u] = true;
        for (int j = head[u]; ~j; j = e[j].fail) {//若找到,對所有與這個點相連并且未訪問過的頂點進行松弛
            int v = e[j].v;
            int w = e[j].w;
            if (!vis[v] && dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
            }
        }
    }
}
int main() {
    init();
    int u, v, w;
    cin >> n >> m;
    while (m--) {
        cin >> u >> v >> w;
        add2(u, v, w);
    }
    dijkstra(1);
    cout << dis[n] << endl;
    return 0;
}

Dijkstra算法的核心思想是從未確定最短路的點的集合中選區一個最小的來更新其他的點,暴力枚舉的話時間復雜度是O(V^2),如果考慮堆優化,用一個set來維護點的集合,復雜度可以降到O((V+E)logV)
set的第二個參數代表堆的排序方法

typedef pair<int, int> PII;
set<PII, less<PII> > min_heap;
int dist[MAX_N];  // 存儲單源最短路的結果
bool vst[MAX_N];  // 標記每個頂點是否在集合 U 中
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    min_heap.insert(make_pair(0, s));
    dist[s] = 0;
    for (int i = 0; i < n; ++i) {
        if (min_heap.size() == 0) {  // 如果小根堆中沒有可用頂點,說明有頂點無法從源點到達,算法結束
            return false;
        }
        // 獲取堆頂元素,并將堆頂元素從堆中刪除
        set<PII, less<PII> >::iterator iter = min_heap.begin();
        int v = iter->second;
        min_heap.erase(*iter);
        vst[v] = true;
        // 進行和普通 dijkstra 算法類似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {
                // 先將對應的 pair 從堆中刪除,再將更新后的 pair 插入堆
                min_heap.erase(make_pair(dist[x], x));
                dist[x] = dist[v] + e[j].w;
                min_heap.insert(make_pair(dist[x], x));
            }
        }
    }
    return true;  // 存儲單源最短路的結果
}

習題
1.騎車比賽

#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
const int inf=0x3f3f3f;
int n,m;
int e[1001][1001];
int dis[1001];
bool vis[1001],have_e[1001][1001];
void dijkstra(int u){
    memset(dis,0x3f,sizeof(dis));
    memset(vis,false,sizeof(vis));
    dis[u]=0;
    for(int i=0;i<n;++i){
        int mind=inf,mint=-1;
        for(int j=1;j<=n;++j){
            if(!vis[j]&&dis[j]<mind){
                mind=dis[j];
                mint=j;
            }
        }
        if(mint==-1)return;
        vis[mint]=true;
        for(int j=1;j<=n;++j){
            if(!vis[j]&&have_e[mint][j]&&dis[j]>dis[mint]+e[mint][j]){
                dis[j]=dis[mint]+e[mint][j];
            }
        }
    }
}
int main(){
    cin>>n>>m;
    int u,v,w;
    while(m--){
        cin>>u>>v>>w;
        e[u][v]=e[v][u]=w;
        have_e[u][v]=have_e[v][u]=true;
    }
    dijkstra(1);
    cout<<dis[n]<<endl;
    return 0;
}

SPFA單源最短路

SPFA單源最短路是bfs的隊列優化版本
一般用di代表頂點到i的最短路,額外用一個隊列來保存即將進行拓展的頂點列表用inqi來表示
算法步驟:
1.首先初始化,初始隊列只包含源點,且ds=0
2.取出隊列的頭頂點u,掃描從頂點u出發的每條邊,并進行松弛操作,若被松弛的頂點v不在隊列中,將v入隊
3.重復步驟2直到頂點為空
代碼:

bool inq[MAX_N];
int d[MAX_N];  // 如果到頂點 i 的距離是 0x3f3f3f3f,則說明不存在源點到 i 的最短路
void spfa(int s) {
    memset(inq, 0, sizeof(inq));
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    inq[s] = true;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inq[u] = false;
        for (int i = p[u]; i != -1; i = e[i].next) {
            int v = e[i].v;
            if (d[u] + e[i].w < d[v]) {
                d[v] = d[u] + e[i].w;
                if (!inq[v]) {
                    q.push(v);
                    inq[v] = true;
                }
            }
        }
    }
}

完整代碼:

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
struct edge{
    int v,w,fail;
    edge(){}
    edge(int _v,int _w,int _fail){
        v=_v;
        w=_w;
        fail=_fail;
    }
}e[M<<1];
int head[N],len;
void init(){
    memset(head,-1,sizeof(head));
    len=0;
}
void add(int u,int v,int w){
    e[len]=edge(v,w,head[u]);
    head[u]=len++;
}
void add2(int u,int v,int w){
    add(u,v,w);
    add(v,u,w);
}

int n, m;
int dis[N];
bool vis[N];
void spfa(int u){
    memset(vis,false,sizeof(vis));
    vis[u]=true;
    memset(dis,0x3f,sizeof(dis));
    dis[u]=0;
    queue<int> q;
    q.push(u);
    while(!q.empty()){
        u=q.front();
        q.pop();
        vis[u]=false;
        for(int j=head[u];~j;j=e[j].fail){
            int v=e[j].v;
            int w=e[j].w;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                if(!vis[v]){
                    q.push(v);
                    vis[v]=true;
                }
            }
        }
    }
}
int main() {
    init();
    int u,v,w;
    cin>>n>>m;
    while(m--){
        cin>>u>>v>>w;
        add2(u,v,w);
    }
    spfa(1);
    cout<<dis[n]<<endl;
    return 0;
}

SPFA判斷負環
在進行spfa時,用一個cnt數組記錄每個頂點的入隊次數,如果一個頂點入隊次數大于頂點總數n,則可以判斷存在負環。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;

struct edge {
    int v, w, fail;
    edge() {}
    edge(int _v, int _w, int _fail) {
        v = _v;
        w = _w;
        fail = _fail;
    }
} e[M << 1];
int head[N], len;
void init() {
    memset(head, -1, sizeof(head));
    len = 0;
}
void add(int u, int v, int w) {
    e[len] = edge(v, w, head[u]);
    head[u] = len++;
}
void add2(int u, int v, int w) {
    add(u, v, w);
    add(v, u, w);
}
int n, m;
int dis[N],in[N];
bool vis[N];
bool spfa(int u){
    memset(vis,false,sizeof(vis));
    vis[u]=true;
    memset(dis,0x3f,sizeof(dis));
    dis[u]=0;
    memset(in,0,sizeof in);
    in[u]=1;
    queue<int> q;
    q.push(u);
    while(!q.empty()){
        u=q.front();
        q.pop();
        vis[u]=false;
        for(int j=head[u];~j;j=e[j].fail){
            int v=e[j].v;
            int w=e[j].w;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                if(!vis[v]){
                    q.push(v);
                    vis[v]=true;
                    ++in[v];
                    if(in[v]>n){
                        return true;
                    }
                }
            }
        }
    }
    return false;
}
int main() {
    init();
    int u, v, w;
    cin >> n >> m;
    while (m--) {
        cin >> u >> v >> w;
        add2(u, v, w);
    }
    if(spfa(1)){
        cout<<"yes"<<endl;
    }else{
        cout<<"no"<<endl;
    }
    return 0;
}

Floyd多源最短路算法

是一種基于動態規劃的最短路算法
dp[k][i][j]是經過1~k點的i到j的最短路,對于k點,其可能經過k點或者不經過k點。
狀態轉移方程:dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j])
代碼:

int g[N][N];  // 鄰接矩陣存圖
int dp[N][N][N];
void floyd(int n) {
    for (int k = 0; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (k == 0) {
                    dp[k][i][j] = g[i][j];
                } else {
                    dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]);
                }
            }
        }
    }    
}

但還可以把k這一維優化掉,使dp數組退化為g數組,代碼:

int g[N][N];
void floyd(int n) {
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
            }
        }
    }    
}

完整代碼:

#include <iostream>
#include <cstring>
using namespace std;
const int N = 101;
int g[N][N];
void floyd(int n){
    for(int k=1;k<=n;++k){
        for(int i=1;i<=n;++i){
            for(int j=1;j<=n;++j){
                g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
            }
        }
    }
}

int main() {
    memset(g,0x3f,sizeof(g));
    for(int i=0;i<N;++i){
        g[i][i]=0;
    }
    int n,m;
    int u,v,w;
    cin>>n>>m;
    while(m--){
        cin>>u>>v>>w;
        g[u][v]=g[v][u]=w;
    }
    floyd(n);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            cout<<g[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

次短路解法

次短路也分為兩種:1.可以經過同一個點 2.不可經過同一個點
第一種用dijkstra時,用兩個dis數組,一個記錄最短路,一個記錄次短路,每次更新時,判斷是否更新最短路與次短路的值。
第二種則枚舉最短路的每條邊,將其去掉,記錄剩下的最短路,取其最小的一個就是答案。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
using namespace std;
int n,m;
struct node{
    int x,y;
}nodes[300];
struct edge{
    int v,fail;
    double w;
    edge(){}
    edge(int _v,double _w,int _fail){
        v=_v;
        w=_w;
        fail=_fail;
    }
}e[50000];
int head[50000],len;
void init(){
    memset(head,-1,sizeof(head));
    len=0;
}
void insert(int u,int v,double w){
    e[len]=edge(v,w,head[u]);
    head[u]=len++;
}
void insert2(int u,int v,double w){
    insert(u,v,w);
    insert(v,u,w);
}
double dis1[300],dis2[300];
bool vis1[300],vis2[300];
void dijkstra(int u){
    fill(dis1,dis1+n+1,1000100);
    fill(dis2,dis2+n+1,1000100);
    memset(vis1,false,sizeof(vis1));
    memset(vis2,false,sizeof(vis2));
    dis1[u]=0;
    for(int i=1;i<=2*n;++i){
        double mint=1000000;
        int v=-1,k;
        for(int j=1;j<=n;++j){
            if(!vis1[j]&&dis1[j]<mint){
                mint=dis1[j];
                k=1;
                v=j;
            }else if(!vis2[j]&&dis2[j]<mint){
                mint=dis2[j];
                k=2;
                v=j;
            }
        }
        if(v==-1)return;
        k==1?vis1[v]=true:vis2[v]=true;
        for(int j=head[v];~j;j=e[j].fail){
            int to=e[j].v;
            double w=e[j].w;
            if(dis1[to]>mint+w){
                dis2[to]=dis1[to];
                dis1[to]=mint+w;
            }else if(dis2[to]>mint+w)dis2[to]=mint+w;
        }
    }
}
int main(){
    init();
    cin>>n>>m;
    int x,y;
    for(int i=1;i<=n;++i){
        cin>>x>>y;
        nodes[i]={x,y};
    }
    int u,v;
    for(int i=1;i<=m;++i){
        cin>>u>>v;
        double d=sqrt(pow(nodes[u].x-nodes[v].x,2)+pow(nodes[u].y-nodes[v].y,2));
        insert2(u,v,d);
    }
    dijkstra(1);
    printf("%.2lf\n",dis2[n]);
    return 0;
}

4.線段樹

一類問題可以抽象成n個數a1....an
1.查詢[l,r]中最小的數
2.修改ai為x
解決這類問題,引入一個高級數據結構:線段樹
用一棵二叉樹來表示線段樹,每個節點代表一個區間,每個非葉子結點都有左右兩個子樹,對每個結點,左結點編號2i,右結點編號2i+1,對于每一個結點如果其表示區間[l,r]。如果l=r,那么是一個葉子結點,否則令mid=(l+r)/2,左兒子為[l,mid],右兒子為[mid+1,r]
線段樹的構建是個遞歸的過程,父節點的信息需要子節點去更新,所以需要遞歸的建立左右子樹,代碼如下:

const int maxn = 10010;
int minv[4 * maxn], a[maxn];
// id 表示結點編號,l, r 表示左右區間
void build(int id, int l, int r) {
    if (l == r) {
        minv[id] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(id << 1, l, mid);
    build(id << 1 | 1, mid + 1, r);
    minv[id] = min(minv[id << 1], minv[id << 1 | 1]);
}

建樹的復雜度為O(n),由于結點編號不一定連續,所以minv數組大小一定要是節點數的四倍,這個經過了嚴格的數學推導。id << 1 | 1代表2*id+1,但是一定不能用+而要用|,因為+的優先級高于位運算。遞歸的建立完左右子樹之后,我們需要用左右子樹的信息更新當前節點,這一步一般稱位pushup。
完整代碼:

#include <iostream>
using namespace std;
const int maxn = 110;
int a[maxn];
int minv[4*maxn];
void pushup(int id){
    minv[id]=min(minv[id<<1],minv[id<<1|1]);
}
void build(int id,int l,int r){
    if(l==r){
        minv[id]=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build(id<<1,l,mid);
    build(id<<1|1,mid+1,r);
    pushup(id);
}
int main() {
    int n;
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>a[i];
    }
    build(1,1,n);
    return 0;
}

實現線段樹的單點更新

更新一次的復雜度為log(n),因為數的深度為log(n)

// 把 x 位置更為成為 v
void update(int id, int l, int r, int x, int v) {
    if (l == r) {
        minv[id] = v;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) {
        update(id << 1, l, mid, x, v);
    } else {
        update(id << 1 | 1, mid + 1, r, x, v);
    }
    pushup(id);
}

實現線段樹的單點查詢

單點查詢和單點更新很像,沿著鏈走到葉子結點就行了。

int query(int id, int l, int r, int x) {
    if (l == r) {
        return minv[id];
    }
    int mid = (l + r) >> 1;
    if (mid <= x) {
        return query(id << 1, l, mid, x);
    } else {
        return query(id << 1 | 1, mid + 1, r, x);
    }
}

區間查詢

對于查詢區間[x,y]就是將結點的區間合并起來,每個子區間完全被查詢區間包含,查詢區間的復雜度也是log(n)

int query(int id, int l, int r, int x, int y) {
    if (x <= l && r <= y) {  // 如果完全包含,直接返回
        return minv[id];
    }
    int mid = (l + r) >> 1;
    int ans = inf;
    if (x <= mid) {
        ans = min(ans, query(id << 1, l, mid, x, y));  // 如果左區間包含,遞歸的查詢左子樹
    }
    if (y > mid) {
        ans = min(ans, query(id << 1 | 1, mid + 1, r, x, y));  // 如果右區間包含,遞歸的查詢右子樹
    }
    return ans;
}

完整代碼:

#include <iostream>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 110;
int a[maxn];
int minv[4 * maxn];
void pushup(int id) {
    minv[id] = min(minv[id << 1], minv[id << 1 | 1]);
}
void build(int id, int l, int r) {
    if (l == r) {
        minv[id] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(id << 1, l, mid);
    build(id << 1 | 1, mid + 1, r);
    pushup(id);
}
void update(int id, int l, int r, int x, int v) {
    if (l == r) {
        minv[id] = v;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid) {
        update(id << 1, l, mid, x, v);
    } else {
        update(id << 1 | 1, mid + 1, r, x, v);
    }
    pushup(id);
}
int query(int id,int l,int r,int x,int y){
    if(x<=l&&r<=y){
        return minv[id];
    }
    int mid=(l+r)>>1;
    int ans=inf;
    if(x<=mid){
        ans=min(ans,query(id<<1,l,mid,x,y));
    }
    if(y>mid){
        ans=min(ans,query(id<<1|1,mid+1,r,x,y));
    }
    return ans;
}
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    build(1, 1, n);
    int q;
    cin >> q;
    for (int i = 0; i < q; ++i) {
        int x, v;
        cin >> x >> v;
        update(1, 1, n, x, v);
    }
    int p;
    cin>>p;
    for(int i=0;i<p;++i){
        int l,r;
        cin>>l>>r;
        cout<<query(1,1,n,l,r)<<endl;
    }
    return 0;
}

斑點蛇

用于記錄區間和

#include <iostream>
#include <string>
using namespace std;
int N,a[50010],minv[200010];

void bulid(int id,int l,int r){
    if(l==r){
        minv[id]=a[l];
        return;
    }
    int mid=(l+r)>>1;
    bulid(id<<1,l,mid);
    bulid(id<<1|1,mid+1,r);
    minv[id]=minv[id<<1]+minv[id<<1|1];
}
void add(int id,int l,int r,int x,int v){
    if(l==r){
        minv[id]+=v;
        return;
    }
    int mid=(l+r)>>1;
    if(mid>=x)add(id<<1,l,mid,x,v);
    else add(id<<1|1,mid+1,r,x,v);
    minv[id]=minv[id<<1]+minv[id<<1|1];
}

void sub(int id,int l,int r,int x,int v){
    if(l==r){
        minv[id]-=v;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid)sub(id<<1,l,mid,x,v);
    else sub(id<<1|1,mid+1,r,x,v);
    minv[id]=minv[id<<1]+minv[id<<1|1];
}

int query(int id,int l,int r,int x,int y){
    if(x<=l&&y>=r){
        return minv[id];
    }
    int mid=(l+r)>>1;
    int ans=0;
    if(x<=mid)ans+=query(id<<1,l,mid,x,y);
    if(y>mid)ans+=query(id<<1|1,mid+1,r,x,y);
    return ans;
}
int main(int argc, char** argv) {
    cin>>N;
    for(int i=1;i<=N;++i){
        cin>>a[i];
    }
    bulid(1,1,N);
    string input="";
    int a,b;
    while(1){
        cin>>input;
        if(input=="End")break;
        cin>>a>>b;
        if(input=="Add"){
            add(1,1,N,a,b);
        }else if(input=="Sub"){
            sub(1,1,N,a,b);
        }else if(input=="Query"){
            cout<<query(1,1,N,a,b)<<endl;
        }
    }
    return 0;
}

5.拓撲排序

拓撲排序本質上是一種bfs,代碼如下:

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAX_N = 100;
const int MAX_M = 10000;
struct edge {
    int v, next;
    int len;
} E[MAX_M];
int p[MAX_N], eid;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    E[eid].v = v;
    E[eid].next = p[u];
    p[u] = eid++;
}
int n,m;
int indegree[MAX_N];
void topo(){
    queue<int> q;
    for(int i=1;i<=n;i++){
        if(indegree[i]==0){
            q.push(i);
        }
    }
    while(!q.empty()){
        int now=q.front();
        cout<<now<<endl;
        q.pop();
        for(int i=p[now];i!=-1;i=E[i].next){
            int v=E[i].v;
            indegree[v]--;
            if(indegree[v]==0){
                q.push(v);
            }
        }
    }
}
int main() {
    init();
    memset(indegree,0,sizeof(indegree));
    cin>>n>>m;
    for(int i=0;i<m;i++){
        int u,v;
        cin>>u>>v;
        insert(u,v);
        indegree[v]++;
    }
    topo();
    return 0;
}

歐拉回路

若圖G中存在一條路徑使其恰好通過G中的每條邊一次,我們稱之為歐拉路徑(半歐拉圖)。若該路是一個環路,我們稱之為歐拉環路(歐拉圖)。
判斷是否是歐拉圖,是否是歐拉回路
歐拉路徑的性質,有且僅有兩個點的入度為奇數
歐拉圖的性質,所有點的入度都為偶數

#include <iostream>
#include <cstring>
using namespace std;
const int MAX_N = 100;
const int MAX_M = 10000;
struct edge {
    int v, next;
    int len;
} E[MAX_M];
int p[MAX_N], eid;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    E[eid].v = v;
    E[eid].next = p[u];
    p[u] = eid++;
}
int n,m;
int degree[MAX_N];
int cnt;
bool vis[MAX_N];
void dfs(int u){
    vis[u]=true;
    cnt++;
    for(int i=p[u];i!=-1;i=E[i].next){
        int v=E[i].v;
        if(!vis[v]){
            dfs(v);
        }
    }
}

void euler(){
    dfs(1);
    if(cnt!=n){
        cout<<"It doesn't have an euler path!"<<endl;
        return;
    }
    int cntodd=0;
    for(int i=1;i<=n;i++){
        if(degree[i]%2==1){
            cntodd++;
        }
    }
    if(cntodd==0){
        cout<<"It has an euler circult"<<endl;
    }else if(cntodd==2){
        cout<<"It has an euler path!"<<endl;
    }else{
        cout<<"It doesn't have an euler path!"<<endl;
    }
}

int main() {
    init();
    memset(degree,0,sizeof(degree));
    cin>>n>>m;
    for(int i=0;i<m;i++){
        int u,v;
        cin>>u>>v;
        insert(u,v);
        insert(v,u);
        degree[u]++;
        degree[v]++;
    }
    euler();
    return 0;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容