1. 程式人生 > 實用技巧 >XJ_Day_29_dp優化_K - Cross the Wall

XJ_Day_29_dp優化_K - Cross the Wall

先附上上午碼的,思路寫在裡面(但是我不會斜率優化,預處理部分看看就好了),我會在後面重新詳細說明

題目

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int MAX=5e4+3;
struct milk
{
    LL h,w;
}a[MAX];
int st[MAX];
inline bool myru(milk x,milk y){if(x.h==y.h)return x.w>y.w;return x.h>y.h;}
LL rin()
{
    LL s=0;
    char c=getchar();
    bool bj=0;
    for(;(c>'9'||c<'0')&&c!='-';c=getchar());
    if(c=='-')c=getchar(),bj=true;
    for(;c>='0'&&c<='9';c=getchar())s=(s<<1)+(s<<3)+(c^'0');
    if(bj)return -s;
    return s;
}
int main()
{
    //將人分成至多k組,每組的費用為max(hi)*max(wi),求最小的費用和
    //首先貪心:
    //1.對於任意1<=i,j<=n,如果滿足h[i]>=h[j]&&w[i]>=w[j],則j是不需要考慮的
    //2.那麼剩下的人中,均滿足,if(h[i]>h[j])w[i]<w[j];(上面的刪的條件保證不會出現相等的情況)
    //3.所以需要考慮的人中,最高的一定寬度最小,最寬的一定高度最小
    //4.按高度從大到小排序,滿足寬度遞增
    //5.顯而易見,如果存在i<j<k(序號),k和j塞在同一組明顯優於k和i塞在同一組,所以就按照這個排序之後的順序,每組都是一段連續的區間
    //6.那麼對於每個i,要麼自己新開一組,要麼接在前一組
    //7.問題在於怎麼找到前一組的開頭(即知道這組的洞的高度來更新答案),如果退化成O(N^2)是肯定不行的

    //奇怪的思路1:
    //列舉k個斷點……,時間O(N^K),而且這題也並不是必須要k個,而是<=k,例子很好舉,比方說,k=3;h1=100,w1=3;h2=99,w2=4;h3=98,w3=5;全放一組明顯最優

    //8.基本上f[N][k]就是極限
    //先寫一波假的複雜度的推導式吧
    //f[i][j]表示當前到i,分了j塊,i為第j個塊的終點 的最小花費
    //ans=max(f[N][j]),1<=j<=k;
    //f[i][j]=min(f[i][j],f[x][j-1]+w[i]*h[x+1]),x<i;時間複雜度O(k*(N^2))
    //分成若干組w[i]*h[x+1]!!!
    
    //奇怪的思路2:
    //if(h[i]*w[N]+h[last]*w[i-1]<h[last]*w[N])last=i;
    //實現了一下,WA了,因為這個zz貪心會導致後面的分塊數不夠
    // 10 4
    // 1 100
    // 2 99
    // 3 98
    // 4 97
    // 5 96
    // 6 95
    // 7 94
    // 8 93
    // 9 92
    // 10000 1
    //對於這組資料,最後一個人肯定是單獨作為一塊,但是由於前面錯誤的貪心導致最後輸出:970592

    //奇怪的思路3:
    //設st[i]表示第i個塊的開始位置
    //初始,st[1]=1;st[i]=N+1,i>1;
    //若當前st[i]往前移能使答案更優,則移動
    //初步估計時間複雜度為O(N*K)
    //h[st[i-1]]*w[st[i]-1]+h[st[i]]*w[st[i+1]-1] 這個是當前這兩段區間的和
    //h[st[i-1]]*w[st[i]-2]+h[st[i]-1]*w[st[i+1]-1] 這個是st[i]向前移動一格後兩端區間的和
    //if(h[st[i-1]]*w[st[i]-2]+h[st[i]-1]*w[st[i+1]-1]<h[st[i-1]]*w[st[i]-1]+h[st[i]]*w[st[i+1]]-1)st[i]--;
    //感覺是跑N次,每次看每個起始點能不能往前移
    //寫出來之後仍然是被那個資料Hack了
    //因為這樣附初值會導致st[2]跑到N就跑不上去了,後面的自然也上不去
    
    //奇怪的思路4:
    //續接奇怪的思路3,考慮外面列舉一層區間數,賦初值就根據這個賦
    //WA了 ,艹
    //反例:
    // 10 4
    // 1 100
    // 2 99
    // 3 98
    // 4 97
    // 5 96
    // 9995 6
    // 9996 5
    // 9997 4
    // 9998 3
    // 10000 1
    // Out put:1000000
    //很好推翻的想法,至少我賦初值的方式不對,從後往前賦,加上正確但不一定完整的判定,WA掉很正常

    //還是想想怎麼優化DP式吧,別整花裡胡哨的東西了
    //9.h遞減,w遞增,這個應該是關鍵
    int i,j;
    int n,k;
    for(;scanf("%d%d",&n,&k)>0;)
    {
        for(i=1;i<=n;i++)a[i].w=rin(),a[i].h=rin();
        sort(a+1,a+n+1,myru);
        int nam=0;
        int max_w=0;
        for(i=1;i<=n;i=j+1)
        {
            for(j=i;j<n&&a[j+1].h==a[i].h;j++);
            if(a[i].w>max_w)max_w=a[i].w,a[++nam]=a[i];
        }
        n=nam;
        // for(i=1;i<=nam;i++)printf("(%d,%d) ",a[i].h,a[i].w);
        //奇怪的思路2程式碼
        // int last=1,s=1;
        // LL ans=0;
        // for(i=2;i<=n;i++)
        // {
        //     if(s==k)break;
        //     // printf("last:%d,ans:%lld -->",last,ans);
        //     if(a[i].h*a[n].w+a[last].h*a[i-1].w<a[last].h*a[n].w)ans+=a[last].h*a[i-1].w,last=i,s++;
        //     // printf("last:%d,ans:%lld\n",last,ans);
        // }
        // ans+=a[last].h*a[n].w;
        // printf("%lld\n",ans);

        //奇怪的思路3:
        // st[1]=1;
        // for(i=2;i<=k+1;i++)st[i]=n+1;
        // for(j=1;j<=n;j++)
        // for(i=2;i<=k;i++)
        // {
        //     if(st[i-1]==st[i])break;
        //     if(a[st[i-1]].h*a[st[i]-2].w+a[st[i]-1].h*a[st[i+1]-1].w<a[st[i-1]].h*a[st[i]-1].w+a[st[i]].h*a[st[i+1]-1].w)st[i]--;
        // }
        // for(i=1;i<=k;i++)printf("%d ",st[i]);printf("\n");
        // LL ans=0;
        // for(i=1;i<=k;i++)ans+=a[st[i]].h*a[st[i+1]-1].w;
        // printf("%lld\n",ans);
        //奇怪的思路4:
        // st[1]=1;
        // LL ans=a[1].h*a[n].w;
        // for(int now=2;now<=k;now++)
        // {
        //     for(i=1;i<now;i++)st[i+1]=n-now+i+1;
        //     st[now+1]=n+1;
        //     for(j=1;j<=n;j++)
        //     for(i=2;i<=k;i++)
        //     {
        //     if(st[i]==st[i-1]+1)continue;
        //     if(a[st[i-1]].h*a[st[i]-2].w+a[st[i]-1].h*a[st[i+1]-1].w<a[st[i-1]].h*a[st[i]-1].w+a[st[i]].h*a[st[i+1]-1].w)st[i]--;
        //     }
        //     // for(i=1;i<=k;i++)printf("%d ",st[i]);printf("\n");
        //     LL sum=0;
        //     for(i=1;i<=now;i++)sum+=a[st[i]].h*a[st[i+1]-1].w;
        //     ans=min(ans,sum);
        // }
        // printf("%lld\n",ans);
    }
    return 0;
}

