1. 程式人生 > >演算法訓練 Lift and Throw (DFS && 位運算)

演算法訓練 Lift and Throw (DFS && 位運算)

問題描述
  給定一條標有整點(1, 2, 3, …)的射線. 定義兩個點之間的距離為其下標之差的絕對值.
  Laharl, Etna, Flonne一開始在這條射線上不同的三個點, 他們希望其中某個人能夠到達下標最大的點.
  每個角色只能進行下面的3種操作, 且每種操作不能每人不能進行超過一次.
  1.移動一定的距離
  2.把另一個角色高舉過頭
  3.將舉在頭上的角色扔出一段距離
  每個角色有一個movement range引數, 他們只能移動到沒有人的位置, 並且起點和終點的距離不超過movement range.
  如果角色A和另一個角色B距離為1, 並且角色B沒有被別的角色舉起, 那麼A就能舉起B. 同時, B會移動到A的位置,B原來所佔的位置變為沒有人的位置. 被舉起的角色不能進行任何操作, 舉起別人的角色不能移動.同時, 每個角色還有一個throwing range引數, 即他能把舉起的角色扔出的最遠的距離. 注意, 一個角色只能被扔到沒有別的角色佔據的位置. 我們認為一個角色舉起另一個同樣舉起一個角色的角色是允許的. 這種情況下會出現3個人在同一個位置的情況. 根據前面的描述, 這種情況下上面的兩個角色不能進行任何操作, 而最下面的角色可以同時扔出上面的兩個角色. 你的任務是計算這些角色能夠到達的位置的最大下標, 即最大的數字x, 使得存在一個角色能夠到達x.
輸入格式
  輸入共三行, 分別為Laharl, Etna, Floone的資訊.
  每一行有且僅有3個整數, 描述對應角色的初始位置, movement range, throwing range.
  資料保證3個角色的初始位置兩兩不相同且所有的數字都在1到10之間.
輸出格式
  僅有1個整數, 即Laharl, Etna, Flonne之一能到達的最大距離.
樣例輸入
9 3 3
4 3 1
2 3 3
樣例輸出
15
樣例說明
  一開始Laharl在位置9, Etna在位置4, Flonne在位置2.
  首先, Laharl移動到6.
  然後Flonne移動到位置5並且舉起Etna.
  Laharl舉起Flonne將其扔到位置9.
  Flonne把Etna扔到位置12.
  Etna移動到位置15.

做這道題,我煞費苦心,可是卻一無所獲,毫無頭緒,糾結了許久,只是知道一點——標記,將每個狀態都表示出來。起初,關於這個標記,我只想到了一種最直觀的方法(一會兒講技巧——位運算標記法,當然這是我自定義的名字),用多個數組分別標記每個動作的狀態。如下:

int initialPlace[4], movement[4], throwing[4], tag[4], tagOne[4], tagTwo[4], tagThree[4], tagFour[4];
// 初始位置 可移動距離 可丟擲距離 操作? 操作一? 操作二? 操作三? 舉?
// 1-10 1-10 1-10 [ 1為可操作,0為不可操作 ] 0未舉,1-3
這種方式既繁瑣,又缺乏關聯性,做起題來十分乏力。

思考了許久沒有結果,最後,還是一位擅長搜尋資源的學長幫我找到了一個不錯的程式碼,這個程式碼極其精妙,再一次印證了一句話,沒有做不到的,只有想不到的,當然這個程式碼我拿到手的時候是個沒有註釋的程式碼,我費盡周折才從本質解讀了這段程式碼的演算法(眾所周知,越是精妙的演算法,可讀性越差,當然有沒有註釋也會有很大的差距)。

接下來,就該先分享一下程式碼了:

