1. 程式人生 > 實用技巧 >5-kunernetes資源排程

5-kunernetes資源排程

斜率優化學習筆記

(解釋太麻煩,這裡籠統的講一下,具體看書,書上更詳盡,所以無視這部分)

斜率優化通常一般可以將\(n^2\)的DP優化到\(O(n)\) ,是一個很強的優化。

形如:

\[f[i]=min\{f[j]+(s[i]-s[j])^2\}(i-d\leq j) \]

看似好像可以用單調佇列的,但是無法把\(i,j\)分開的DP,大部分可以用斜率優化

一般斜率優化具有單調性,可以用單調佇列維護有效決策;

單調性什麼意思?有效決策什麼意思?

例如這個方程,把它變一下,\(min\)去掉,把關於\(j\)的都移到左邊,關於\(i\)\(i,j\)的移到右邊

\[f[j]+s[j]^2=2*s[i]*s[j]+f[i]-s[i]^2 \]

\(s[i]​\)都是已知的,要在一堆\(\{f[j],s[j]\}​\)中選出一個使得\(f[i]​\)最小;

那麼對於每一個\(\{f[j],s[j]\}\) ,放在平面直角座標系中設\(f[j]+s[j]^2\)\(y\)\(s[j]\)\(x\) ,可以畫出一條斜率為\(2*s[i]\)的直線,那麼這條線的截距就為\(f[i]-s[i]^2\)

那麼使得截距最小的\(\{f[j],s[j]\}\) 就為所求。

所以決策就是這些\(\{f[j],s[j]\}\) ,而有效決策就是對求\(f[i]\)有貢獻的決策。

這裡\(s[i]\)單調遞增,就因為著斜率單調遞增,每次新加入的決策也在最右邊。

然後結合書上講的有些關於凹凸的東西,直線平移,什麼的,看書。(滑稽)

(ps: 加黑的都是斜率優化的套路)

列出斜率方程後就基本上可以直接套板子了;


任務安排2

一開始想到暴力DP方程:(設\(f[i][j]\)為前\(j\)個任務分為\(i\)批的最小費用,\(sum\)為各種字首和)

\[f[i][j]=min\{f[i-1][k]+(S*i+sumT[j])*(sumC[j]-sumC[k])\} \]

拆開也是可以斜率優化的,因為拆開後只會得到一項\(j,k\)無法分開的,這是可以套板子的,但是\(i\)是沒有限制的,所以難以得出答案;

那麼考慮當每分一個組的時候後面的時間都會往後移\(S\)

,那麼最後總費用就會增加\(S*(sumC[n]-sumC[j])\) ,那就好辦了,設\(f[i]\)為前\(i\)個任務的最小費用,就有狀態轉移方程:

\[f[i]=min\{f[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[n]-sumC[i])\} \]

其實這裡\(f[i]\)的定義有誤,應該為前\(i\)個任務對最後答案的最小貢獻。(無視這句話也可以寫)

轉化為斜率方程:

\[f[j]=(S+sumT[i])*sumC[j]+f[i]-sumT[i]*sumC[i]-S*sumC[n] \]

這裡斜率和加入點的橫座標是單調遞增的,然後又可以開心的套板子了。


任務安排3

和上題不一樣的地方就是列出的斜率方程,斜率沒有單調性,但是有效決策的橫座標的加入還是單調遞增的,那就二分查詢最優的決策就可以了。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=3e5+5;
ll f[N],n,s;
ll sumc[N],sumt[N];
ll head=1,tail=1;
ll q[N];
ll read()
{
    ll f=1;
    char ch;
    while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
    ll res=ch-'0';
    while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
    return res*f;
}
ll check(int i)
{
    if(head==tail) return q[head];
    int l=head,r=tail;
    while(l<r)
    {
        int mid=(l+r)>>1;
        if(f[q[mid+1]]-f[q[mid]]<=(s+sumt[i])*(sumc[q[mid+1]]-sumc[q[mid]])) l=mid+1;
        else r=mid;
    }
    return q[r];
}
void solve(int i)
{
    ll j=check(i);
    f[i]=f[j]-(s+sumt[i])*sumc[j]+sumt[i]*sumc[i]+sumc[n]*s;
    while(head<tail&&(f[i]-f[q[tail]])*(sumc[q[tail]]-sumc[q[tail-1]])<=(f[q[tail]]-f[q[tail-1]])*(sumc[i]-sumc[q[tail]])) tail--;
    q[++tail]=i;
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("task.in","r",stdin);
    freopen("task.out","w",stdout);
#endif
    n=read(),s=read();
    for(int i=1;i<=n;i++)
    {
        ll t=read(),c=read();
        sumt[i]=sumt[i-1]+t;
        sumc[i]=sumc[i-1]+c;
    }
    for(int i=1;i<=n;i++) solve(i);
    printf("%lld\n",f[n]);
    return 0;
}

Cats Transport

這題有個巧妙的思路(巧妙的思路當然不是我想出來的啊)\(a[i]=t[i]-\sum_{j=1} ^i d[j]\) ,這樣就可以表示出如果飼養員要接到第\(i\)只貓就要在\(a[i]\)之後出發,且貓\(i\)等待的時間為\(t-a[i]\)

