例20 過河卒
題目描述
如圖1,在棋盤的A點有一個過河卒,需要走到目標B點。卒行走規則:可以向下、或者向右。同時在棋盤上的任一點有一個對方的馬(如圖1的C點),該馬所在的點和所有跳躍一步可達的點稱為對方馬的控制點。例如,圖1中C點上的馬可以控制9個點(圖中的P1,P2,…,P8 和C)。卒不能通過對方馬的控制點。
棋盤用坐標表示,A點(0,0)、B點(n,m)(n,m為不超過50的整數,並由鍵盤輸入),同樣馬的位置坐標通過鍵盤輸入,並約定C<>A,同時C<>B。
編寫程序,計算出卒從A點能夠到達B點的路徑的條數。
圖1 棋盤上的過河卒和對方的控制馬
輸入格式
一行四個數據,分別表示B點坐標和馬的坐標。
輸出格式
一個數據,表示所有的路徑條數。
輸入樣例
6 6 3 3
輸出樣例
6
(1)編程思路。
在棋盤的A點(0,0)的過河卒要到達棋盤上的任意一點,只能從左邊和上邊兩個方向過來。因此,要到達某一點的路徑數,等於和它相鄰的左、上兩個點的路徑數和:
F[i][j] = F[i-1][j] + F[i][j-1]。
可以使用逐列(或逐行)遞推的方法來求出從起始頂點到終點的路徑數目,即使有障礙(將馬的控制點稱為障礙),這一遞推方法也完全適用,只要將到達該點的路徑數目設置為0即可,用F[i][j]表示到達點(i,j)的路徑數目,g[i][j]表示點(i,j)有無障礙,遞推方程如下:
F[0][0] = 1 初始點直接可達。
F[i][0] = F[i-1][0] (i > 0,g[i][0] =0) // 左邊界
F[0][j] = F[0][j-1] (j > 0,g[0][j] = 0) // 上邊界
F[i][j] = F[i-1][j] + F[i][j-1] (i > 0,j > 0,g[x, y] = 0) // 遞推式
(2)源程序。
#include <stdio.h>
int main()
{
int i,j,x,y,n,m,forbidden[51][51];
long int ans[51][51];
int dx[8]={-2,-1,1,2,2,1,-1,-2};
int dy[8]={1,2,2,1,-1,-2,-2,-1};
scanf("%d%d%d%d",&n,&m,&x,&y);
for (i=0;i<=n;i++)
for (j=0;j<=m;j++)
{
forbidden[i][j]=0;
ans[i][j]=0;
}
ans[0][0]=1;
forbidden[x][y]=1;
for (i=0;i<8; i++)
if (x+dx[i]>=0 && x+dx[i]<=n && y+dy[i]>=0 && y+dy[i]<=m)
forbidden[x+dx[i]][y+dy[i]]=1;
for (i=1; i<=n; i++)
if (forbidden[i][0]==0)
ans[i][0]=1;
else break;
for (i=1; i<=m; i++)
if (forbidden[0][i]==0)
ans[0][i]=1;
else break;
for (i=1; i<=n; i++)
for (j=1; j<=m; j++)
if (forbidden[i][j]==0)
ans[i][j]=ans[i-1][j]+ans[i][j-1];
printf("%ld\n",ans[n][m]);
return 0;
}
習題20
20-1 馬的行走路徑
問題描述
設有一個n*m的棋盤(2<=n<=50,2<=m<=50),在棋盤上任一點有一個中國象棋馬,如圖2(a)所示。馬行走的規則為:(1)馬走日字;(2)馬只能向右走,即如圖2(b)所示的4種走法。
編寫一個程序,輸入n和m,找出一條馬從棋盤左下角(1,1)到右上角(n,m)的路徑。例如:輸入n=4、m=4時,輸出路徑 (1,1)->(2,3)->(4,4)。這一路經如圖2(c)所示。若不存在路徑,則輸出"No!"
圖2 棋盤及馬兒的行走
輸入格式
一行兩個數據,表示終點的位置坐標。
輸出格式
一條可行的行走路徑。如果可行的行走路徑有多條,任意輸出一條即可。若不存在路徑,則輸出"No!"。
輸入樣例
10 10
輸出樣例
(1,1)->(2,3)->(3,5)->(4,7)->(5,9)->(6,7)->(7,9)->(9,8)->(10,10)
(1)編程思路
先將棋盤的橫坐標規定為i,縱坐標規定為j,對於一個n×m的棋盤,i的值從1到n,j的值從1到m。棋盤上的任意點都可以用坐標(i,j)表示。
對於馬的移動方法,用變量k來表示四種移動方向(1、2、3、4);而每種移動方法用偏移值來表示,並將這些偏移值分別保存在數組dx和dy中,如表1所示。
表1 4種移動方法對應偏移值
K |
Dx[k] |
Dy[k] |
1 |
2 |
1 |
2 |
2 |
-1 |
3 |
1 |
2 |
4 |
1 |
-2 |
根據馬走的規則,馬可以由(i-dx[k],j-dy[k])走到(i,j)。只要馬能從(1,1)走到(i-dx[k],j-dy[k]),就一定能走到(i,j),當然,馬走的坐標必須保證在棋盤上。
以(n,m)為起點向左遞推,當遞推到(i-dx[k],j-dy[k])的位置是(1,1)時,就找到了一條從(1,1)到(n,m)的路徑。
程序中可用一個二維數組a表示棋盤,使用倒推法,從終點(n,m)往左遞推,設初始值a[n][m]為(-1,-1)(表示終點),如果從(i,j)一步能走到(n,m),就將(n,m)存放在a[i][j]中。如表2所示,a[3][2]和a[2][3]的值是(4,4),表示從這兩個點都可以到達坐標(4,4)。從(1,1)可到達(2,3)、(3,2)兩個點,所以a[1][1]存放兩個點中的任意一個即可。遞推結束以后,如果a[1][1]值為(0,0)說明不存在路徑,否則a[1][1]值就是馬走下一步的坐標,以此順推輸出路徑。
表2 N=4,M=4時,數組a的賦值情況
|
|
|
A[4][4]={-1,-1} |
|
A[2][3]={4,4} |
|
|
|
|
A[3][2]={4,4} |
|
A[1][1]={2,3} |
|
|
|
(2)源程序。
#include <stdio.h>
int main()
{
int dx[5]={0,2,2,1,1},dy[5]={0,1,-1,2,-2};
struct point
{
int x;
int y;
};
struct point a[51][51];
int i,j,n,m,k;
for(i=0;i<51;i++)
for (j=0;j<51;j++)
a[i][j].x=a[i][j].y=0;
scanf("%d%d",&n,&m);
a[n][m].x=-1; // 標記為終點
a[n][m].y=-1;
for (i=n;i>=2;i--) // 倒推
for (j=1;j<=m;j++)
if (a[i][j].x!=0)
for (k=1;k<=4;k++)
{
a[i-dx[k]][j-dy[k]].x=i;
a[i-dx[k]][j-dy[k]].y=j;
}
if (a[1][1].x==0)
printf("No!\n");
else // 存在路徑
{
i=1; j=1;
printf("(%d,%d)",i,j);
while (a[i][j].x!=-1)
{
k=i;
i=a[i][j].x; j=a[k][j].y;
printf("->(%d,%d)",i,j);
}
printf("\n");
}
return 0;
}
20-2 方格取數(一)
題目描述
設有N×N的方格圖(N≤9),我們將其中的某些方格中填入正整數,而其他的方格中則放入數字0。如下所示(見樣例):
某人從圖的左上角的A點出發,可以向下行走,也可以向右走,直到到達右下角的B點。在走過的路上,他可以取走方格中的數(取走后的方格中將變為數字0)。
此人從A點走到B點,試找出1條這樣的路徑,使得取得的數之和為最大。
輸入格式
輸入的第一行為一個整數N(表示N×N的方格圖),接下來的每行有三個整數,前兩個表示位置,第三個數為該位置上所放的數。一行單獨的0表示輸入結束。
輸出格式
只需輸出一個整數,表示找出的1條路徑上取得的最大的和。
輸入樣例
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
輸出樣例
36
(1)編程思路。
因為行走的方向是:可以向下行走,也可以向右走。因此,位置(i,j)可以由上邊的格子(i-1,j)走到,也可以由左邊的格子(i,j-1)走到。
設f[i][j]表示走到格子(i,j)處所取方格數的最大值,a[x][y]表示格子(x,y)上的數字。顯然有
f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j];
初始時 f[1][1]=a[1][1]。
(2)源程序。
#include <stdio.h>
int max(int a,int b)
{return a<b?b:a;}
int main()
{
int f[10][10]={0}, a[10][10]={0};
int n;
scanf("%d",&n);
while(1)
{
int x, y, w;
scanf("%d%d%d",&x,&y,&w);
if (x==0 && y==0 && w==0) break;
a[x][y]=w;
}
f[1][1]=a[1][1];
int i, j;
for (i=1;i<=n;i++)
{
for (j=1;j<=n;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j];
}
}
printf("%d\n",f[n][n]);
return 0;
}
20-3 方格取數(二)
題目描述
設有N×N的方格圖(N≤9),我們將其中的某些方格中填入正整數,而其他的方格中則放入數字0。如下所示(見樣例):
某人從圖的左上角的A點出發,可以向下行走,也可以向右走,直到到達右下角的B點。在走過的路上,他可以取走方格中的數(取走后的方格中將變為數字0)。
此人從A點到B點共走兩次,試找出2條這樣的路徑,使得取得的數之和為最大。
輸入格式
輸入的第一行為一個整數N(表示N×N的方格圖),接下來的每行有三個整數,前兩個表示位置,第三個數為該位置上所放的數。一行單獨的0表示輸入結束。
輸出格式
只需輸出一個整數,表示2條路徑上取得的最大的和。
輸入樣例
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
(1)編程思路1。
本題要求找到2條從(1,1)到(n,n)的路徑,被取走的格子里的數變為0,使得在兩條路徑上格子中數之和最大時,就成為了“二取方格數”問題。
最容易想到的就是先后做兩次單條路徑“方格取數”,這一算法的本質是貪心,但這是錯誤的,反例如下:
3 |
4 |
5 |
0 |
0 |
0 |
2 |
8 |
2 |
貪心:第一路徑:3->4->8->2 (17) 第二路徑:5 (5) 總和為22
事實上我們可以將所有的數都取完,總和為24。
解決“二取方格數”問題需要用到“多進程DP”。即解決本題時,由於此人從A點到B點共走兩次,要找出2條這樣的路徑,因此可以考慮為兩個人同時從A走到B。
設f[i][j][k][l]為第一個人走到(i,j),第二個人走到(k,l)時方格取數能達到的最大值,a[x][y]表示格子(x,y)上的數字。
狀態轉移情況如下:
1)兩個人同時向右走
f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k-1][l]+a[i][j]+a[k][l]);
2)兩個人同時向下走
f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k][l-1]+a[i][j]+a[k][l]);
3)兩個人分別向右和向下走
f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k][l-1]+a[i][j]+a[k][l]);
4)兩個人分別向下和向右走
f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k-1][l]+a[i][j]+a[k][l]);
當然,若兩人走到了同一個格子,即(i,j)和(k,l)是同一個點,f[i][j][k][l]值還要減去a[i][j]。
兩個人都走到(n,n)格子時,得到答案,即f[n][n][n][n]為所求。
(2)源程序1。
#include <stdio.h>
int max(int a,int b)
{return a<b?b:a;}
int main()
{
int f[10][10][10][10]={0}, a[10][10]={0};
int n;
scanf("%d",&n);
while(1)
{
int x, y, w;
scanf("%d%d%d",&x,&y,&w);
if (x==0 && y==0 && w==0) break;
a[x][y]=w;
}
f[1][1][1][1]=a[1][1];
int i, j, k, l;
for (i=1;i<=n;i++)
{
for (j=1;j<=n;j++)
for (k=1;k<=n;k++)
for (l=1;l<=n;l++)
{
f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k-1][l]+a[i][j]+a[k][l]);
f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k][l-1]+a[i][j]+a[k][l]);
f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k][l-1]+a[i][j]+a[k][l]);
f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k-1][l]+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;
}
(3)編程思路2。
按思路1的方法,由於狀態總共有n^4種,所以時間復雜度為O(n^4)。
如果讓兩個人同時從(1,1)處出發,並同時向前延伸,那么當兩個人都走了k步,兩條路徑都已經各自包含k個方格時,兩條路徑的末端必同在整個矩陣的第k條對角線上。如下圖3所示。