設第\(i\)個人的高度為\(h(i)\),寬度為\(w(i)\)

1.對於任意\(1\le i,j\le n,\)如果滿足\(h(i)\ge h(j)\&\&w(i)\ge w(j)\),則\(j\)是不需要考慮的人

這個應該是很好理解的,更高更寬的人都能過,就不需要考慮更矮更窄的人


2.那麼剩下的人中,均滿足,\(if(h(i)>h(j))w(i)<w(j);\)(上面的刪的條件保證不會出現相等的情況)

寬度(或高度)相等的若干個人,高度最高的會把其他的人都判成沒用的


3.所以需要考慮的人中,最高的一定寬度最小,最寬的一定高度最小

4.按高度從大到小排序,滿足寬度遞增

這裡就不用解釋了吧,很好懂的,就是由2推導過來的結論


5.顯而易見,如果存在\(i<j<k\)(序號),\(k\)\(j\)塞在同一組明顯優於\(k\)\(i\)塞在同一組,所以就按照這個排序之後的順序,每組都是一段連續的區間

這句話當時寫的比較模糊,這裡重新解釋一下:

在前面排序以後的情況下,是這樣的圖形

對於當前人\(i\),考慮讓他和前面的某個人\(j\)分成一組,打一個兩個人都能通過的洞

高度為\(h(j)\),寬度為\(w(i)\)

那麼這個操作之後\(i\)\(j\)之間的所有點都可以直接通過這個洞(這個結論很顯然,不想解釋,前面的排序就是為了能夠得到這個結論)

所以可以知道,每一組的人員都是一段連續的區間上的人,設左端點為\(l\),右端點為\(r\),這組的花費就是\(h(l)\ast w(r)\)


6.那麼對於每個\(i\),要麼自己新開一組,要麼接在前一組

其實到這裡狀態轉移方程就很明顯了:

\(f(i)(j)\)表示(當前到第\(i\)個人,已經分了\(j\)塊,\(i\)為第\(j\)個塊的終點)的最小花費

\(ans=max(f(n)(j)),1\le j\le k\)

\(f(i)(j)=min(f(i)(j),f(x)(j-1)+w(i)*h(x+1)),x<i\)

時間複雜度\(O(k\ast N^2)\)

這個方程需要優化,一看就知道是用斜率優化來做

然後是需要把高度從小到大排序,滿足\(h\)單調遞減(不過這個不是什麼大問題)

但是我還不會