那麼這樣就好辦了,只要將\(a[i]\)從小到大排序,求出字首和\(suma[i]\),那要帶走區間\([l+1,r]\)的貓,這些貓要等待待的時間為\(a[r]*(r-l)-(suma[r]-suma[l])\)

這樣就有狀態轉移方程:

\[f[i][j]=min\{f[i-1,k]+a[j]*(j-k)-(suma[j]-suma[k])\} \]


玩具裝箱

直接劃分就好了,設\(f[i]\)為前\(i\)個的最小价值,\(Sum[i]\)為加上了填充物之後的長度的字首和,即\(Sum[i]=Sum[i-1]+c[i]+1\) ,這樣就有DP方程:

\[f[i]=min\{f[j]+(Sum[i]-(Sum[j]+1)-L)^2\} \]

為了簡化式子,再設\(S[j]=Sum[j]+1\) ,然後去掉\(min\)把這個式子變為斜率優化通常的亞子:

\[f[j]+S[j]^2+2*L*S[j]=2*Sum[i]*S[j]+f[i]-(Sum[i]-L)^2 \]

然後以此建立座標系,方程左邊為\(y\)\(S[j]\)\(x\) ,然後每條線斜率為\(2*Sum[i]\)


倉庫建設

為了把最後得到的斜率方程使得斜率和\(x\)都單調遞增,我嘗試了一堆表示方法(甚至一度懷疑是不是又要二分了),不過最後還是在我不懈的努力下列出來了(哭泣)

由題意可得最後一個工廠一定要設定倉庫,所以可以設\(f[i]\)為前\(i\)個工廠的最小花費,且第\(i\)個工廠建設倉庫,這樣\(f[n]\) 就是最後的答案;

下面就是我想了很久的東西了:(ps:\(d[i]\)為題目中的\(x[i]\)

對於每個工廠,\(d[i]*p[i]\)為每個工廠送到工廠1所需要的費用,那麼設\(sumf[i]\)為這費用的字首和,然後再設\(sump[i]\)\(p[i]\)的字首和,所以對於區間\([l,r]\)的物品全部運到\(r\)所需要的費用為\(d[r]*(sump[r]-sump[l])-(sumf[r]-sumf[l])\) ,可以通過畫圖理解;

所以有狀態轉移方程:

\[f[i]=min\{f[j]+d[i]*(sump[i]-sump[j])-(sumf[i]-sumf[l])+c[i]\} \]

變成斜率優化的方程,把關於\(j\)的都移到左邊,關於\(i\)\(i,j\)的移到右邊:

\[f[j]+sumf[j]=d[i]*sump[j]+f[i]-d[i]*sump[i]+sumf[i]-c[i] \]

斜率為\(d[i]\)\(x\)\(sump[j]\) ,他們都單調遞增,那就寫完了,套板子就好了。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e6+5;
int n;
ll sumf[N],sump[N],d[N],c[N],f[N];
ll q[N],x[N],y[N];
ll read()
{
    ll f=1;
    char ch;
    while((ch=getchar())<'0'||ch>'9') if(ch=='-') f=-1;
    ll res=ch-'0';
    while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0';
    return res*f;
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("publi.in","r",stdin);
    freopen("publi.out","w",stdout);
#endif
    n=read();
    for(int i=1;i<=n;i++)
    {
        d[i]=read();
        ll p=read();
        c[i]=read();
        sumf[i]=sumf[i-1]+d[i]*p;
        sump[i]=sump[i-1]+p;
    }
    int head=1,tail=1;
    for(int i=1;i<=n;i++)
    {
        while(head<tail&&(y[head+1]-y[head])<=d[i]*(x[head+1]-x[head])) head++;
        f[i]=f[q[head]]+d[i]*(sump[i-1]-sump[q[head]])-(sumf[i-1]-sumf[q[head]])+c[i];
        while(head<tail&&(f[i]+sumf[i]-y[tail])*(x[tail]-x[tail-1])<=(y[tail]-y[tail-1])*(sump[i]-x[tail])) tail--;
        q[++tail]=i,y[tail]=f[i]+sumf[i],x[tail]=sump[i];
    }
    printf("%lld",f[n]);
    return 0;
}

特別行動隊

與前幾題不也一樣的地方就是斜率遞減,但\(x\)還是遞增;還是可以用單調佇列,維護一個單調遞減的斜率;在匹配斜率的時候只需要找到一個斜率比它小的就可以了;

DP方程:

\[f[i]=max\{f[j]+a*(sum[i]-sum[j])^2+b*(sum[i]-sum[j])+c\} \]

後面還是板子;


列印文章

板子;


鋸木廠選址

和倉庫建設類似,不過更簡單,先\(O(n)\)算出第一個鋸木廠對於前\(i\)顆樹的最優策略,然後求二個鋸木廠最優策略就和倉庫建設的思路一樣了,最後再\(O(n)\)列舉一下第二個鋸木廠的位置,後面的都運到山底去,取個\(min\)就完事了;


任務安排4

以後要寫的題。

點的加入和斜率都不具有單調性,需要動態加點,那就需要平衡樹維護,具體怎麼操作,這裡留個坑,等我寫完了再補回來。