1. 程式人生 > >[NOI2009]二叉查找樹

[NOI2009]二叉查找樹

怎麽 平衡 作用 cmp pan sum 單單 合並 bits

題目大意:

給定一棵嚴格的treap,父親節點的優先級必然小於兒子節點的。權值按照二叉樹的定義,左兒子小於父親小於右兒子。

深度從1開始定義,每個點除優先級、數值之外,還有一個訪問頻度。

訪問頻度所產生的代價是:訪問頻度*該點深度(這和事實相符)

可以用給定的k的代價,修改任意個點的優先級為任意實數(當然,修改優先級,樹的形態,各點深度就可能變化了)

最終的總代價為:頻度產生代價+修改代價。

最小化這個總代價。

N<=70,1<=K<=30000000

分析:

平衡樹是一個動態的數據結構,難以抓住形態的變化,也不方便記錄深度之類。所以必須抓住不變的量當做突破口。

不管平衡樹怎麽轉,根據二叉樹的定義,它的中序遍歷一定是不變的。

所以我們可以找到這棵樹的中序遍歷,就把這棵樹變成了一個靜態的區間,只不過每個區間所代表的點的優先級可能會變。

發現,每一個連續的子區間,都對應treap的連續一部分。可以把小的區間先建樹,再把大的區間用小的區間合並。我們合並的時候枚舉的劃分點,就是這部分treap的樹根

區間DP順理成章。

除了f[l][r]之外,為了維護優先級的關系,必然要再記錄一維。

發現,只要根節點的優先級確定,子樹的優先級的範圍就確定了。

所以考慮記錄根節點優先級。(這裏優先級只考慮相對大小,而且範圍又大,所以要離散化為1~n)

但是,樸素的f[l][r][w]中,w單單記錄根節點優先級的話,由於子樹所有大於w的都可以轉移,還要for一遍。狀態n^3,轉移n^2,會爆。

所以,我們令f[l][r][w]表示,將l~r這段區間建成treap,其中根節點優先級大於等於w的最小代價。

根據枚舉的根節點是否修改,可以設計轉移方程是:

修改:

f[l][r][w]=min(f[l][r][w],f[l][k-1][w]+f[k+1][r][w]+K+sum[r]-sum[l-1]) ————其中,sum[i]表示,區間中,1~i的訪問頻度和

當劃分點的優先級ch[k]大於w時,可以不修改。

f[l][r][w]=min(f[l][r][w],f[l][k-1][ch[k]]+f[k+1][r][ch[k]]+sum[r]-suim[l-1])

最後答案就是:f[1][n][1];

註意,o循環的時候,必須倒序!!因為ch[k]>=o時候,要從o更大的地方獲取最小值,必須先把o較大的處理完。