圖3 行走對角線示意圖
由圖3可知,走1步可到達(1,1)格子(標注為2),走兩步可到達(1,2)或(2,1)格子(標注為2),走三步可到達(1,3)、(2,2)或(3,1)格子(標注為4),……。
由圖可知,對於每一條路徑,向右延伸的格子數+向下延伸的格子數=k(定值),也就是末端兩個格子的縱橫坐標之和=k。
所以我們只需要知道兩路徑末端所在的行編號x1,x2以及兩末端所在對角線編號k,就可以確定末端節點的位置(x1,k-x1),(x2,k-x2)。這樣,可以只枚舉對角線、x1和x2。
設狀態f[l][x1][x2]第一個人橫坐標為x1(即第一個路徑末端在第x1行),第二個人橫坐標為x2(即第二路徑末端在第x2行),且兩末端同在第k條對角線上時的最優解。
到達狀態f[l][x1][x2]有有4種可能:
1)第1人從x1的左邊向右到達x1,第2人從x2的左邊向右到達x2,其前一狀態應為f[k-1][x1-1][x2-1];
2)第1人從x1的上邊向下到達x1,第2人從x2的上邊向下到達x2,其前一狀態應為f[k-1][x1][x2];
3)第1人從x1的左邊向右到達x1,第2人從x2的上邊向下到達x2,其前一狀態應為f[k-1][x1-1][x2];
4)第1人從x1的上邊向下到達x1,第2人從x2的左邊向右到達x2,其前一狀態應為f[k-1][x1][x2-1];
這樣,可以得到狀態轉移方程:
tmp=max(max(f[k-1][x1-1][x2-1],f[k-1][x1][x2]),max(f[k-1][x1-1][x2],f[k-1][x1][x2-1]));
f[k][x1][x2]=max(f[k][x1][x2],tmp+a[x1][k-x1]+a[x2][k-x2]);
同樣,如果點(x1,k-x1)和(x2,k-x2)重合了,需要減去一個點中的數(每個點只能取一次)。
(4)源程序2。
#include <stdio.h>
int max(int a,int b)
{return a<b?b:a;}
int main()
{
int f[19][10][10]={0}, a[10][10]={0};
int n;
scanf("%d",&n);
while(1)
{
int x, y, w;
scanf("%d%d%d",&x,&y,&w);
if (x==0 && y==0 && w==0) break;
a[x][y]=w;
}
int d=n*2;
f[2][1][1]=a[1][1];
for (int i=3;i<=d;i++)
{
int c=i<n+1?i:n+1;
int s=i>n?i-n:1;
for(int j=s;j<c;j++)
for(int k=s;k<c;k++)
{
int x1=j,x2=k,y1=i-j,y2=i-k;
int tmp=max(max(f[i-1][x1-1][x2-1],f[i-1][x1][x2]),
max(f[i-1][x1-1][x2],f[i-1][x1][x2-1]));
f[i][x1][x2]=max(f[i][x1][x2],tmp+a[x1][y1]+a[x2][y2]);
if (x1==x2&&y1==y2) f[i][x1][x2]=f[i][x1][x2]-a[x1][y1];
}
}
printf("%d\n",f[d][n][n]);
return 0;
}