1. 程式人生 > >【日常學習】【區間DP+高精】codevs1166 矩陣取數遊戲題解

【日常學習】【區間DP+高精】codevs1166 矩陣取數遊戲題解

題目來自NOIP2007TG3

如果在考場上我現在已經歇菜了吧

今天一整天的時間全部投在這道題上,收穫不小。

先上題目

題目描述 Description

【問題描述】
帥帥經常跟同學玩一個矩陣取數遊戲:對於一個給定的n*m 的矩陣,矩陣中的每個元素aij均
為非負整數。遊戲規則如下:
1. 每次取數時須從每行各取走一個元素,共n個。m次後取完矩陣所有元素;
2. 每次取走的各個元素只能是該元素所在行的行首或行尾;
3. 每次取數都有一個得分值,為每行取數的得分之和,每行取數的得分= 被取走的元素值*2i
其中i 表示第i 次取數(從1 開始編號);
4. 遊戲結束總得分為m次取數得分之和。
帥帥想請你幫忙寫一個程式,對於任意矩陣,可以求出取數後的最大得分。

輸入描述 Input Description

第1行為兩個用空格隔開的整數n和m。
第2~n+1 行為n*m矩陣,其中每行有m個用單個空格隔開的非負整數。

輸出描述 Output Description

輸出 僅包含1 行,為一個整數,即輸入矩陣取數後的最大得分。

樣例輸入 Sample Input

2 3
1 2 3
3 4 2

樣例輸出 Sample Output

82

資料範圍及提示 Data Size & Hint

樣例解釋

第 1 次:第1 行取行首元素,第2 行取行尾元素,本次得分為1*21+2*21=6
第2 次:兩行均取行首元素,本次得分為2*22

+3*22=20
第3 次:得分為3*23+4*23=56。總得分為6+20+56=82

【限制】
60%的資料滿足:1<=n, m<=30, 答案不超過1016
100%的資料滿足:1<=n, m<=80, 0<=aij<=1000

首先讀題後,我們發現行與行之間相互獨立沒有干擾,這樣我們就可以讀一行處理一行,每一行都是一個單獨的問題,相當於給了n組資料,求出答案的和。

這樣我們把空間從n²降到了n。

接下來開始考慮思路。不難發現,取走一部分數字後,剩下的數字總是形成一個區間。由於取走的數的個數已知,剩下的數字按順序的權值也就知道了,由此得出,這是一個區間DP,和石子合併類似。它滿足最優子結構,也滿足無後效性。

用f[i][j]表示區間[i, j]的最優解,有兩種解法

f[i][j] = max(a[i]  + 2 * f[i+1][j], a[j] + 2 * f[i][j-1]);//直接計算數的權值
f[i][j] = max(a[i]*2^(m-j+i) + f[i+1][j], a[j]*2^(m-j+i) + f[i][j-1])//每次翻倍

第一種,就是先做一個二的冪次方表,直接計算,很好理解。

第二種,也是更加方便的一種,只需要每次將小區間乘二即可,應用了乘法分配律的原理。進行完整個區間後,區間長度小乘的次數就多,最後的效果也是二的冪次方。應該也不難理解吧。

時間複雜度:O(n*m^2)

這樣,我們這道題的框架就出來了,程式如下:

//codevs1166 矩陣取數遊戲 區間DP+高精 
//copyright by ametake
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn=80+5;
const int maxl=32;
int n,m;
int a[maxn],f[maxn][maxn],aa[maxn],bb[maxn],ans[maxn];


int main()
{
    int ans=0;
    scanf("%d%d",&n,&m);//n行m列 
    for (int i=1;i<=n;i++)
    {
        for (int j=1;j<=m;j++) scanf("%d",&a[j]);
        for (int j=1;j<=m;j++) f[j][j]=a[j];
        for (int j=1;j<=m-1;j++)//區間長度 
        {
            for (int k=1;k<=m-j;k++)//起點
            {
                int l=k+j;
                f[k][l] = max(a[k]  + 2 * f[k+1][l], a[l] + 2 * f[k][l-1]);
            } 
        }
        ans+=2*f[1][m];
    }
    printf("%d\n",ans);
    return 0;
} 

