1. 程式人生 > >【2019雅禮集訓】【可持久化線段樹】【模型轉化】D1T2Permutation

【2019雅禮集訓】【可持久化線段樹】【模型轉化】D1T2Permutation

目錄

題意

給定一個長度為n的序列A[],你需要確定一個長度為n的排列P[],定義當前排列的值為:
\[\sum_{i=1}^{n}{A[i]P[i]}\]
現在給定一個整數k,需要你求出,在所有可能的排列中(顯然有n!種),最小的k個"排列的值"是多少?依次輸出。排雷不同,值相同的,算不同的方案。
1<=n<=100000

輸入格式

第一行為兩個整數n,k,含義如題意所示。接下來n行為n個整數,代表A[]陣列。

輸出格式

輸出k個整數,分別表示前k個"排列的值",從小到大輸出。

思路

感覺這道題比較神。
這裡就不再提及騙分的做法了,直接說正解是怎樣的。

首先第一步我都沒想到...
可以將題目中的式子轉化為:
\[\sum_{i=1}^{n}A_{P_i}(n-i+1)\]
不僅沒想到,光是看懂這一步就花了我不少的時間。如果覺得比較顯然的話,可以直接跳過下面的解說:

首先讓我們改變一下P[]陣列的定義:原來是單純的一個排列,而現在指的是i這個數字在排列中位於P[i]這個位置。很容易就能夠發現這樣子是一一對應的,只不過式子就變成了\(\sum_{i=1}^{n}A_{P_i}i\),好像和題解不一樣???
但是我們還可以這樣看。只要保證了一一對應,就能夠映射回去。將上述定義下的P[]陣列倒序一下,是不是式子就變對了呢?(根據博主自己的理解,這個時候的P[i]應該是(n-i+1)這個數字在排列中的哪個位置)

將問題轉化之後,考慮如何從一個P[]轉移到另外一個P'[]來得到連續的k個答案。但是在這之前,我們能夠發現,在這種問題的定義下,初始狀態下,A[]應該是遞增的(因為要求值最小,所以說i=1是應乘一個較小的數)。

然後又是很神奇的一步:
考慮這樣的一種轉移:對於P[]陣列而言,假設開始的t位都已經確定了(不能再改變),那麼就考慮剩下的t+1~n個數字。
設一個位置u,一個長度v,保證u-v>t。當前我們令P[u-v]=P[u],並將P[u-v~u-1]這一段區間內的數全部都向後移動一位,同時將t更新為u-v。也就是說整個陣列由:P[u-v],P[u-v+1]...P[u]變為了P[u],P[u-v],P[u-v+1]...P[u-1]。

考慮這樣變化之後,整個序列的值的增量是多少:P[u-v~u-1]都向右移了一段,而乘的數字是遞減的(n,n-1,...3,2,1),所以說減少了\(\sum_{i=u-v}^{u-1}A[P[i]]\);又因為P[u]向左移了u-1-(u-v)+1=v格,所以增加了v*P[u]。將式子合併之後就成了\(\sum_{i=1}^{v}(A_{P_{u}}-A_{P_{u-i}})\)。設一次轉移的增量是g(u,v)。由於我們已經發現了A[]是遞增的,所以說g(u,v)>=g(u,v+1),也就是說只有取v儘量小的時候是最優的。同樣哦我們也能夠發現,只有進行一次轉移,值才有可能最小,所以說我們只需要考慮一次轉移就可以了。

為啥這樣子就能夠代表所有可能狀態的轉移呢??下面又是博主的"想象":

假設下一步不是這樣子轉移的,而是一個"random_shuffle"之後的結果(仍要保證t及其之前的不變)。學過氣泡排序的都知道,這玩意是能夠用氣泡排序做到的。而冒排的一次交換操作可以看成是將一個元素放到了另一個元素的前面,那麼就成了上面的操作。又因為我們已經證明了一次最多隻需要考慮一次操作,所以說就可以歸為上面的情況了。

真是刺激
然而一切才剛剛開始...
以上的分析只是一個前置而已...現在我們考慮一個解決這個問題的一個大致思路:通過對初始狀態(A[]遞增)進行上述的不斷的變化,放進優先佇列裡面,每一次都把隊首取出來,作為當前能夠找到的最小的值輸出,並在這個狀態的基礎上進行更新。在佇列的每一個狀態中,我們都需要維護一個可持久化的線段樹,來維護區間內最小的g(u,v)以及u,v,還有當前這個區間內可用的A[]的個數s。

