<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          漫畫:有趣的 “切蛋糕“ 問題

          共 7738字,需瀏覽 16分鐘

           ·

          2022-06-12 21:13

          導讀:一道又燒腦又有趣的ACM比賽題目。


          作者:小灰
          來源:程序員小灰(ID:chengxuyuanxiaohui)


          —————  第二天  —————



          舉個例子:

          我們有5塊蛋糕,蛋糕的大小分別是 5,17,25,3,15 


          我們有7位顧客,他們的飯量分別是 2,5,7,9,12,14,20


          (每個蛋糕大小和顧客食量都是小于1000的整數(shù),蛋糕和顧客的數(shù)量不超過1000)

          在分發(fā)蛋糕時,有一個特殊的規(guī)則:蛋糕可分不可合。

          什么意思呢?

          一塊較大的蛋糕,可以切分成多個小塊,用來滿足多個胃口較小的顧客:


          但是,若干塊較小的蛋糕,不允許合并成一塊大蛋糕,用來滿足一個胃口較大的顧客:


          最后的問題是:給定蛋糕大小的集合cakes,給定顧客飯量的集合mouths,如何計算出這些蛋糕可以滿足的最大顧客數(shù)量?

          • 比如:輸入cakes集合 {2,9};輸入mouths集合 {5,4, 2,8}
          • 正確返回:3



          小灰的思路:

          為了讓更多的顧客吃飽,肯定要優(yōu)先滿足食量小的顧客,所以小灰決定使用貪心算法

          首先,把蛋糕和顧客從小到大進行排序:

          按照上面的例子,排序結果如下:


          接下來,讓每一個蛋糕和顧客按照從左到右的順序匹配。如果蛋糕大于顧客飯量,則切分蛋糕;如果蛋糕小于顧客飯量,則丟棄該蛋糕。

          第1塊蛋糕大小是3,第1個顧客飯量是2,于是把蛋糕切分成2+1,滿足顧客。剩下的1大小蛋糕無法滿足下一位顧客,丟棄掉。


          第2塊蛋糕大小是5,第2個顧客飯量是5,剛好滿足顧客。


          第3塊蛋糕大小是15,第3個顧客飯量是7,于是把蛋糕切分成7+8,滿足顧客。剩下的8大小蛋糕無法滿足下一位顧客,丟棄掉。


          第4塊蛋糕大小是17,第4個顧客飯量是9,于是把蛋糕切分成9+8,滿足顧客。剩下的8大小蛋糕無法滿足下一位顧客,丟棄掉。


          第5塊蛋糕大小是25,第5個顧客飯量是12,于是把蛋糕切分成12+13,滿足顧客。剩下的13大小蛋糕無法滿足下一位顧客,丟棄掉。


          這樣一來,所有蛋糕都用完了,由貪心算法得出結論,最大能滿足的顧客數(shù)量是5。



          例子當中:

          • 3的蛋糕滿足2的顧客,
          • 5的蛋糕滿足5的顧客,
          • 15的蛋糕滿足12的顧客,
          • 17的蛋糕滿足7和9的顧客,
          • 25的蛋糕滿足14的顧客。

          顯然,面試官隨意給出的吃法,滿足了6個顧客。

          ————————————





          這句話聽起來有點繞,什么意思呢?我們可以看看下面這張圖:


          其實道理很簡單,由于顧客的飯量是從小到大排序的,優(yōu)先滿足飯量小的顧客,才能盡量滿足更多的人。

          因此,在記錄顧客飯量的數(shù)組中,必定存在一段從最左側開始的連續(xù)元素,符合當前蛋糕所能滿足的最多顧客組合。

          這樣一來,我們的題目就從尋找最大滿足顧客數(shù)量,轉化成了尋找顧客飯量有序數(shù)組中的最大滿足臨界點:



          讓我們先來回顧一下二分查找的思路:


          1. 選擇中間元素,下標mid = (0 + 6)/2 = 3  ,因此中間元素是9:


          2. 判斷9>5,選擇元素9左側部分的中間元素,下標mid = (0+2)/2 = 1,因此中間元素是5:


          3. 判斷5=5,查找結束。

          但是,切蛋糕的問題比普通的二分查找要復雜得多,因為我們要尋找的顧客飯量數(shù)組臨界元素,并不是簡單地判斷元素是否相等,而是要驗證給定的蛋糕能否滿足臨界元素之前的所有顧客。

          如何來實現(xiàn)呢?我們仍舊使用剛才的例子,演示一下詳細過程:


          第一步,尋找顧客數(shù)組的中間元素。

          在這里,中間元素是9:


          第二步,驗證飯量從2到9的顧客能否滿足。

          子步驟1,遍歷蛋糕數(shù)組,尋找大于9的蛋糕,最終尋找到元素15。


          子步驟2,飯量9的顧客吃掉15的蛋糕,還剩6大小。


          子步驟3,重新遍歷蛋糕數(shù)組,尋找大于7的蛋糕,最終尋找到元素17。


          子步驟4,飯量7的顧客吃掉17的蛋糕,還剩10大小。


          子步驟5,重新遍歷蛋糕數(shù)組,尋找大于5的蛋糕,最終尋找到元素5。


          子步驟6,飯量5的顧客吃掉5的蛋糕,還剩0大小。


          子步驟7,重新遍歷蛋糕數(shù)組,尋找大于2的蛋糕,最終尋找到元素3。


          子步驟8,飯量2的顧客吃掉3的蛋糕,還剩1大小。


          到此為止,從2到9的所有顧客都被滿足了,驗證成功。

          接下來,我們需要引入更多顧客,從而試探出蛋糕滿足的顧客上限。

          第三步,重新尋找顧客數(shù)組的中間元素。

          由于第二步驗證成功,所以我們要在元素9右側的區(qū)域,重新尋找中間元素。顯然,這個中間元素是14:


          第四步,驗證飯量從2到14的顧客能否滿足。

          這一步和第二步的思路是相同的,這里就不詳細闡述了。最終的驗證結果是,從2到14的顧客能夠滿足。

          接下來,我們還要引入更多顧客,試探出蛋糕滿足的顧客上限。

          第五步,重新尋找顧客數(shù)組的中間元素。

          由于第四步驗證成功,所以我們要在元素14右側的區(qū)域,重新尋找中間元素。顯然,這個中間元素也就是唯一的元素20:


          第六步,驗證飯量從2到20的顧客能否滿足。

          這一步和第二步、第四步的思路是相同的,這里就不詳細闡述了。最終的驗證結果是,從2到20的顧客不能夠滿足。

          經過以上步驟,我們找到了最大滿足顧客的臨界點14,從2到14共有6個顧客,所以給定蛋糕最大能滿足的顧客數(shù)量是6。


          //剩余蛋糕數(shù)量
          static int leftCakes[];
          //蛋糕總量(不是數(shù)量,而是大小之和)
          static int totalCake = 0;
          //浪費蛋糕量
          static int lostCake = 0;
          static boolean canFeed(int[] mouths, int monthIndex, int sum)
          {
             if(monthIndex<=0) {
                 //遞歸邊界
                 return true;
             }
             //如果 蛋糕總量-浪費蛋糕量 小于當前的需求量,直接返回false,即無法滿足
             if(totalCake - lostCake < sum) {
                 return false;
             }
             //從小到大遍歷蛋糕
             for(int i=0;i<leftCakes.length; i++) {
                 if (leftCakes[i] >= mouths[monthIndex]) {
                     //找到并吃掉匹配的蛋糕
                     leftCakes[i] -= mouths[monthIndex];
                     //剩余蛋糕小于最小的需求,變?yōu)槔速M蛋糕
                     if (leftCakes[i] < mouths[1]){
                         lostCake += leftCakes[i];
                     }
                     //繼續(xù)遞歸,試圖滿足mid下標之前的需求
                     if (canFeed(mouths, monthIndex-1, sum)) {
                         return true;
                     }
                     //無法滿足需求,蛋糕狀態(tài)回滾
                     if (leftCakes[i] < mouths[1]) {
                         lostCake -= leftCakes[i];
                     }
                     leftCakes[i] += mouths[monthIndex];
                 }
             }
             return false;
          }
          public static int findMaxFeed(int[] cakes, int[] mouths){
             //蛋糕升序排列,并把0下標空出
             int[] cakesTemp = Arrays.copyOf(cakes, cakes.length+1);
             Arrays.sort(cakesTemp);
             for(int cake: cakesTemp){
                 totalCake += cake;
             }
             //顧客胃口升序排列,并把0下標空出
             int[] mouthsTemp = Arrays.copyOf(mouths, mouths.length+1);
             Arrays.sort(mouthsTemp);
             leftCakes = new int[cakes.length+1];
             //需求之和(下標0的元素是0個人的需求之和,下標1的元素是第1個人的需求之和,下標2的元素是第1,2個人的需求之和.....)
             int[] sum = new int[mouths.length+1];
             for(int i=1;i<=mouths.length;i++) {
                 sum[i] = sum[i - 1] + mouthsTemp[i];
             }
             //left和right用于計算二分查找的“中點”
             int left=1,right=mouths.length;
             //如果胃口總量大于蛋糕總量,right指針左移
             while(sum[right]> totalCake){
                 right--;
             }
             //中位指針,用于做二分查找
             int mid=((left+right)>>1);
             while(left<=right)
             {
                 //重置剩余蛋糕數(shù)組
                 leftCakes = Arrays.copyOf(cakesTemp, cakesTemp.length);
                 //重置浪費蛋糕量
                 lostCake =0;
                 //遞歸尋找滿足需求的臨界點
                 if(canFeed(mouthsTemp, mid, sum[mid])){
                     left=mid+1;
                 } else {
                     right = mid - 1;
                 }
                 mid=((left+right)>>1);
             }
             //最終找到的是剛好滿足的臨界點
             return mid;
          }
          public static void main(String[] args{
             int[] cakes = new int[]{3,5,15,17,25};
             int[] mouths = new int[]{2,5,7,9,12,14,20};
             int maxFeed = findMaxFeed(cakes, mouths);
             System.out.println("最大滿足顧客數(shù):" + maxFeed);
          }

          這段代碼比較復雜,需要說明幾點:

          1. 主流程方法findMaxFeed,執(zhí)行各種初始化,控制二分查找流程。
          2. 方法canFeed,用于檢驗某一位置之前的顧客是否能被給定蛋糕滿足。
          3. 數(shù)組leftCakes,用于臨時存儲剩余的蛋糕大小,每次重新設置中間下標時,這個數(shù)組需要被重置。





          延伸閱讀??


          延伸閱讀《算法導論》


          干貨直達??


          更多精彩??

          在公眾號對話框輸入以下關鍵詞
          查看更多優(yōu)質內容!

          讀書 | 書單 | 干貨 | 講明白 | 神操作 | 手把手
          大數(shù)據(jù) | 云計算 | 數(shù)據(jù)庫 | Python | 爬蟲 | 可視化
          AI | 人工智能 | 機器學習 | 深度學習 | NLP
          5G | 中臺 | 用戶畫像 數(shù)學 | 算法 數(shù)字孿生

          據(jù)統(tǒng)計,99%的大咖都關注了這個公眾號
          ??
          瀏覽 14
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爱爱视频免费看日本 | 麻豆久久一区二区三区 | 午夜黄色视频在线观看 | 成人精品视频网站 | 日韩 人妻 精品 无码 |