但是等等,為什麼會CE?

我們假設一種極端情況,當矩陣為80*80,且每項為1000時,結果最大,為193428131138340667952988000000。

不用數啦,這個數字是30位的。

既然這樣顯然我們要用高精度了。我們沒有JAVA神奇的大整數,但我們也不必像pascal那樣酷比手寫,我們有C++的過載運算子(pas也有但是你敢用嗎= =被限制的死死的一不小心就報錯而且並沒有什麼用)

第一次手寫如此大規模的高精過載,我耗費了整整一上午的時間。

因為沒有任何經驗,我找到了CSDN IcEnternal的程式碼。在這裡引用一下

#include <cstdio>
#include <cstring>
#include <algorithm>


using namespace std;      //程式碼非原創,來源CSDN使用者IcEnternal 源地址http://blog.csdn.net/devillaw_zhc/article/details/7776578


const int power = 1;      //每次運算的位數為10的power次方,在這裡定義為了方便程式實現
const int base = 10;      //10的power次方。


//要壓位的時候,只需改power 和 base即可,如壓萬位高精,那麼power = 4, base = 10000


const int MAXL = 1001;    //陣列的長度。


char a[MAXL], b[MAXL];
struct num
{
    int a[MAXL];
    num() { memset(a, 0, sizeof(a)); }                      //初始化
    num(char *s)                                            //將一個字串初始化為高精度數
    {
        memset(a, 0, sizeof(a));
        int len = strlen(s);
        a[0] = (len+power-1) / power;                       //數的長度
        for (int i=0, t=0, w; i < len ;w *= 10, ++i)        
        {
            if (i % power == 0) { w = 1, ++t; }
            a[t] += w * (s[i]-'0');
        }
        //初始化陣列,這裡自己模擬一下,應該很容易懂的~
    }
    void add(int k) { if (k || a[0]) a[ ++a[0] ] = k; }     //在末尾新增一個數,除法的時候要用到
    void re() { reverse(a+1, a+a[0]+1); }                   //把數反過來,除法的時候要用到
    void print()                                            //列印此高精度數
    {
        printf("%d", a[ a[0] ]);      
        //先列印最高位,為了壓位 或者 該高精度數為0 考慮
        for (int i = a[0]-1;i > 0;--i)
        printf("%0*d", power, a[i]);  
        //這裡"%0*d", power的意思是,必須輸出power位,不夠則前面用0補足
        printf("\n");
    }
} p,q,ans;


bool operator < (const num &p, const num &q)              //判斷小於關係,除法的時候有用
{
    if (p.a[0] < q.a[0]) return true;
    if (p.a[0] > q.a[0]) return false;
    for (int i = p.a[0];i > 0;--i)
    {
        if (p.a[i] != q.a[i]) return p.a[i] < q.a[i];
    }
    return false;
}


num operator + (const num &p, const num &q)               //加法,不用多說了吧,模擬一遍,很容易懂
{
    num c;
    c.a[0] = max(p.a[0], q.a[0]);
    for (int i = 1;i <= c.a[0];++i)
    {
        c.a[i] += p.a[i] + q.a[i];
        c.a[i+1] += c.a[i] / base;
        c.a[i] %= base;
    }
    if (c.a[ c.a[0]+1 ]) ++c.a[0];
    return c;
}


num operator - (const num &p, const num &q)               //減法,也不用多說,模擬一遍,很容易懂
{
    num c = p;
    for (int i = 1;i <= c.a[0];++i)
    {
        c.a[i] -= q.a[i];
        if (c.a[i] < 0) { c.a[i] += base; --c.a[i+1]; }
    }
    while (c.a[0] > 0 && !c.a[ c.a[0] ]) --c.a[0];          
    //我的習慣是如果該數為0,那麼他的長度也是0,方便比較大小和在末尾新增數時的判斷。
    return c;
}