#include <stdio.h>
#include <string.h>
#define TRUE 1
#define FALSE 0
#define max(a, b) a > b ? a : b
//定義陣列大小為4,從一開始,空出下標為0,方便計算
int x[4]; //三個人的位置 int l[4]; //三個人的機動性(可移動距離) int t[4]; //三個人的拋的距離 int ans = 0; //經過操作後的最遠距離,初始化為0 int w[4]; //初始化為0,0表示可以進行操作,非零表示不可以 int p[4]; //初始化為0,表示a[i]所舉起的人 int a[4] = {3, 3, 3, 3}; //初始化為3,表人的狀態,這裡a對應的二進位制為0011,後三位分別是三個動作:丟擲,舉起,移動。0(無意義)0(不可丟擲)1(未進行舉起)1(未進行移動)。這道題中,a只有六個可能值:0(0000)、1(0001)、2(0010)、3(0011)、4(0100)、5(0101),表示人的六種狀態 //bool型別 int near(int s) { int i = 1; for (; i <= 3; i++) { if (s == x[i] + 1 || s == x[i] - 1) { return TRUE; } } return FALSE; } //dfs深度遍歷 void dfs(int d) { int i = 1, j = 1, e = 0; //每次都取最遠(大)的位置 for (; i <= 3; i++) { ans = max(ans, x[i]); } for (i = 1; i <= 3; i++) { //是否可以進行操作 if (w[i]) { continue; } //a[i] == 1 || a[i] == 3(未進行移動且不可丟擲) if ((a[i] & 1) && !(a[i] & 4)) { for (j = 1; j <= l[i]; j++) //移動 { x[i] += j; //a[i]向前移動j a[i] ^= 1; //已移動 if (near(x[i]) || j == l[i]) //如果a[i]移動後的位置旁邊有人或者移動距離達到上限 { dfs(d + 1); } x[i] -= j; //歸位 x[i] -= j; //a[i]向後移動j if (near(x[i]) || j == l[i]) //如果a[i]移動後的位置旁邊有人或者移動距離達到上限 { dfs(d + 1); } x[i] += j; //歸位 a[i] ^= 1; //還原為未移動 } } //a[i] == 2 || a[i] == 3 || a[i] == 5(未進行舉起) if (a[i] & 2) { for (j = 1; j <= 3; j++) //舉起 { if (i != j && !w[j] && t[i] > 0) //是否可以進行操作 { if (x[i] == x[j] + 1 || x[j] == x[i] + 1) //a[i]附近是否有人 { w[j] = 1; //即將舉起(丟擲)j,丟擲前將j是否可操作標記變更為否 a[i] ^= 2; //已舉起 a[i] ^= 4; //可丟擲 p[i] = j; //記錄a[i]舉起(丟擲)了j e = x[j]; //記錄a[j]的舉起前位置 x[j] = -j; //a[j](被舉起)的位置定為負數,只作用於下一層遞迴時的取最遠位置的迴圈 dfs(d + 1); x[j] = e; //歸位 w[j] = 0; //還原為可以進行操作 a[i] ^= 2; //還原為未舉起 a[i] ^= 4; //還原為不可丟擲 } } } } //a[i] == 4 || a[i] == 5(可丟擲) if (a[i] & 4) { for (j = 1; j <= t[i]; j++) //丟擲 { w[p[i]] = 0; //變更a[j]為可操作(以下a[j]指a[i]所舉起的人) a[i] ^= 4; //不可丟擲 e = x[p[i]]; //記錄a[j]被舉起前位置 x[p[i]] = x[i] + j; //丟擲a[j],並更新a[j]位置 if (near(x[p[i]]) || j == t[i]) //如果a[j]被丟擲後的位置旁邊有人或者丟擲距離達到上限 { dfs(d + 1); } x[p[i]] -= j; //歸位 x[p[i]] -= j; //a[j]向後丟擲j if (near(x[p[i]]) || j == t[i]) //如果a[j]被丟擲後的位置旁邊有人或者丟擲距離達到上限 { dfs(d + 1); } x[p[i]] = e; //還原a[j]為未舉起前的位置 a[i] ^= 4; //還原a[j]為可丟擲 w[p[i]] = 1; //還原a[j]為不可操作 } } } return ; } int main() { int i = 1; //鍵入每個人的資訊 for (; i <= 3; i++) { scanf("%d %d %d", &x[i], &l[i], &t[i]); } //深度優先遍歷 dfs(1); //輸出最遠距離 printf("%d\n", ans); return 0; }

這裡,我要說的有兩點,第一:dfs遠比我想象中的要靈活的多,如果不是看了這段程式碼,我真不曉得dfs也可以這樣多變,這樣靈活。一開始根本就不知道這道題如何去用dfs實現它;第二:那就是位運算標記法。上邊我的標記的方法已經說過了有很多缺點,唯一的優點就是容易理解,而現在要說的這種標記方法和我用的第一種方法剛好相反,除了不易理解外,其他各方面都十分優異。這裡用a[1]、a[2]、a[3]表示三個人的狀態,每個人的狀態又分為六種,即:0(0000),1(0001),2(0010),3(0011),4(0100),5(0101)。

這裡我們對其進行初始化,初始化為3,即0011(轉換成二進位制後的,因為要用到位運算,所以涉及到二進位制),從這個二進位制串中,我們可以解讀到我們所需要的資訊——標記。0(無實際意義,為了方便看所以加上的0)0(不可丟擲,若為1,則可丟擲)1(未進行舉起,若為0,則已經進行舉起)1(未進行移動,若為1,則已經進行移動),這裡需要強調,只有最後兩位是標記是否進行過,而倒數第三位則是標記是否可以進行丟擲,之所以這樣標記,是因為丟擲和舉起是連鎖的操作關係,我們可以通過標記是否進行舉起來鎖定丟擲次數,而倒數第三位是否可以丟擲則可以告訴我們其是否滿足丟擲的必要條件——頭上有人(這裡我們知道,就算沒有進行過丟擲,我們也不一定可以進行丟擲動作,必須頭上有人才可以進行丟擲動作)。故我們這樣來定義一個人的三個操作的狀態。

我們在程式碼實現的過程中可以通過位運算來實現每個操作狀態的判斷,並且我們還有一個十分重要的標記w[i],它標記了這個人是否可以進行操作,相當於一個總的標記,表示一個人是否被舉起,如果被舉起,則不可進行操作,否則可以進行操作。

最後細心的你可能會有些疑惑,為什麼這裡沒有考慮如果一個人移動或者被拋到另一個有人的位置上的情況,那我就要麻煩你和我一起思考了,我們的深度優先遍歷的條件是(near(x[i]) || j == l[i]) || (x[i] == x[j] + 1 || x[j] == x[i] + 1),也就是說,我們舉起並丟擲的那個人必須是我們相鄰的人,而不是重疊的人,那麼我們縱使不處理重疊位置的情況,其依然可以不影響到最終的結果,所以我們可以不予考慮,導致畫蛇添足。

綜上所述,為個人對這道題的DFS演算法的解讀,十分敬佩能寫出這樣精妙演算法的人和能靈活應用位運算的人。我需要好好學習,爭取儘快學習到這些程式設計的精髓之所在。

OVER!!!