A move consists of taking a point (x, y) and transforming it to either (x, x+y) or (x+y, y).
Given a starting point (sx, sy) and a target point (tx, ty), return True if and only if a sequence of moves exists to transform the point (sx, sy) to (tx, ty). Otherwise, return False.
Examples: Input: sx = 1, sy = 1, tx = 3, ty = 5 Output: True Explanation: One series of moves that transforms the starting point to the target is: (1, 1) -> (1, 2) (1, 2) -> (3, 2) (3, 2) -> (3, 5) Input: sx = 1, sy = 1, tx = 2, ty = 2 Output: False Input: sx = 1, sy = 1, tx = 1, ty = 1 Output: True
Note:
sx, sy, tx, tywill all be integers in the range[1, 10^9].
這道題說有一個點的變換方式,就是可以將 (x, y) 變成 (x + y, y) 或 (x, x + y),然后給了我們兩個坐標點,一個是起始點 (sx, sy),一個是目標點 (tx, ty),問利用這種轉換方式能否將起始點轉換為目標點。這題給了一個很大的限定條件,就是所有坐標的數字都是正數,即所有的點都在第一象限里。這是個很大的限定條件,因為這樣的話,坐標數字變換的過程中總是在不斷的變大,因為沒有負數的存在,就不會縮小。這樣的話,只要發現只要起始點中的橫縱坐標有任意一個大於了目標點的橫縱坐標,那么直接返回 false 就可以了。首先,可以先來想想 brute force 怎么做,非常的直截了當,先判斷之前說的返回 false 的兩種情況,再判斷如果起始點等於了目標點,返回 true,然后就是調用兩個遞歸函數了,毫無懸念的 TLE 了,可以參見官方解法貼中的代碼。令博主感到意外的是,這道題的時間要求很苛刻啊,官方解題貼中的前三種解法全都 TLE 了,但是經過分析后發現,前三種方法 TLE 是必然的,因為沒用使用到此題想考察的核心要點。這道題的標簽是 Math,對於博主來說,標記為 Math 的題跟腦筋急轉彎 brainteaser 沒啥區別,因為都很難想出來。再想想為啥 brute force 的解法會超時,如果起始點的數字很小的話,而目標點的數字特別的巨大,那么僅僅通過加法來慢慢的累加到的一個巨大的數,怎么可能不超時。快速累加的高效方法是乘法,但要知道需要累加的個數,就需要用除法來計算,其實我們對累加的個數也不那么感興趣,而是對余數感興趣,那么求余運算就是很高效的方法。求余運算是將數字變小的操作,可以將目標數字縮小,看能否縮小到起始位置,也是符合題意的,只不過此時的變換方式由加法變為了減法而已。
我們的目標是將 tx 和 ty 分別縮小到 sx 和 sy,不可能一步就縮小到位,那么這肯定是一個循環,終止條件是 tx 和 ty 中任意一個小於了 sx 和 sy,在循環內部,想要縮小 tx 或 ty,先縮小兩者中較大的那個,若 tx 大於 ty,可以嘗試縮小 tx,但是如果此時 ty 等於 sy 了話,可以迅速判斷出結果,通過計算此時 tx 和 sx 的差值是否是 ty 的倍數,因為此時 ty 不能改變了,只能縮小 tx,若能通過減去整數倍數個 ty 得到 sx 的,就表示可以到達。如果 ty 不等於 sy 的話,那么直接 tx 對 ty 取余即可。同理,若 ty 大於 tx,我們可以嘗試縮小 ty,但是如果此時 tx 等於 sx 了話,我們可以迅速判斷出結果,通過計算此時 ty 和 sy 的差值是否是 tx 的倍數,如果 tx 不等於 sx 的話,那么直接 ty 對 tx 取余即可。循環退出后檢測起始點和目標點是否相等,參見代碼如下:
解法一:
class Solution { public: bool reachingPoints(int sx, int sy, int tx, int ty) { while (tx >= sx && ty >= sy) { if (tx > ty) { if (ty == sy) return (tx - sx) % ty == 0; tx %= ty; } else { if (tx == sx) return (ty - sy) % tx == 0; else ty %= tx; } } return tx == sx && ty == sy; } };
下面這種解法將沒有在循環內部處理相等的情況,而是無腦縮小 tx 和 ty,最后循環退出后,再來判斷 tx 和 ty 的關系,然后快速的判斷,由於取余運算縮小的太快了,所以博主不認為二者的運行效率能差多少,參見代碼如下:
解法二:
class Solution { public: bool reachingPoints(int sx, int sy, int tx, int ty) { while (tx >= sx && ty >= sy) { if (tx > ty) tx %= ty; else ty %= tx; } if (tx > ty) return tx == sx && ty == sy % sx; return ty == sy && tx == sx % sy; } };
下面這種寫法跟上面的沒有太大的區別,就看起來更加整齊一些,博主有強迫症嗎???不過需要注意的是,需要在最前面加一個判定條件,目標點的橫縱坐標都必須大於等於起始點的橫縱坐標,因為坐標點橫縱坐標都是正數,變換方式是加法,所以只能越加越大,參見代碼如下:
解法三:
class Solution { public: bool reachingPoints(int sx, int sy, int tx, int ty) { if (tx < sx || ty < sy) return false; while (tx >= sx && ty >= sy) { if (tx > ty) tx %= ty; else ty %= tx; } if (tx == sx) return (ty - sy) % sx == 0; if (ty == sy) return (tx - sx) % sy == 0; return false; } };
再來看一種遞歸的寫法,就四行啊,其實就是用取余運算代替加法優化了 brute force 的解法,參見代碼如下:
解法四:
class Solution { public: bool reachingPoints(int sx, int sy, int tx, int ty) { if (tx < sx || ty < sy) return false; if (tx == sx && (ty - sy) % sx == 0) return true; if (ty == sy && (tx - sx) % sy == 0) return true; return reachingPoints(sx, sy, tx % ty, ty % tx); } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/780
參考資料:
https://leetcode.com/problems/reaching-points/solution/
https://leetcode.com/problems/reaching-points/discuss/114726/C++-Simple-iterative.
https://leetcode.com/problems/reaching-points/discuss/122916/concise-c++-solution
https://leetcode.com/problems/reaching-points/discuss/116110/C++-iterative-solution
https://leetcode.com/problems/reaching-points/discuss/114732/Java-Simple-solution-with-explanation
