一、鏡面反射
題目:
/** * 有一個特殊的正方形房間,每面牆上都有一面鏡子。除西南角以外,每個角落都放有一個接受器,編號爲 0, 1,以及 2。 * 正方形房間的牆壁長度爲 p,一束激光從西南角射出,首先會與東牆相遇,入射點到接收器 0 的距離爲 q 。 * 返回光線最先遇到的接收器的編號(保證光線最終會遇到一個接收器)。 */
示例:
輸入: p = 2, q = 1
輸出: 2
解釋: 這條光線在第一次被反射回左邊的牆時就遇到了接收器 2 。
可以想象沒有背面的牆,東西兩個方向的牆無限長,光線會被不斷反射,我們會發現光線被接收一定是走過的豎直路徑長度是p的整數倍,最終落在西牆那麼肯定就是被2接收,如果落在東牆就會被0和1接收,他倆的接收又有一定規律,如果走過的路徑長度是p的偶數倍那麼就會被0接收,奇數倍就會被1接收。我們根據此寫出程序。
public int method1(int p, int q) {
int pq=0;
for (int i=(p>q?p:q);i<=p*q;i++){
if (i%p==0&&i%q==0){
pq=i;
break;
}
}
if ((pq/p)%2==0){
if ((pq/q)%2==0){
return 2;
}else {
return 0;
}
}else {
if ((pq/q)%2==0){
return 2;
}else {
return 1;
}
}
}
這個算法還有可以優化的地方,在上面這個算法中我們是先求出p,q的最小公倍數,然後再進行判斷,我們可以把它的效率提高一些,因爲最終都是執行了最小公倍數除p除q再對二取餘的判斷,所以我們可以直接分別對p,q除2運算,直到一方對二取餘不等於0,然後再對2取餘結果進行判斷返回
public int method2(int p, int q) {
while(p%2==0&&q%2==0) {
p /= 2;
q /= 2;
}
if(p%2 == 0)
return 2;
if(q%2 == 0)
return 0;
return 1;
}
二、吃巧克力
題目:
/** * 小Q的父母要出差N天,走之前給小Q留下了M塊巧克力。小Q決定每天吃的巧克力數量不少於前一天吃的一半, * 但是他又不想在父母回來之前的某一天沒有巧克力吃,請問他第一天最多能吃多少塊巧克力 * 輸入描述: * 每個輸入包含一個測試用例。 * 每個測試用例的第一行包含兩個正整數,表示父母出差的天數N(N<=50000)和巧克力的數量M(N<=M<=100000)。 * 輸出描述: * 輸出一個數表示小Q第一天最多能吃多少塊巧克力。 * 輸入例子1: * 3 7 * 輸出例子1: * 4 */
這題和猴子吃桃有相似之處,猴子吃桃是每天吃昨天的一半多一個,給天數和最後的數量求開始的數量,這題是吃不少於昨天數量的一半,問第一天最多能吃多少。
重要的條件有兩個:一個是每天吃的巧克力數量不少於前一天的一半 也就是說可以吃前一天的一半 二就是不能在父母回來之前的某一天沒巧克力吃,可以剛好在最後一天吃完。
那麼要想在第一天吃的更多就要想辦法讓後面吃的少一些,儘量壓着最低數量(前一天數量的一半)去吃
那昨天吃的數量是偶數,今天就吃昨天的一半,如果昨天吃的數量是奇數,那就吃前一天數量一半向上取整
然後再對天數進行判別:如果給的天數是0或者1,那麼就可以直接吃完
如果多於一天,那第一天最多吃三分之二的,因爲最低天數爲2的話,第一天吃三分之二,第二天吃三分之一是最佳分配
然後用一個for循環取檢測第一天吃這個數量是否符合要求
public static int getMaxNum(int day, int chocolates) {
if (day < 2) return chocolates;
double maxNum=(chocolates/3.0)*2;
for (; maxNum > 1; maxNum--) {
double currentNum = maxNum, currentChocolates = 0;
for (int i = 0; i < day; i++) {
currentChocolates += currentNum;
currentNum = Math.ceil(currentNum / 2.0);
if (currentChocolates > chocolates) {
break;
}
}
if (currentChocolates <= chocolates) {
return (int) maxNum;
}
}
return 1;
}
但這種方法很低效,要判斷很多無用的數字,在學習基礎查找算法時我們學過二分查找,在這裏可以派上用場,不斷去縮小查找範圍。
public static void method2() {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();//天數
int m = sc.nextInt();//巧克力個數
double min = 0;//設置二分初始低位爲0
double max = (double) m;//設置二分初始高位爲初始巧克力個數m
double stillHas = (double) m;//剩餘巧克力個數
boolean flag = true;
double temp;
while (min < max)//當低位<高位時,開始二分查找
{
temp = Math.ceil((min + max) / 2);//取低位和高位中間的一位向上取整,即爲試探的第一天吃的巧克力數
for (int i = 1; i < n + 1; i++)//循環n天
{
if (temp <= stillHas)//當前天需要吃的巧克力個數<=剩餘巧克力個數時,減少巧克力,同時第二天巧克力個數變爲第一天的一半
{
stillHas = stillHas - temp;
temp = Math.ceil(temp / 2);
} else//當前天需要吃的巧克力個數>剩餘巧克力個數時,說明沒有撐到n天巧克力吃完,置flag=false;跳出循環
{
flag = false;
break;
}
}
if (flag)//flag==true,說明上面的for循環正常循環結束跳出,說明當前的第一天吃的Math.ceil((min+max)/2)個巧克力滿足要求
{
//判斷一下,如果比Math.ceil((min+max)/2)個巧克力大1個巧克力時
//isTrue返回false說明再大1個巧克力就不滿足要求了,那麼當前的Math.ceil((min+max)/2)就是最大的第一天吃的巧克力數,輸出即可
if (!isTrue(n, m, Math.ceil((min + max) / 2) + 1)) {
System.out.println((int) Math.ceil((min + max) / 2));
return;
}
//如果大1個巧克力仍然滿足要求那麼說明當前的第一天吃的Math.ceil((min+max)/2)取小了應取大一點的巧克力數,需要繼續二分查找
else {
min = Math.ceil((min + max) / 2);//取低位爲當前的試探的第一天吃的巧克力數
stillHas = (double) m;//重置剩餘巧克力數爲總數
}
}
//flag==false,說明上面的for循環遇到break跳出,說明當前的第一天吃的Math.ceil((min+max)/2)取大了應取小一點的巧克力數,需要繼續二分查找
else {
max = Math.ceil((min + max) / 2);//取高位爲當前的試探的第一天吃的巧克力數
stillHas = (double) m;//重置剩餘巧克力數爲總數
flag = true;//重置標誌位
}
}
}
//用於判斷當每天吃X個巧克力時是否能撐到父母回來的方法
public static boolean isTrue(int n, double m, double x) {
for (int i = 1; i < n + 1; i++) {
if (x <= m) {
m = m - x;
x = Math.ceil(x / 2);
} else {
return false;
}
}
return true;
}
三、打家劫舍
題目:
/** *你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統, * 如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。 *給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。 * * 示例 1: * 輸入: [1,2,3,1] * 輸出: 4 * 解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。 * 偷竊到的最高金額 = 1 + 3 = 4 。 * * 示例 2: * 輸入: [2,7,9,3,1] * 輸出: 12 * 解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。 * 偷竊到的最高金額 = 2 + 9 + 1 = 12 。 */
這道題是一個很經典的動態規劃問題,這道題的本質相當於在一列數組中取出一個或多個不相鄰數,使其和最大。那麼我們對於這類求極值的問題首先考慮動態規劃Dynamic Programming來解, 我們維護一個一位數組dp,其中dp[i]表示到i位置時不相鄰數能形成的最大和,那麼狀態轉移方程怎麼寫呢, 我們先拿一個簡單的例子來分析一下,比如說nums爲{3, 2, 1, 5},那麼我們來看我們的dp數組應該是什麼樣的, 首先dp[0]=3沒啥疑問,再看dp[1]是多少呢,由於3比2大,所以我們搶第一個房子的3,當前房子的2不搶,所以dp[1]=3,那麼再來看dp[2], 由於不能搶相鄰的,所以我們可以用再前面的一個的dp值加上當前的房間值,和當前房間的前面一個dp值比較,取較大值當做當前dp值, 所以我們可以得到狀態轉移方程dp[i] = max(num[i] + dp[i - 2], dp[i - 1]), 由此看出我們需要初始化dp[0]和dp[1], 其中dp[0]即爲num[0],dp[1]此時應該爲max(num[0], num[1])
//dp 方程 dp[i] = max(dp[i-2]+nums[i], dp[i-1])
public static int method2(int[] nums){
int n = nums.length;
if (n <= 1) return n == 0 ? 0 : nums[0];
int[] memo = new int[n];
memo[0] = nums[0];
memo[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++)
memo[i] = Math.max(memo[i - 1], nums[i] + memo[i - 2]);
return memo[n - 1];
}
由此我們可以擴展講解一下動態規劃的基礎問題,硬幣問題
如果我們有面值爲1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠n元;
我們用d(i)=j來表示湊夠i元最少需要j個硬幣。於是我們已經得到了d(0)=0,表示湊夠0元最小需要0個硬幣。
當i=1時,只有面值爲1元的硬幣可用,因此我們拿起一個面值爲1的硬幣,接下來只需要湊夠0元即可,
而這個是已經知道答案的,即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。當i=2時,
仍然只有面值爲1的硬幣可用,於是我拿起一個面值爲1的硬幣,接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數量),
而這個答案也已經知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到這裏,你都可能會覺得,好無聊,感覺像做小學生的題目似的。
因爲我們一直都只能操作面值爲1的硬幣!耐心點,讓我們看看i=3時的情況。當i=3時,我們能用的硬幣就有兩種了:
1元的和3元的( 5元的仍然沒用,因爲你需要湊的數目是3元!5元太多了親)。既然能用的硬幣有兩種,我就有兩種方案。
如果我拿了一個1元的硬幣,我的目標就變爲了:湊夠3-1=2元需要的最少硬幣數量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。
這個方案說的是,我拿3個1元的硬幣;第二種方案是我拿起一個3元的硬幣,我的目標就變成:湊夠3-3=0元需要的最少硬幣數量。
即d(3)=d(3-3)+1=d(0)+1=0+1=1. 這個方案說的是,我拿1個3元的硬幣。好了,這兩種方案哪種更優呢?記得我們可是要用最少的硬幣數量來湊夠3元的。
所以,選擇d(3)=1,怎麼來的呢?具體是這樣得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。 OK,碼了這麼多字講具體的東西,
讓我們來點抽象的。從以上的文字中,我們要抽出動態規劃裏非常重要的兩個概念:狀態和狀態轉移方程。 上文中d(i)表示湊夠i元需要的最少硬幣數量,
我們將它定義爲該問題的"狀態",這個狀態是怎麼找出來的呢?我在另一篇文章 動態規劃之揹包問題(一)中寫過:根據子問題定義狀態。
你找到子問題,狀態也就浮出水面了。最終我們要求解的問題,可以用這個狀態來表示:d(11),即湊夠11元最少需要多少個硬幣。
那狀態轉移方程是什麼呢?既然我們用d(i)表示狀態,那麼狀態轉移方程自然包含d(i),上文中包含狀態d(i)的方程是:
d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯,它就是狀態轉移方程,描述狀態之間是如何轉移的。當然,我們要對它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j個硬幣的面值; 有了狀態和狀態轉移方程,這個問題基本上也就解決了
四、快樂數
題目:
/** * 編寫一個算法來判斷一個數是不是“快樂數”。 * 一個“快樂數”定義爲:對於一個正整數,每一次將該數替換爲它每個位置上的數字的平方和,然後重複這個過程直到這個數變爲 1, * 也可能是無限循環但始終變不到 1。如果可以變爲 1,那麼這個數就是快樂數。 * * 示例: * 輸入: 19 * 輸出: true * 解釋: * 12 + 92 = 82 * 82 + 22 = 68 * 62 + 82 = 100 * 12 + 02 + 02 = 1 */
百度一下:
不是快樂數的數稱爲不快樂數(unhappy number),所有不快樂數的數位平方和計算,最後都會進入 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 的循環中。
但是我們要如何檢測一個數是不是一個快樂數呢,一個重點:不快樂數會陷入到循環之中,我們就要用這點來解題,我們把所有計算過的結果用一個容器記住,然後一旦新的結果在容器中出現過,那它就會陷入循環,它就不快樂,反之快樂數最終會收斂到1.
public static boolean isHappy(int n) {
ArrayList<Integer> list = new ArrayList<Integer>();
while (getPowSum(n)!=1) {
if (list.contains(n)) {
return false ;
}else {
list.add(n);
n = getPowSum(n);
}
}
return true ;
}
public static int getPowSum(int n) {
int temp = 0 ;
while (n>0) {
temp+=Math.pow(n%10, 2);
n/=10;
}
return temp;
}