咱們作題的思路能夠這樣:ios
①先看一下出題日期(畢竟是NOIP的題目,有必定的水準),而後發現是2000年的普及第四題c++
咱們要知道的是,好像比較前面的幾年因爲1999的數塔IOI問題後,接下來幾年的最後一兩題都很喜歡出DP算法
因此,咱們首先看一下題目的內容,求路徑最大的方法,這時候就要想到DP或者DFS數組
②而後咱們發現題目的數據規模不大,n<=9,因此咱們能夠考慮用DFS或者DP均可以markdown
可是鑑於 "好像比較前面的幾年因爲1999的數塔IOI問題後,接下來幾年的最後一兩題都很喜歡出DP "網絡
咱們以爲用DP會比較好ide
③並且,NOIP的壓軸DP題你想要2維過(在考場上是很難想出來的)函數
因此咱們考慮高維spa
④咱們找到一個東西叫作四維DP,由於這題是兩我的走,咱們思考一下能不能單純用兩我的的模擬過呢?code
顯然是能夠的,咱們記f[i][j][k][l]表示第1條路線的i,j走法和第2條路線的k,l走法
顯然咱們能夠兩我的一塊兒走,複雜度最多就是9*9*9*9=6561(哈哈哈時間複雜度這麼低)
因此咱們就用這個方法了!
⑤而後咱們思考動歸方程的寫法:
第1條路線只多是從i-1,j或者i,j-1轉移,第2條路線也只可能從k-1,l或者k,l-1轉移
並且由於是2我的走,若是走到一點咱們的那個點就要打標記說那點上面的值爲0
因此咱們獲得了咱們的動歸方程(注意:萬一i,j與k,l相同這是要當心的!)
f[i][j][k][l]=max(f[i-1][j][k-1][l],f[i][j-1][k-1][l],f[i-1][j][k][l-1],f[i][j-1][k][l-1])+a[i][j]+a[k][l];
#include<bits/stdc++.h> using namespace std; int n,x,y,val,ans=0,maxn,f[12][12][12][12],a[12][12];//a[i][j][k][l]表示兩我的同時走,一個走i,j 一個走k,l int main(){ scanf("%d",&n); memset(a,0,sizeof a); while(1){ scanf("%d%d%d",&x,&y,&val); if(x==0&&y==0&&val==0)break; a[x][y]=val; } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ for(int k=1;k<=n;k++){ for(int l=1;l<=n;l++){ f[i][j][k][l]=max(f[i-1][j][k-1][l],max(f[i][j-1][k-1][l],max(f[i-1][j][k][l-1],f[i][j-1][k][l-1])))+a[i][j]+a[k][l]; if(i==k&&j==l)f[i][j][k][l]-=a[i][j]; } } } } printf("%d\n",f[n][n][n][n]); return 0; }
看了下題解尚未spfa的費用流解法,就本身發一份了。來自一位不會動規的蒟蒻。
簡要介紹一下如何構圖
拆點:由於每一個方格只取一次,但要走兩遍,所以咱們考慮對於矩陣中每一個方格拆爲兩個節點,一個爲流入點,記爲i;一個爲流出點,記爲i'。連兩條邊從i->i’,兩條容量都爲1,費用爲-g[i][j]和0。
編號:這個你們有各自的習慣。個人題解中具體看我程序中的hashin和hashout函數和註釋,hashin用於編號我前文所提到的i,hashout用於編號我前文所提到的i'。
鏈接節點:每一個節點的out鏈接它的右邊和它下邊節點的流入點,對於邊界特殊處理一下,s連(0,0)的入點,(n-1,n-1)連t點。
這樣構圖跑一遍spfa的最小費用最大流就OK了。
#include <cstdio> #include <cstring> #include <queue> #define INF 0x7f7f7f7f using namespace std; struct Edge{ int u;//大多數算法在鄰接表中並不須要這個,但費用流比較例外 int v; int f;//殘量 int c;//費用 int next; }e[850];//網絡流的題目都要記得邊數開兩倍,由於還有反向弧 int head[170]; int n,m,s,t; int ecnt = 0; inline void AddEdge(int _u,int _v,int _f,int _c) { e[ecnt].next = head[_u]; head[_u] = ecnt; e[ecnt].u = _u; e[ecnt].v = _v; e[ecnt].f = _f; e[ecnt].c = _c; ecnt++; } inline void Add(int _u,int _v,int _f,int _c) { AddEdge(_u,_v,_f,_c); AddEdge(_v,_u,0,-_c); } int dis[170]; bool inq[170]; int pre[170]; bool SPFA() { queue <int> q; q.push(s); memset(dis,0x7f,sizeof(dis)); memset(inq,0,sizeof(inq)); memset(pre,-1,sizeof(pre)); inq[s] = true; dis[s] = 0; while (!q.empty()) { int cur = q.front(); q.pop(); inq[cur] = false; for (int i = head[cur];i != -1;i = e[i].next) { if (e[i].f != 0 && dis[e[i].v] > dis[cur] + e[i].c) { dis[e[i].v] = dis[cur] + e[i].c; pre[e[i].v] = i; if (!inq[e[i].v]) { inq[e[i].v] = true; q.push(e[i].v); } } } } return dis[t] != INF; } void MICMAF(int &flow,int &cost) { flow = 0; cost = 0; while (SPFA()) { int minF = INF; for (int i=pre[t];i != -1;i=pre[e[i].u]) minF = min(minF,e[i].f); flow += minF; for (int i=pre[t];i != -1;i=pre[e[i].u]) { e[i].f -= minF; e[i^1].f += minF; } cost += dis[t] * minF; } } /* 節點編號規則: 源點:0 矩陣節點(入):n*x+y+1 矩陣節點(出):n*n+n*x+y+1 匯點:2*n*n+1 */ int g[10][10]; inline int hashin(int x,int y) { return n*x+y+1; } inline int hashout(int x,int y) { return n*n + n * x + y + 1; } int main() { memset(head,-1,sizeof(head)); scanf("%d",&n); int x,y,v; while (scanf("%d%d%d",&x,&y,&v) == 3) { if (x == 0 && y == 0 && v == 0) break; x --; y --; g[x][y] = v; } s = 0; t = 2 * n * n + 1; Add(s,1,2,0); Add(2*n*n,t,2,0); for (int i=0;i<n;i++) for (int j=0;j<n;j++) { int in = hashin(i,j); int out = hashout(i,j); Add(in,out,1,0);//鄰接表中後插入的先遍歷,卡常,f=1是由於只可能再通過一次 Add(in,out,1,-g[i][j]); if (i != n - 1) Add(out,hashin(i+1,j),2,0); if (j != n - 1) Add(out,hashin(i,j+1),2,0); } int f,c; MICMAF(f,c); printf("%d\n",-c); return 0; }
見都是動規的帖子,來來來,貼一個深搜的題解(手動滑稽)。。。
這道題深搜的最優方法就是兩種方案同時從起點出發。由於若是記錄完第一種方案,再計算第二種方案,不可控的因素太多了,大多都不是最優解→_→,但兩種方案同時執行就行,由於這能夠根據當前的狀況來判斷最優。
總的來講,每走一步都會有四個分支(你理解成選擇或者狀況也能夠):
一、兩種都向下走
二、第一種向下走,第二種向右走
三、第一種向右走,第二種向下走
四、兩種都向右走
每走一步走枚舉一下這四種狀況,由於在每一個點的方案具備惟一性(也就是在某個點走到終點的取數方案只有一個最優解,本身理解一下),因此咱們能夠開一個數組來記錄每一種狀況,當重複枚舉到一種狀況時就直接返回(對,就是剪枝),大大節省了時間(否則會超時哦~)。深搜和動歸的時間複雜度時同樣的!
#include<iostream> using namespace std; int N=0; int s[15][15],f[11][11][11][11]; int dfs(int x,int y,int x2,int y2)//兩種方案同時執行,表示當第一種方案走到x,y,第二種方案走到x2,y2時到終點取得的最大數 { if (f[x][y][x2][y2]!=-1) return f[x][y][x2][y2];//若是這種狀況已經被記錄過了,直接返回,節省時間 if (x==N&&y==N&&x2==N&&y2==N) return 0;//若是兩種方案都走到了終點,返回結束 int M=0; //若是兩種方案都不在最後一列,就都往下走,統計取得的數,若是有重複,就減去一部分 if (x<N&&x2<N) M=max(M,dfs(x+1,y,x2+1,y2)+s[x+1][y]+s[x2+1][y2]-s[x+1][y]*(x+1==x2+1&&y==y2)); //若是第一種方案不在最後一行,第二種方案不在最後一列,第一種就向下走,第二種就向右走, //統計取得的數,若是有重複,就減去一部分 if (x<N&&y2<N) M=max(M,dfs(x+1,y,x2,y2+1)+s[x+1][y]+s[x2][y2+1]-s[x+1][y]*(x+1==x2&&y==y2+1)); //若是第一種方案不在最後一列,第二種方案不在最後一行,第一種就向右走,第二種就向下走, //統計取得的數,若是有重複,就減去一部分 if (y<N&&x2<N) M=max(M,dfs(x,y+1,x2+1,y2)+s[x][y+1]+s[x2+1][y2]-s[x][y+1]*(x==x2+1&&y+1==y2)); //若是第一種方案和第二種方案都不在最後一列,就都向右走,統計取得的數,若是有重複,就減去一部分 if (y<N&&y2<N) M=max(M,dfs(x,y+1,x2,y2+1)+s[x][y+1]+s[x2][y2+1]-s[x][y+1]*(x==x2&&y+1==y2+1)); //對最後那個 s[x][y+1]*(x==x2&&y+1==y2+1))的解釋:這個是用來判斷兩種方案是否是走到了同一格的 //若是是真,就返回1,不然返回0,若是是1的話,理所固然的能夠減去s[x][y+1]*1,不然減去s[x][y+1]*0至關於 //不減,寫得有點精簡,省了4個if,見諒哈~ f[x][y][x2][y2]=M;//記錄這種狀況 return M;//返回最大值 } int main() { cin>>N; //將記錄數組初始化成-1,由於可能出現取的數爲0的狀況,若是直接判斷f[x][y][x2][y2]!=0(見dfs第一行) //可能出現死循環而致使超時,細節問題 for(int a=0;a<=N;a++) for(int b=0;b<=N;b++) for(int c=0;c<=N;c++) for(int d=0;d<=N;d++) f[a][b][c][d]=-1; for(;;)//讀入 { int t1=0,t2=0,t3=0; cin>>t1>>t2>>t3; if(t1==0&&t2==0&&t3==0) break; s[t1][t2]=t3; } cout<<dfs(1,1,1,1)+s[1][1];//輸出,由於dfs中沒有考慮第一格,即s[1][1],因此最後要加一下 return 0; }
設兩個起點,總起點向副起點連一條容量爲二,費用爲零的邊(只走兩次)
用結構體存儲每一個費用不爲零的點的信息(id是第幾個被輸入)
每一個費用不爲零的點又分爲入點和出點,入出點之間連一條容量爲一,費用爲當前點權值的邊(取走這個點的值),再連一條容量爲二,費用爲零的邊(不取走這個點的值)
副起點向每一個費用不爲零的入點連一條容量爲inf,費用爲零的邊
每一個費用不爲零的點的出點向終點連一條容量爲inf,費用爲零的邊
每一個費用不爲零的點的出點只須要連與當前點最「近」的點的入點(須要排序)
詳細說明請見下文:
下面來自wjyyy大神題解
對於座標系中一個點,它能夠由橫座標非嚴格小於它,且縱座標非嚴格小於它的點(在可行域中)轉移。咱們爲了控制邊數,只用鏈接與它最近的點。咱們在可行域中首先找到橫座標最大(同等條件下縱座標最大)的點,接着屏蔽掉以原點與這個點的連線爲對角線的矩形,由於矩形中的點均可以或直接或間接地轉移到這個右上角點來:
咱們依次這樣作下去,就會獲得這兩個藍色點和紅色點,從藍點指向紅點是一條邊權爲∞,費用爲0的承接邊。
不過,在某些狀況下,下面剩的兩個黑點直接走到紅點是更優的解,這樣咱們只須要把以前拆的點之間從新建一條邊,邊權爲∞,費用爲0的承接邊,表示不通過這個點的兩點連線經過這個點鏈接到一塊兒,與這個點無關。這樣一來,與上面的拆點一塊兒,每一個點有了兩條自環邊,實則分紅了兩個點,它們之間有兩條連線,一條是承接邊,一條是費用邊,即對費用增長有貢獻的邊。
最後跑最大費用最大流便可
#include<bits/stdc++.h> #define maxn 200000 #define inf INT_MAX using namespace std; struct edge{ int x,y,f,v,next; }e[maxn*10]; bool vis[maxn]; int n,m=1,cnt=0,mc=0; int head[maxn],pre[maxn],sum[maxn]; inline void add(int a,int b,int c,int d){ e[cnt].x=a; e[cnt].y=b; e[cnt].f=c; e[cnt].v=d; e[cnt].next=head[a]; head[a]=cnt++; } inline void ad(int a,int b,int c,int d){ add(a,b,c,d); add(b,a,0,-d); } void init() { cnt=0;memset(head,-1,sizeof(head)); } bool spfa(int s,int t){ queue<int>q; for(int i=0;i<=t+1;i++){ sum[i]=-inf; pre[i]=-1; vis[i]=0; } sum[s]=0; vis[s]=1; q.push(s); while(!q.empty()){ int x=q.front(); q.pop(); vis[x]=0; for(int i=head[x];i!=-1;i=e[i].next){ int y=e[i].y; int f=e[i].f; int v=e[i].v; if(f>0&&sum[y]<sum[x]+v){ pre[y]=i; sum[y]=sum[x]+v; if(!vis[y]){ vis[y]=1; q.push(y); } } } } return sum[t]>0; } void ek(int s,int t){ mc=0; while(spfa(s,t)){ int minn=inf; for(int i=pre[t];i!=-1;i=pre[e[i].x]) minn=min(minn,e[i].f); mc+=sum[t]*minn; for(int i=pre[t];i!=-1;i=pre[e[i].x]){ e[i].f-=minn; e[i^1].f+=minn; } } printf("%d",mc); } struct data{ int x,y,z,id; }bean[2005]; bool cmp(data a,data b){ if(a.x==b.x) return a.y<b.y; return a.x<b.x; } int main() { init(); scanf("%d",&n); int s=0,t,ss; while(scanf("%d%d%d",&bean[m].x,&bean[m].y,&bean[m].z)){ if(bean[m].x==0&&bean[m].y==0&&bean[m].z==0) break; bean[m].id=m; m++; } m--; t=m*2+1; ss=t+1; ad(s,ss,2,0); for(int i=1;i<=m;i++){ ad(ss,i,inf,0); ad(i+m,t,inf,0); ad(i,i+m,1,bean[i].z); ad(i,i+m,2,0); } sort(bean+1,bean+m+1,cmp); for(int i=1;i<m;i++){ int minn=inf; for(int j=i+1;j<=m;j++) if(bean[j].y>=bean[i].y&&bean[j].y<minn){ minn=bean[j].y; ad(m+bean[i].id,bean[j].id,inf,0); } } ek(s,t); }
本題是簡單的雙路動歸。
注意到題目有一個設定,取走後的方格中的數字將變爲0,若是沒有這個設定,則能夠考慮在搜索時求出最大值和次大值,相加便可。
一旦加上這個設定,則意味着對於大於0的格子只能取一次,因此直接在搜索時求最大值和次大值會出現錯誤。
題目要求是走兩次,可是咱們並不必定循序漸進地一次一次地走,能夠兩條線路同時走!
假設某時刻走到(i,j)和(k,l)兩個點,那麼它們的來向就有四種可能:
1.同時從上方過來
2.從上方和左方過來
3.從左方和上方過來
4.同時從左方過來
由此能夠推出狀態轉移方程
sum[i][j][k][l]表示兩個點走到(i,j)和(k,l)時取得的最大值
sum[i][j][k][l]=max( sum[i-1][j][k-1][l], sum[i-1][j][k][l-1], sum[i][j-1][k-1][l], sum[i][j-1][k][l-1]) +fang[i][j]+(i==k&&j==l ? 0 : fang[k][l])
其中fang[i][j]爲方格中有的數
#include<iostream> using namespace std; int fang[15][15],sum[15][15][15][15]; int max_(int a,int b,int c,int d)//max_() 是我手打的一個判斷四個數中最大那個數的那個數的函數 { if(a<b)a=b; if(a<c)a=c; if(a<d)a=d; return a; } int main()//主函數 { int n; cin>>n; int x,y,c; cin>>x>>y>>c; while(x!=0 and y!=0 and c!=0){//讀入,讀到0中止 fang[x][y]=c; cin>>x>>y>>c; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) for(int k=1;k<=n;k++) for(int l=1;l<=n;l++)//四重循環動歸 sum[i][j][k][l]=max_( sum[i-1][j][k-1][l],//1.同時從上方過來 sum[i-1][j][k][l-1],//2.從上方和左方過來 sum[i][j-1][k-1][l],//3.從左方和上方過來 sum[i][j-1][k][l-1])//4.同時從左方過來 +fang[i][j]+(i==k&&j==l ? 0 : fang[k][l]);//若是同時走到同一個點就不重複加了 cout<<sum[n][n][n][n]; return 0; }