一、题目描述
:::info
设有 N×N 的方格图 (N≤9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0。如下图所示(见样例):
A  
 0   0  0   0  0  0  0  0  
 0   0 13  0  0  6  0  0  
 0   0  0   0  7  0  0  0  
 0   0  0 14  0  0  0  0  
 0 21  0   0  0  4  0  0  
 0  0 15   0  0  0  0  0  
 0 14  0   0  0  0  0  0  
 0  0   0   0  0  0  0  0
                                  B 
某人从图的左上角的 A 点出发,可以向下行走,也可以向右走,直到到达右下角的 B 点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0)。
此人从 A 点到 B 点共走两次,试找出 2 条这样的路径,使得取得的数之和为最大。
输入格式
输入的第一行为一个整数 N(表示 N×N 的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的 0 表示输入结束。
输出格式
输入输出样例
输入 
8 
2 3 13
2 6  6
3 5  7
4 4 14
5 2 21
5 6  4
6 3 15
7 2 14
0 0  0 
输出
67
:::
二、深度优先搜索
首先这道题用两遍dp是不行的,有的情况下会出现 wrong answer 的情况。由于数据的量级不大,我直接用dfs 的方式搜索出最终答案。
// 方格取数#include<cstdio>#include<cstring>#include<vector>using namespace std;#define MAX(A,B) (((A)>(B))?(A):(B))#define Init 0#define First 1#define Second 2#define None 3struct route_num{int _value;route_num(): _value(0) {}route_num(int v): _value(v) {}};route_num operator+(const route_num & r, const int & i){return (r._value+i>3)?3:(r._value+i);}bool operator==(const route_num & r, const route_num & s){return r._value==s._value;}bool operator==(const route_num & r, const int & s){return r._value==s;}struct coordinate{int i;int j;int value;route_num curr_route;coordinate(): i(0), j(0), value(0), curr_route() {}coordinate(int ii, int jj, int vv): i(ii), j(jj), value(vv), curr_route() {}};bool operator==(const coordinate & c1, const coordinate & c2){return (c1.i==c2.i)&&(c1.j==c2.j);}bool i_less(const coordinate & c1, const coordinate & c2){return (c1.i<c2.i);}bool j_less(const coordinate & c1, const coordinate & c2){return (c1.j<c2.j);}struct route{vector<coordinate> coordinate_list;bool insert(coordinate &c){if(coordinate_list.size()==0){coordinate_list.push_back(c);return true;}int c_pos = coordinate_list.size();for(int i=0; i<coordinate_list.size(); i++){if(!i_less(coordinate_list[i], c)){c_pos = i+1;}}if(c.j>=coordinate_list[c_pos-1].j){coordinate_list.insert(coordinate_list.begin()+c_pos, c);return true;}return false;}bool pop(coordinate &c){bool done = false;for(int i=0; i<coordinate_list.size(); i++){if(coordinate_list[i]==c){coordinate_list.erase(coordinate_list.begin()+i);done = true;break;}}return done;}void display(){printf("-------------\n");for(auto c: coordinate_list){printf("i=%d, j=%d, value=%d\n", c.i, c.j, c.value);}printf("--------------\n");}};int Find_Max_Route(vector<coordinate> &v){route route_a,route_b;int max_value = 0;int value = 0;int i=0;while(i>=0){if(i==v.size()){i--;}if(v[i].curr_route == First){if(route_a.pop(v[i])){value -= v[i].value;}} else if(v[i].curr_route == Second){if(route_b.pop(v[i])){value -= v[i].value;}} else if(v[i].curr_route == None){v[i].curr_route = Init;i--;continue;}v[i].curr_route = v[i].curr_route + 1;if(v[i].curr_route == First){if(route_a.insert(v[i])){//成功安置value += v[i].value;max_value = MAX(max_value, value);i++;}} else if(v[i].curr_route == Second){if(route_b.insert(v[i])){//成功安置value += v[i].value;max_value = MAX(max_value, value);i++;}} else{i++;}}return max_value;}int main(){// freopen("lg_p1004.test_data", "r", stdin);int n;scanf("%d", &n);vector<coordinate> ground;int i, j, k;while(scanf("%d %d %d", &i, &j, &k)){if(i==0&&j==0&&k==0){break;}ground.push_back(coordinate(i,j,k));}int a = Find_Max_Route(ground);printf("%d\n", a);return 0;}
三、四维DP
结果看其他人的题解时,才发现这还是一道 dp 题,只不过并不是对二维数组做两遍 dp, 而是对一个四维的数组做一遍 dp。大概思路如下:
dp[i][j][k][l] 代表 第一条路径抵达点 ( i , j ),第二条路径抵达点 ( k , l ) 时的最优解。因为在只考虑一条线路的时候,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。所以我们可以很自然地得到两条线路同时 dp 的状态转移方程:
dp[i][j][k][l] = max(dp[i-1][j][k-1][l], dp[i-1][j][k][l-1], dp[i][j-1][k-1][l], dp[i][j-1][k][l-1])
且当 ( i , j ) 等于 ( k , l ) 时,我们要再减去 value[i][j]。因为题目中存放在方格中的数只能被取一次。
完整代码如下:
#include<iostream>#include<cstdio>#include<algorithm>using namespace std;int f[12][12][12][12],a[12][12],n,x,y,z;int main() {cin>>n>>x>>y>>z;while(x!=0||y!=0||z!=0){a[x][y]=z;cin>>x>>y>>z;}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(max(f[i-1][j][k-1][l],f[i-1][j][k][l-1]),max(f[i][j-1][k-1][l],f[i][j-1][k][l-1]))+a[i][j]+a[k][l];if(i==k&&l==j)f[i][j][k][l]-=a[i][j];}}}}cout<<f[n][n][n][n];return 0;}
四、费用流
还看到了一个费用流的解法,之前没接触过相关的概念,实在是看不懂代码里面在写什么,先摘抄过来 mark 一下。
( 本节内容 copy 自 https://www.luogu.com.cn/blog/chenyy/solution-p1004 )
看了下题解还没有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 0x7f7f7f7fusing 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;}