代碼1(未簡化):

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=77;
const ll inf=2e18;
ll f[N][N][N];
int n;
ll m;
int a[N];
int tot;
int w[N],p[N],d[N];
int prio[N];
ll sum[N];
int ch[N];//離散化後的優先級 
bool cmp(int a,int b)
{
    return w[a]<w[b];
}
int main()
{
    scanf("%d%lld",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&w[i]),a[i]=i;
    for(int i=1;i<=n;i++) scanf("%d",&p[i]),prio[i]=p[i];
    for(int i=1;i<=n;i++) scanf("%d",&d[i]);
    sort(a+1,a+n+1,cmp);
    sort(prio+1,prio+n+1);
    for(int i=1;i<=n;i++) ch[i]=lower_bound(prio+1,prio+n+1,p[a[i]])-prio;//離散化 
    
    for(int i=1;i<=n;i++)
     for(int j=1;j<=n;j++)
      for(int k=1;k<=n;k++)
        f[i][j][k]=inf;   
    for(int i=1;i<=n;i++)
     for(int k=n;k>=1;k--)
     {
         if(k<=ch[i]) f[i][i][k]=d[a[i]];
         else f[i][i][k]=d[a[i]]+m;//註意,k>ch的時候,不一定是+oo,可以通過修改改變 
     }//l=1的初值 
    
    for(int i=1;i<=n;i++)
     sum[i]=sum[i-1]+d[a[i]];//前綴和 
    for(int l=2;l<=n;l++)
     for(int i=1;i<=n;i++)
    {
        int j=l+i-1;
        if(j>n) break;
        if(l==2)//長度為二的時候,只能二並一 
        {
           for(int o=n;o>=1;o--)
            {
                f[i][j][o]=min(f[i][j][o],f[i][i][o]+m+sum[j]-sum[i-1]);
                f[i][j][o]=min(f[i][j][o],f[j][j][o]+m+sum[j]-sum[i-1]);
                if(ch[i]>=o) f[i][j][o]=min(f[i][j][o],f[i][i][ch[i]]+f[j][j][ch[i]]+sum[j]-sum[i-1]-d[a[i]]);
                if(ch[j]>=o) f[i][j][o]=min(f[i][j][o],f[i][i][ch[j]]+f[j][j][ch[j]]+sum[j]-sum[i-1]-d[a[j]]);
            }
        }
        else{
           for(int o=n;o>=1;o--)
            for(int k=i;k<=j;k++)
           {
                if(k==i)//k在端點處,只能用端點和右邊所有部分合並 
                {
                    f[i][j][o]=min(f[i][j][o],f[i+1][j][o]+m+sum[j]-sum[i-1]);
                    if(ch[i]>=o) f[i][j][o]=min(f[i][j][o],f[i][i][ch[i]]+f[i+1][j][ch[i]]+sum[j]-sum[i-1]-d[a[i]]);
             }
             else if(k==j)//同理 
             {
                 f[i][j][o]=min(f[i][j][o],f[i][j-1][o]+m+sum[j]-sum[i-1]);
                 if(ch[j]>=o) f[i][j][o]=min(f[i][j][o],f[i][j-1][ch[j]]+f[j][j][ch[j]]+sum[j]-sum[i-1]-d[a[j]]);
             }
             else{//正宗轉移方程 
                 f[i][j][o]=min(f[i][j][o],f[i][k-1][o]+f[k+1][j][o]+m+sum[j]-sum[i-1]);
                 if(ch[k]>=o) f[i][j][o]=min(f[i][j][o],f[i][k-1][ch[k]]+f[k+1][j][ch[k]]+sum[j]-sum[i-1]);
             }
           }
        }
    }
    printf("%lld",f[1][n][1]);
    return 0;
}

太惡心了。為了保證l<=r,做出了巨大的討論。

其實不用這麽麻煩,只要讓l>r的時候,賦值為0就好,相當於不存在。根本不影響答案。

代碼2(化簡)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=77;
const ll inf=2e18;
ll f[N][N][N];
int n;
ll m;
int a[N];
int tot;
int w[N],p[N],d[N];
int prio[N];
ll sum[N];
int ch[N];//離散化後的優先級 
bool cmp(int a,int b)
{
    return w[a]<w[b];
}
int main()
{
    scanf("%d%lld",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&w[i]),a[i]=i;
    for(int i=1;i<=n;i++) scanf("%d",&p[i]),prio[i]=p[i];
    for(int i=1;i<=n;i++) scanf("%d",&d[i]);
    sort(a+1,a+n+1,cmp);
    sort(prio+1,prio+n+1);
    for(int i=1;i<=n;i++) ch[i]=lower_bound(prio+1,prio+n+1,p[a[i]])-prio;
    
    for(int i=1;i<=n;i++)
     for(int j=i;j<=n;j++)
      for(int o=0;o<=n;o++)
         f[i][j][o]=inf;
       for(int i=1;i<=n;i++)
        for(int o=0;o<=n;o++)
         f[i][i-1][o]=0;//其實這步不需要,因為上面就沒有給它賦值,只是在這裏強調一下。 
         
    for(int i=1;i<=n;i++)
     sum[i]=sum[i-1]+d[a[i]];
      for(int o=n;o>=1;o--)
     for(int i=n;i>=1;i--)
      for(int j=i;j<=n;j++)
        for(int k=i;k<=j;k++) 
        {
          f[i][j][o]=min(f[i][j][o],f[i][k-1][o]+f[k+1][j][o]+m+sum[j]-sum[i-1]);
          if(ch[k]>=o) f[i][j][o]=min(f[i][j][o],f[i][k-1][ch[k]]+f[k+1][j][ch[k]]+sum[j]-sum[i-1]);
        }//不放心,可以考慮代入長度小於等於2的情況。0的作用就出來了。 
        //連初始化l=1都省了。 
    printf("%lld",f[1][n][1]);
    return 0;
}

總結:

1.對於琢磨不透的變化,一定有不變的東西。一定要抓住其中的不變量,作為突破口。
2.循環順序要註意,一個是不能有後效性,一個是要保證能影響到這個狀態的所有狀態都處理完了。

3.註意考慮清楚所有可能轉移的方式。

[NOI2009]二叉查找樹