下面是一些實現細節:(我能說是照辦標程的做法嗎?我實在是不會寫...

  1. 開一個數組TMP[]來存一個狀態。每一個位置存三個東西,一個叫Now的線段樹表示當前這個狀態還有那些狀態能夠使用,主要是為了方便查詢下一步最優轉移;一個叫Ori(Origin)的線段樹,存這個狀態最開始被放進佇列的的時候長啥樣,主要是為了求出下一步的轉移後線段樹應有的狀態,而避免轉移之間互相影響。還有一個數字sum,表示Ori對應的那個狀態,也就是剛剛被放進佇列的時候的序列的
  2. 最開始將A[]從小到大排序之後,將資訊儲存在TMP[0].Now裡面,線段樹的位置i表示A[i]-A[i-1],即最小的轉移。注意線段樹中位置1一定要初始化為INF,因為A[0]是不存在的。
  3. 將TMP[0].Ori初始化為TMP[0].Now
  4. 將TMP[0]放進佇列,注意插進去的答案為TMP[0].sum+TMP[0].Now->g,即轉移一次之後的答案。
  5. 每一次將佇列的頭取出來(狀態編號為T),輸出答案,然後進行更新:
  6. 更新的時候,其實就是將TMP[T].Ori進行了之前已經判斷出來的一次轉移之後的狀態。申請新的狀態num,表示上述含義。並將TMP[num].Now的初值賦為TMP[T].Ori,原因上上面已經說過了。然後將u-v-1之前的點全部刪掉,因為t向後移了(忘記定義的往前找找)。再將原來的u刪掉,因為它被移到u-v之後就在t上了,以後就固定了。再將原來的u-v那個地方的線段樹中的值變為INF,因為它前面已經沒有數字了。然後為原來的u+1尋找新搭配,因為它原來的搭配u已經被刪除了。這樣就完成了TMP[num].Now的更新。當然最後不忘將TMP[num].Ori賦為TMP[num].Now,然後再欽定轉移後入隊。
  7. 當然,如你所料,這還沒完。因為要防止重複轉移,但又要進行完所有可能的轉移,這個時候就要發揮Now的作用了。這裡將講解如何將Now進行更新。首先,之後肯定是不可能再使用g(u,v)進行轉移了,所以說我們要將這個狀態轉移去掉,將線段樹中的u位置賦為新的轉移方式g(u,v+1)。而根據之前增量公式可見,這個時候只需要加上A[P[u]]-A[P[u-v-1]]就可以了。然後..就沒有然後了...最後記得將這個狀態也欽點了轉移之後入隊。

可能博主說的有些含糊不清,但是應該比官方題解清晰一些了,具體還請參見程式碼~~~

程式碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define MAXN 300000
#define INF 1000000000000000000LL
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
struct node
{
    node *ch[2];
    int u,v,s;
//u,v,g(u,v),s含義參上。u,v,s,都是再這個子區間的意義下的
    LL g;
}tree[MAXN*40+5];
node *ncnt=&tree[0],*NIL=&tree[0];
struct ArrNode
{
    node *Ori,*Now;
    LL sum;
}TMP[MAXN*40+5];//狀態節點
int tcnt=0;
LL A[MAXN+5];
int n,k;
priority_queue<PII,vector<PII>,greater<PII> > que;
void Init()
{
    ncnt=NIL=&tree[0];
    NIL->ch[0]=NIL->ch[1]=NIL;
    NIL->u=NIL->v=NIL->s=0;
    NIL->g=INF;
}
inline node* NewNode()
{
    node *p=++ncnt;
    p->ch[0]=p->ch[1]=NIL;
    p->u=p->v=p->s=0;
    p->g=INF;
    return p;
}
void PushUp(node *rt)
{
    node *lch=rt->ch[0],*rch=rt->ch[1];
    if(lch->g<rch->g)
        rt->g=lch->g,rt->u=lch->u,rt->v=lch->v;
    else
        rt->g=rch->g,rt->u=lch->s+rch->u,rt->v=rch->v;
    rt->s=lch->s+rch->s;
}
void Build(node *&rt,int l,int r)
{
    rt=NewNode();
    rt->s=0;
    if(l==r)
    {
        if(l>1) rt->u=rt->v=rt->s=1,rt->g=A[l]-A[l-1];
        else    rt->u=rt->v=rt->s=1,rt->g=INF;//注意特殊處理
        return;
    }
    int mid=(l+r)/2;
    Build(rt->ch[0],l,mid);
    Build(rt->ch[1],mid+1,r);
    PushUp(rt);
}
void Insert(node *&rt,int l,int r,int p,LL gval,int sval)
//Insert是+=,而change是直接賦值。而且Insert是找線段樹的第p個,change是找第p個還存在的點
{
    node *q=NewNode();
    *q=*rt;rt=q;
    if(l==r)
    {
        rt->g+=gval;
        rt->u=rt->s=rt->v=sval;
        return;
    }
    int mid=(l+r)/2;
    if(p<=rt->ch[0]->s) Insert(rt->ch[0],l,mid,p,gval,sval);
    else                Insert(rt->ch[1],mid+1,r,p-rt->ch[0]->s,gval,sval);
    PushUp(rt);
}
void ChangePoint(node *&rt,int l,int r,int p,LL gval,int sval)//與Insert的區別見上
{
    node *q=NewNode();
    *q=*rt;rt=q;
    if(l==r)
    {
        rt->g=gval;
        rt->u=rt->s=rt->v=sval;
        return;
    }
    int mid=(l+r)/2;
    if(p<=rt->ch[0]->s) ChangePoint(rt->ch[0],l,mid,p,gval,sval);
    else                ChangePoint(rt->ch[1],mid+1,r,p-rt->ch[0]->s,gval,sval);
    PushUp(rt);
}
void DelSeg(node *&rt,int l,int r,int sum)
{
    if(sum==0)
        return;
    int mid=(l+r)/2;
    node *q=NewNode();
    *q=*rt;rt=q;
    if(rt->ch[0]->s>sum)    DelSeg(rt->ch[0],l,mid,sum);
    else    DelSeg(rt->ch[1],mid+1,r,sum-rt->ch[0]->s),rt->ch[0]=NIL;
    PushUp(rt);
}
LL Query(node *rt,int l,int r,int p)
{
    if(l==r)
        return A[l];
    int mid=(l+r)/2;
    if(rt->ch[0]->s>=p) return Query(rt->ch[0],l,mid,p);
    else                return Query(rt->ch[1],mid+1,r,p-rt->ch[0]->s);
}
void Extend(int T)
{
    int Z=++tcnt;
    node *&TN=TMP[T].Now,*&TO=TMP[T].Ori;
    
    TMP[Z].sum=TMP[T].sum+TN->g;//先算值 
    
    TMP[Z].Now=TO;//下面再進行線段樹形態的求解 
    int u=TN->u,v=TN->v,pos=u-v;//根據之前維護的資訊求得當前的最優轉移 
    //---------pos-pos+1-------u-----------
    //---------pos+1--------u-pos----------
    //先刪掉u這個點 
    ChangePoint(TMP[Z].Now,1,n,u,INF,0);
    //再刪掉前面~pos-1的部分
    if(pos-1>=1)
        DelSeg(TMP[Z].Now,1,n,pos-1);
    //為u+1重新尋找新的搭檔
    if(v+1<=TMP[Z].Now->s)
    {
        LL newval=Query(TMP[Z].Now,1,n,v+1)-Query(TMP[Z].Now,1,n,v);
        ChangePoint(TMP[Z].Now,1,n,v+1,newval,1);
    }
    ChangePoint(TMP[Z].Now,1,n,1,INF,1);//原來的pos+1也已經失去了搭檔了(因為此時為第一個可用的數)
    //更新完成,儲存初始狀態
    TMP[Z].Ori=TMP[Z].Now;
    //對於TMP[Z]的更新完成了
    que.push(PII(TMP[Z].sum+TMP[Z].Now->g,Z)); 
    
    if(u-v-1>=1)
    {
        LL delta=Query(TN,1,n,u)-Query(TN,1,n,u-v-1);
        Insert(TN,1,n,u,delta,1);//更新為g(u,v+1)
    }
    else
        Insert(TN,1,n,u,INF,1);
    que.push(PII(TMP[T].sum+TMP[T].Now->g,T));//欽定後入隊
}
int main()
{
    Init();
    scanf("%d %d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%lld",&A[i]);
    sort(A+1,A+1+n);
    Build(TMP[0].Ori,1,n);//賦初值
    for(int i=1;i<=n;i++)
        TMP[0].sum+=A[i]*(n-i+1);//算最初的答案
    TMP[0].Now=TMP[0].Ori;
    printf("%lld\n",TMP[0].sum);//先把最小的值輸出來
    que.push(PII(TMP[0].sum+TMP[0].Now->g,0));//欽定轉移併入隊
    for(int i=1;i<=k-1;i++)
    {
        PII fro=que.top();
        que.pop();
        printf("%lld\n",fro.first);//先輸出早就欽定好的答案
        Extend(fro.second);//再進行欽定後的結果的狀態的更新
    }
    return 0;
}