num operator * (const num &p, const num &q)                 
//乘法,還是模擬一遍。。其實高精度就是模擬人工四則運算!
{
    num c;
    c.a[0] = p.a[0]+q.a[0]-1;
    for (int i = 1;i <= p.a[0];++i)
    for (int j = 1;j <= q.a[0];++j)
    {
        c.a[i+j-1] += p.a[i]*q.a[j];
        c.a[i+j] += c.a[i+j-1] / base;
        c.a[i+j-1] %= base;
    }
    if (c.a[ c.a[0]+1 ]) ++c.a[0];
    return c;
}


num operator / (const num &p, const num &q)               //除法,這裡我稍微講解一下
{
    num x, y;
    for (int i = p.a[0];i >= 1;--i)                       //從最高位開始取數
    {
        y.add(p.a[i]);             //把數添到末尾(最低位),這時候是高位在前,低位在後
        y.re();                    //把數反過來,變為統一的儲存方式:低位在前,高位在後
        while ( !(y < q) )         //大於等於除數的時候,如果小於的話,其實答案上的該位就是初始的“0”
            y = y - q, ++x.a[i];   //看能減幾個除數,減幾次,答案上該位就加幾次。
        y.re();                    //將數反過來,為下一次添數做準備
    }
    x.a[0] = p.a[0];
    while (x.a[0] > 0 && !x.a[x.a[0]]) --x.a[0];
    return x;
}


int main()
{
    scanf("%s", a);
    scanf("%s", b);
    reverse(a, a+strlen(a));
    reverse(b, b+strlen(b));

    p = num(a), q = num(b);

    ans = p + q;
    ans.print();

    ans = p - q;
    ans.print();

    ans = p * q;
    ans.print();

    ans = p / q;
    ans.print();
}

上面這位前輩的程式碼寫的相當簡明易懂(相比wfwbz同學來說的確是這樣),受益匪淺,但他的過載實在結構體外面寫的。

我模仿著在結構體裡寫下了這樣的程式碼(上述程式碼中並沒有):

num operator = (int b) //將一個常數賦值給高精結構體
    {
        num c;
        c.a[0]=0;
        while (b)
        {
            c.a[0]++;
            c.a[c.a[0]]=b%base;
            b/=base;
        }
        return c;
    }

以及
num operator + (int b) //高精結構體加常數
    {
        num c;
        c.a[0]=a[0];
        c.a[1]+=b;
        int i=1;
        while (c.a[i]>=base)
        {
            c.a[i+1]+=c.a[i]/base;
            c.a[i]%=base;
            i++;
        }
        if (c.a[c.a[0]+1])++c.a[0];  
        return c;
    }

然而令我百思不得其解的是,這兩個程式碼跑出來都沒有效果,具體來說就是這樣的:

主函式輸出:

int main()
{ 
    scanf("%s", a);  
    scanf("%s", b);  
    reverse(a, a+strlen(a));  
    reverse(b, b+strlen(b));  
  
    p = num(a), q = num(b);  
  
    ans = p + q;
    ans.print();
    ans=12345;
    ans.print();
    ans = ans + 50;  
    ans.print();
    while (1);
    return 0;
} 

結果 輸入 111 222
第一次輸出333 正確(其他大資料也正確)
第二次輸出333 改成return *this輸出12345
第三次輸出50
如果輸入的是大資料 比如一萬左右的 第三次輸出00050這樣類似的

於是我翻出從前wfwbz神犇寫的高精模板,參照那個把前面賦值常數改成了下面這樣,儘管並不理解為什麼,結果成功了

num operator = (int b)
    {
        a[0]=0;
        while (b)
        {
            a[0]++;
            a[a[0]]=b%base;
            b/=base;
        }
        return *this;
    }


