猴子課堂:插頭DP(基於連通性狀態壓縮的動態規劃問題)(讓你從入門到絕望)

来源:https://www.cnblogs.com/zhangjianjunab/archive/2018/09/24/9694676.html
-Advertisement-
Play Games

今天,我,Monkey ~~king~~ 又為大家帶來大(ju)佬(ruo)的演算法啦!——插頭DP 例題(菜OJ上的網址:http://caioj.cn/problem.php?id=1489): 那麼,這道題怎麼做呢?~~(雖然菜OJ上有視頻)~~ 插頭DP能完美解決! 註:我採用的是括弧表示法~ ...


今天,我,Monkey king 又為大家帶來大(ju)佬(ruo)的演算法啦!——插頭DP

例題(菜OJ上的網址:http://caioj.cn/problem.php?id=1489):

這裡寫圖片描述

那麼,這道題怎麼做呢?(雖然菜OJ上有視頻)

插頭DP能完美解決!

註:我採用的是括弧表示法(一個神奇的、猛如虎的神奇表示法)

首先,我們先講一下插頭,總共6種雙插頭(一般用來解決迴路問題和輔助單插頭完成路徑問題)和不知多少種單插頭(用來解決路徑問題)
別看插頭多,其實大部分相同!
插頭:
......。。。。。。拿錯了
插插插頭畫得好醜
註:這隻畫了6種單插頭,因為單插頭在不同情況下可能會分()出不同的情況,所以單插頭難度比較大。

現在講一下輪廓線:
這裡寫圖片描述紅不溜秋的東西十分重要!
如圖,一條迴路如同一大堆插頭拼在一起!

括弧表示法就是把輪廓線的狀態用括弧表示,從而壓縮狀態!
因為每一塊(連在一起的插頭)插頭左右的要去擴張的插頭會一直往下擴,所以不會出現左插頭飛右插頭右邊
如:
這裡寫圖片描述
所以,我們可以把左插頭表示為左括弧,右插頭為右括弧(一對),插頭中的一部分或沒插頭為空。
這裡寫圖片描述
但是,狀態壓縮呀,總不可能開個字元吧!那麼,把左插頭表示成1,右插頭為2,空為0,每個數用兩個二進位表示:01,10,00
然後,從左讓右將二進位數合成一個大的數,用來表示當前狀態!
如:
這裡寫圖片描述
因為要包括沒搞定的格子,所以輪廓線長度為m(矩陣寬度)+1
那麼為什麼要用二進位呢?位運算!
取出第輪廓線上的第q個位置的數:

int  set(int  s,int  p)
{
    return  (s>>((p-1)*2))&3;
}

改變第q位上的數為v:

void  change(int  &s/*引用,別打漏了*/,int  p,int  v)
{
    s^=set(s,p)<<((p-1)*2);
    s^=v<<((p-1)*2);
}

不理解的同學搜一下C++的位運算理解一下!
這裡寫圖片描述
其實,插頭講究的是分類討論,對每種插頭情況分類討論!
先將Hash表,定一個inf和幾個數組
定x,y,z,k分別%inf後得1 4 5 3
那麼,得:
這裡寫圖片描述
代碼如下:

struct  node
{
    int  hash[mod]/*壓狀態*/,key[mod]/*記錄原本的數值*/,size/*記錄存了多少數*/;ll  num[mod]/*記錄當前數值代表了多少狀態*/;
    void  mem()
    {
        memset(hash,-1,sizeof(hash));size=0;//初始化函數
    }
    void  add(int  S,ll  sum)
    {
        int  s=S%mod;
        while(hash[s]!=-1  &&  key[hash[s]]!=S)
        {
            s++;s%=mod;
        }//判斷重覆,這樣做是可以保證下次扔一個同樣的數也可以到這個hash值
        if(hash[s]==-1)
        {
            hash[s]=++size;key[size]=S;num[size]=sum;
        }//新建一個hash格
        else  num[hash[s]]+=sum;//有的話直接加
    }
}dp[2];//滾動數組

有人會問:為什麼同樣的數值表示不用狀態的方案數是可以加在一起的呢?
因為:
這裡寫圖片描述
不管下麵組成怎樣,他們都可以接受,所以,他們的雖然樣子不同,但狀態相同,我們就可以把他們歸為一類。
那麼,接下來最難的其實是分類討論,插頭最難的就是因為它難調且容易漏了幾種情況。
現在,我們設現在準備安上插頭的格子從左面來的插頭為q,上面來的為p。
如:
這裡寫圖片描述

那麼,我們就要利用q和p來分類討論。。。就是代碼。。。就不要在意了(一百多行)
現在到轉移狀態了。
以這圖為例:

這裡寫圖片描述
因為這是一個障礙,所以只有當q=0並且p=0可以繼承狀態。

來個沒障礙的:
這裡寫圖片描述

那麼,再講兩種比較難想的。
這種:
這裡寫圖片描述

插頭的概念就是可以延伸的插頭一定會去延伸或和其他插頭結合,但是什麼時候結束呢?

這道題而言:

就是當q=1並且p=2時且已經到最後一個非障礙格子時就可以將當前的狀態記入狀態。
但是!q=1並且p=2的情況不在最後一個非障礙格子時,就算一個廢的情況(提前生成迴路)。
為什麼q=1並且p=2的情況一定生成迴路呢?
如圖:
這裡寫圖片描述
那麼最後一個格會不會不會出現一對的情況?(沒有單插頭)
只要有合法情況出現,因為插頭的概念就是可以延伸的插頭一定會去延伸或和其他插頭結合,所以兩個插頭會一直延伸到最後一個格相遇!
就結束了。

註意這道題要判斷全是障礙的情況。

上代碼!

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
typedef  long  long  ll;//不知道叫什麼,感覺很大佬,就學了^_^
const  int  mod=200000;//hash表大小,因為hash表有壓縮功能,所以一般100萬就夠了。
int  map[50][50],n,m,ex=-1,ey=-1;//最後一個障礙格子的坐標
char  ss[210];
ll  ans=0;
struct  node
{
    int  hash[mod],key[mod],size;ll  num[mod];
    void  mem()
    {
        memset(hash,-1,sizeof(hash));size=0;
    }//初始化
    void  add(int  S,ll  sum)
    {
        int  s=S%mod;
        while(hash[s]!=-1  &&  key[hash[s]]!=S)
        {
            s++;s%=mod;
        }//找格子
        if(hash[s]==-1)
        {
            hash[s]=++size;key[size]=S;num[size]=sum;
        }
        else  num[hash[s]]+=sum;//將方案記錄
    }
}dp[2];
int  now,php;
int  set(int  s,int  p)
{
    return  (s>>((p-1)*2))&3;
}//取出第k個數
void  change(int  &s,int  p,int  v)
{
    s^=set(s,p)<<((p-1)*2);
    s^=(v&3)<<((p-1)*2);
}//改變
void  work()
{
    ll  sum=0;
    now=0;php=1;
    dp[now].mem();
    dp[now].add(0,1);//初始化滾動型DP數組
    for(int  i=1;i<=n;i++)
    {
        for(int  j=1;j<=m;j++)
        {
            swap(now,php);
            dp[now].mem();//滾動(dan)數組
            for(int  k=1;k<=dp[php].size;k++)
            {
                int  s=dp[php].key[k];
                sum=dp[php].num[k];
                int  q=set(s,j);
                int  p=set(s,j+1);
                if(map[i][j]==0)
                {
                    if(q==0  &&  p==0)dp[now].add(s,sum);
                    continue;
                }//障礙格子
                if(q==0  &&  p==0)
                {
                    if(map[i+1][j]==1  &&  map[i][j+1]==1)//判斷是否有障礙
                    {
                        change(s,j,1);change(s,j+1,2);//從這個格子建兩個插頭
                        dp[now].add(s,sum);
                    }
                }//沒人指我,只好自己建一個插頭
                else  if(q>0  &&  p==0)
                {
                    if(map[i+1][j]==1)dp[now].add(s,sum);//其實你拆開後簡化就是這樣子的
                    if(map[i][j+1]==1)//將插頭引入右邊,繼承下去
                    {
                        change(s,j,0);change(s,j+1,q);
                        dp[now].add(s,sum);
                    }
                }//繼承
                else  if(q==0  &&  p>0)
                {
                    if(map[i][j+1]==1)dp[now].add(s,sum);
                    if(map[i+1][j]==1)//將插頭引入下邊,繼承下去
                    {
                        change(s,j,p);change(s,j+1,0);
                        dp[now].add(s,sum);
                    }
                }//繼承
                else  if(q==1  &&  p==1)
                {
                    int  find=1;//算上p為1!很重要。
                    for(int  tt=j+2;tt<=m;tt++)//找到與p相對的右插頭並修改為這一大塊插頭的左插頭
                    {
                        int  vs=set(s,tt);
                        if(vs==1)find++;
                        else  if(vs==2)find--;//為什麼可以?因為中間罩住的插頭不會出去這個大插頭的範圍,所以只有碰到與p成對的插頭才會清零
                        if(find==0)
                        {
                            change(s,j,0);change(s,j+1,0);change(s,tt,1);
                            dp[now].add(s,sum);
                            break;//你不加這個你試試
                        }
                    }
                }
                else  if(q==2  &&  p==2)
                {
                    int  find=1;
                    for(int  tt=j-1;tt>=1;tt--)//找到與q相對的左插頭並修改為這一大塊插頭的右插頭
                    {
                        int  vs=set(s,tt);
                        if(vs==2)find++;
                        else  if(vs==1)find--;//這樣是不是太啰嗦了?
                        if(find==0)
                        {
                            change(s,j,0);change(s,j+1,0);change(s,tt,2);
                            dp[now].add(s,sum);
                            break;
                        }
                    }
                }
                else  if(q==2  &&  p==1)//呵呵,這樣的狀態十分好想!因為對應的插頭剛好也是我們想要的,只要把當前的q和p連接就好了!
                {
                    change(s,j,0);change(s,j+1,0);
                    dp[now].add(s,sum);
                }
                else  if(q==1  &&  p==2)
                {
                    if(ex==i  &&  ey==j)ans+=sum;//得到答案
                }
            }
        }
        for(int  j=1;j<=dp[now].size;j++)dp[now].key[j]<<=2;//看下麵解釋
    }
}
int  main()
{
    scanf("%d%d",&n,&m);
    for(int  i=1;i<=n;i++)
    {
        scanf("%s",ss+1);
        for(int  j=1;j<=m;j++)
        {
            if(ss[j]=='.'){map[i][j]=1;ex=i;ey=j;}//這樣做方便後面的判斷
        }
    }
    if(ex==-1){printf("0\n");return  0;}//特判
    work();
    printf("%lld\n",ans);//註意答案到了long long
    return  0;
}

在代碼中只繼承了合法方案,不合法方案已經被過濾了

估計許多人不理解這段

else  if(q==2  &&  p==1)
{
    change(s,j,0);change(s,j+1,0);
    dp[now].add(s,sum);
}

如圖:
這裡寫圖片描述
還有這段

for(int  j=1;j<=dp[now].size;j++)dp[now].key[j]<<=2;

因為每次完成一層,輪廓線都要下降一層,如:
這裡寫圖片描述(紅色是輪廓線)
因為代碼中判斷矩陣外的格子為障礙,所以輪廓線最後那條豎線代表的是0,而且從下一行開始,一開始也沒有插頭指向開頭那條豎線(總不可能在矩陣外開插頭吧!),也為0,剛好每個狀態(除了那條豎線外)也往後一位,所以,我們就把二進位往右移兩位(因為每個括弧要兩個二進位數表示)來表示一層向下一層的轉移!
那麼,插頭DP就解決啦!

這裡寫圖片描述

註:上面的圖片侵權抱歉!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JavaScript流程式控制制語句腦圖 圖片是從網上找來的,在這記錄一下,以備後面需要的時候查找方便。 JavaScript通過規定的語句讓有條件的按照一定的方式執行。 分為:迴圈語句 while do-while for for-in 跳轉語句 return break continue 選擇語句 ...
  • JavaScript 中的 ajax 很早之前就有一個詬病————複雜業務下的 callback 嵌套的問題。promise 正是 js 中解決這一問題的鑰匙。 接下來我們在react項目中應用到的fetch 就用到了最新的 promise。 那我們如何在react項目中應用fetch呢? 第一步: ...
  • 前言 此系列是針對springboot的啟動,旨在於和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的源碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的源碼,不知道springboot在啟動過程中做了些 ...
  • 7 Anonymous inner class (視頻下載) (全部書籍) 馬克-to-win:有時如此簡單,都沒有必要清清楚楚明確出類名,用一下就完,就用匿名內部類。註意: 下麵的new FigureMark_to_win(){。。。。};的語法形式。它和以往的new FigureMark_to_ ...
  • 在Ubuntu18.04中,傳統的配置/etc/network/interfaces已無用 新方法:修改 配置如下: 重啟網路 如果網路不正常可以使用: 測試一下遠程連接 ...
  • 什麼是靜態內部(Static Inner)類,語法要註意什麼? ...
  • 什麼是實例內部類 Instance inner class有什麼語法? ...
  • 學完了ISAP,感覺心情舒暢,畢竟ISAP比Dinic好一點。 說到底ISAP其實是Dinic(不熟悉Dinic的人去我的博客找猴子課堂 最大流與最小割(看看思想),已經置頂)優化版,熟悉的人知道Dinic是通過不斷分層來做的,但是,我們如果用打標記(貂蟬的標記)的方法就會快一些! 會快的原因就是因 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...