怎麼調也沒辦法,里奧神犇表示愛莫能助。這時候我才深深體會到朋友和前輩的重要性。這道題目的解決,尤其要感謝廣饒一中YQL同學和去年一等的那位神犇,清華的HYM神犇以及即將清華的Ag爺WYW神犇,耐心給我解釋了兩個小時···尤其是HYM前輩,小輩各種無知仍然不要其煩,深受感動。原本發出求助後以為沒人注意我,結果一會兒大家都來回答,真的很感動= =

碎碎唸到此為止,總之聽了各位前輩的講解總結出以下幾點:

1.賦值號和加號作用原理不一樣,分開討論,不能一概而論。

2.對於賦值號,上述做法中的第一種錯在不能重新定義一個num c,因為這個變數是區域性變數,在整個函式(過載相當於跑一個函式)進行完的時候自動銷燬收回記憶體。那麼return c不起作用嗎?不起作用。賦值號有自己的返回值。如果這裡我們寫(ans=123).print(),輸出是123,因為這裡輸出的是返回值,賦值號return c,c=123,就輸出123。但是return的這個c並沒有賦值給等號左邊,只返回一個值但沒有效果。

那麼怎麼辦呢?在operator中,對於賦值號左邊有一個只在函式內壁生效的名稱,那就是this。

this是一個指標,指向賦值號左邊的元素。在這裡,它指向被複制的num結構體。

也就是說,對*this操作就是在對等號左邊操作。比如,我們想使ans.a[0]=b,就可以寫(*this).a[0]=b,或者this->a[0]=b;更推薦後面一種寫法。

最後,我們返回*this,作為賦值號的返回值。儘管並不太懂得賦值號返回值有什麼用,但這樣就能保證賦值和輸出都正確。

更改後就是上面的第二種寫法

3.對於加號,結構體加結構體的話,可以開一個臨時結構體c,由於最後起作用的是賦值號,這個c會被賦值給左邊的ans。但是,如果寫結構體加常數就不可以。

為什麼呢?HYM神犇是這樣說的

這兩個確實是不一樣的,你先記憶一下好了。大學會學的。

這是什麼鬼= =

總之大概是我的智商難以承受的東西吧···ORZ神犇的世界

總之更改之後,應當是這樣子:

num operator + (const int &b)
    {
        this->a[1]+=b;
        int i=1;
        while (this->a[i]>=base)
        {
            this->a[i+1]+=this->a[i]/base;
            this->a[i]%=base;
            i++;
        }
        if (this->a[this->a[0]+1])this->a[0];  
        return *this;
    }

由於成員函式定義在結構體內部,可以用a[1]代替this->a[1],簡化後,應當是這樣:
num operator + (const int &b)//this->都可以省略 
    {
        a[1]+=b;
        int i=1;
        while (a[i]>=base)
        {
            a[i+1]+=a[i]/base;
            a[i]%=base;
            i++;
        }
        if (a[a[0]+1])a[0]++;  
        return *this;
    }

而且事實上並不需要&b 直接const傳數字就好(廣饒一中神犇語)

這裡還要補充一個小問題,由WYW神犇友情解答:

 bool operator < (const num &b)const 

最後為什麼要加const?
答:需要用到stl就要加const。這只是為了相容一些stl了,加上const宣告保證在這個函式裡面不會改變結構體成員的值,否則stl會報錯。

最後的最後,我之所以調了一下午程式碼,僅僅是因為一個低階錯誤:

c.a[i]+=a[i]+b.a[i];

應該是+=但我第一次寫成了= 這樣進位的時候直接掛= =

事實證明,千里之堤,潰於蟻穴。細節決定成敗TUT

好了,講完了,放出程式碼君:

//codevs1166 矩陣取數遊戲 區間DP+高精 
//copyright by ametake
#include
#include
#include
using namespace std;

const int maxn=80+5;
int n,m;
int a[maxn];//以上是非高精類定義 

const int power=4;
const int base=10000;
const int maxl=10;

/*
*需要用到
  *高精加高精(狀態轉移時陣列加陣列)
  *高精加單精(狀態轉移時a[k]+f[i][j]) 
  *過載等號 用於把int賦值給結構體 
  *把f陣列開成結構體,每個結構體都是一個高精數
  *按普通陣列讀入,高精處理
  *可以嘗試一下,先寫出來框架 
*/

struct num
{
    int a[maxl];
    
    num()
    {
         memset(a,0,sizeof(a));
    }
    
    num operator + (const num &b)
    {
        num c;
        c.a[0]=max(a[0],b.a[0]);
        for (int i=1;i<=c.a[0];i++)
        {
            c.a[i]+=a[i]+b.a[i];
            c.a[i+1]+=c.a[i]/base;
            c.a[i]=c.a[i]%base;
        }
        if (c.a[c.a[0]+1])++c.a[0];  
        return c; 
    }
    
    num operator + (const int &b)//this->都可以省略 
    {
        a[1]+=b;
        int i=1;
        while (a[i]>=base)
        {
            a[i+1]+=a[i]/base;
            a[i]%=base;
            i++;
        }
        if (a[a[0]+1])a[0]++;  
        return *this;
    }
    
    num operator = (int b)
    {
        a[0]=0;
        while (b)
        {
            a[0]++;
            a[a[0]]=b%base;
            b/=base;
        }
        return *this;
    }
    
    bool operator < (const num &b)const//必須加const 
    {
        if (a[0] < b.a[0]) return true;  
        if (a[0] > b.a[0]) return false;  
        for (int i = a[0];i > 0;--i)  
        {  
            if (a[i] != b.a[i]) return a[i] < b.a[i];  
        }  
        return false;  
    }
    
    void print()
    {
        printf("%d", a[ a[0] ]);        
        //先列印最高位,為了壓位 或者 該高精度數為0 考慮  
        for (int i = a[0]-1;i > 0;--i)  
        printf("%0*d", power, a[i]);    
        //這裡"%0*d", power的意思是,必須輸出power位,不夠則前面用0補足  
        printf("\n"); 
    }
    
}ans,f[maxn][maxn];

int main()
{ 
    scanf("%d%d",&n,&m);//n行m列 
    for (int i=1;i<=n;i++)
    {
        for (int j=1;j<=m;j++) scanf("%d",&a[j]);
        for (int j=1;j<=m;j++) f[j][j]=a[j];//如何賦值? 可以嘗試過載等號 
        for (int j=1;j<=m-1;j++)//區間長度 
        {
            for (int k=1;k<=m-j;k++)//起點
            {
                int l=k+j;
                //f[k][l] = max(a[k] + f[k+1][l] + f[k+1][l], a[l] + f[k][l-] + f[k][l-1]);//這樣寫無法識別 除非在結構體外面寫 或者過載前加friend 
                f[k][l] = max(f[k+1][l] + f[k+1][l] + a[k], f[k][l-1] + f[k][l-1] + a[l]);//高精加即可 過載一下加號用於高精加單精 
            } 
        }
        ans = ans + f[1][m];
        ans = ans + f[1][m];
    }
    ans.print();
    return 0;
} 

/*

//先寫樸素框架,再改成高精版本 16:56
int main()
{
    int ans=0;//高精的時候要改 
    scanf("%d%d",&n,&m);//n行m列 
    for (int i=1;i<=n;i++)
    {
        for (int j=1;j<=m;j++) scanf("%d",&a[j]);
        for (int j=1;j<=m;j++) f[j][j]=a[j];
        for (int j=1;j<=m-1;j++)//區間長度 
        {
            for (int k=1;k<=m-j;k++)//起點
            {
                int l=k+j;
                f[k][l] = max(a[k]  + 2 * f[k+1][l], a[l] + 2 * f[k][l-1]);
            } 
        }
        ans+=2*f[1][m];
    }
    printf("%d\n",ans);
    return 0;
} 
*/

——西塞山前白鷺飛,桃花流水鱖魚肥