1. 程式人生 > 實用技巧 >[NOI2020]製作菜品 題解

[NOI2020]製作菜品 題解

題意分析

給出 $n$ 個數和 $m$ 個 $k$ ,可以某些 $k$ 拆兩個正整數,使得拆後的數可以拼成給出的 $n$ 個數。

思路分析

上面的解釋是因為這樣寫比較方便,實際上按照題意應該是用 $n$ 個數拆分拼成 $m$ 個 $k$ 。

觀察資料範圍,發現有 $m\geq n-2$ 的限制和 $m\geq n-1$ 的部分分,考慮從這裡切入分析。

先分析 $m\geq n-1$ 的情況。令 $d_{min},d_{max}$ 分別表示最小的 $d$ 和最大的 $d$ 。

很容易想到一個貪心,若 $d_{min}<k$,用 $d_{max}$ 與其配對,讓 $d_{max}$ 剩下的量儘量地大;否則就不斷用 $d_{min}$ 單獨做一道菜,直到它剩下的量小於 $k$ ,若還有剩餘,再採用上一種策略。

如何證明這個貪心是正確的?

可以證明當 $m\geq n$ 時, $d_{max}\geq k$ ,即與 $d_{min}$ 配對後一定有剩餘,因此此時每次只會僅讓 $m$ 減 $1$ ( $d_{min}\geq k$ )或 $n,m$ 各減 $1$ ( $d_{min}<k$ )。

若 $m=n-1$ ,可能會出現 $d_{min}+d_{max}\leq k$ 的情況。可以證明此時 $d_{min}+d_{max}\geq 0$ ,因此只會出現 $d_{min}+d_{max}=k$ 的情況,這也說明了 $m=n-1$ 的情況下一定有解。這樣就會有 $n$ 減 $2$ 而 $m$ 減 $1$ ,轉化為 $m=n$ 。但是根據之前的分析,當 $m=n$ 時有 $d_{max}\geq k$ ,而之前的 $d_{max}< k$ 。因此這種情況當且僅當 $n=2$ 時會發生,即當前拼完之後就沒有剩餘的 $d$ 了,此時用剩下的兩個 $d$ 拼成一個 $k$ 即可。另外,可以證明此時 $d_{min}<k$ ,即只會出現 $n,m$ 各減 $1$ 的情況。因此,只要出現 $m=n-1$ ,之後就會一直維持在這個情況,直至 $n=2$ 。

綜上,根據這個貪心策略,可以使 $m\geq n-1$ 所有情況不斷向 $m=n-1$ 靠近,進入 $m=n-1$ 情況後就會保持不變,直至 $n=2$ ,此時用剩下的兩個 $d$ 拼成一個 $k$ 即可,一定有解。用 set 等資料結構優化,時間複雜度 $O(m\log n)$ 。其實暴力 $O(mn)$ 也可以過

題目中的 $\sum d_i =m*k$ 是個很重要的性質,上面的“可以證明”都可以根據這個性質來證明。

接下來分析 $m=n-2$ 的情況。

可以想到把這種情況劃分成兩個獨立的 $m=n-1$ 的情況進行求解,此時兩部分的 $d$ 是沒有重合的。會不會有重合的情況呢?若存在這種情況,即從第二個 $k$ 開始,每次配對只能多用一個 $d$ ,最後只能用 $n-1$ 個 $d$ ,顯然是不可能把 $d$ 全部用完的,因此不存在這種情況。

如何劃分?

分析這個策略,發現實質是要找到一個集合 $S$ 使得 $\sum_{d_i\in S} d_i=(|S|-1)*k$ , $|S|$ 表示集合 $S$ 的大小,即前面的 $n$ ,而不在這個集合內的元素就歸到另一個集合。移項整理可以得到 $\sum_{d_i\in S} (d_i-k)=-k$ 。看到這個式子很容易想到 01 揹包。用 bitset 優化,時間複雜度 $O(\frac{n^2k}{\omega})$ 。然後按照貪心策略分別求解,總的時間複雜度是 $O(\frac{n^2k}{\omega}+nlogn)$ 。注意,由於可能會出現負數,因此要將所有數處理為正數再進行 DP 。

綜上,若 $m\geq n-1$ ,直接按照貪心策略進行求解;否則,用 01 揹包將所有的 $d$ 劃分成滿足 $\sum_{d_i\in S} (d_i-k)=-k$ 的兩個集合,再按照貪心策略分別求解,此時一定有解。若找不到滿足條件的集合,則無解。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<set>
using namespace std;
const int N=600,M=2e6+5e5;
int T,n,m,k;
int d[N],v[N];
bool pd[N];
multiset<pair<int,int> > s;//維護 d_min 和 d_max
void solve()
{
    while(s.size())
    {
        multiset<pair<int,int> >::iterator it=s.begin();
        int minv=(*it).first,minb=(*it).second;
        s.erase(it);//取 d_min
        while(minv>=k)//不斷用 d_min 單獨構成一個 k
        {
            printf("%d %d\n",minb,k);
            minv-=k;
        }
        if(minv && s.size())//還沒解完
        {
            it=s.end();it--;
            int maxv=(*it).first,maxb=(*it).second;
            s.erase(it);//取 d_max
            printf("%d %d %d %d\n",minb,minv,maxb,k-minv);
            maxv-=k-minv;//配對
            if(maxv)
                s.insert(make_pair(maxv,maxb));// d_max 還有剩餘
        }
    }
}//貪心處理
void parti()
{
    memset(pd,0,sizeof(pd));
    bitset<2*M+1> f[N];
    memset(f,0,sizeof(f));
    for(int i=1;i<=n;i++)
        v[i]=d[i]-k;
    f[0].set(M);// 0  的位置初始化
    for(int i=1;i<=n;i++)
        if(v[i]>=0)
            f[i]|=f[i-1]<<v[i],f[i]|=f[i-1];
        else
            f[i]|=f[i-1]>>(-v[i]),f[i]|=f[i-1];//正負數分別處理,取或不取
    if(!f[n][M-k])//找不到滿足條件的集合,無解
    {
        puts("-1");
        return ;
    }
    int now=M-k;
    s.clear();
    for(int i=n;i;i--)//倒著來,找到一個滿足條件的集合
        if(f[i-1][now-v[i]])
        {
            s.insert(make_pair(d[i],i));
            pd[i]=1;now-=v[i];
        }
    solve();s.clear();
    for(int i=1;i<=n;i++)//找另一個集合
        if(!pd[i])
            s.insert(make_pair(d[i],i));
    solve();
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d%d",&n,&m,&k);
        for(int i=1;i<=n;i++)
            scanf("%d",&d[i]);
        if(m==n-2)
            parti();
        else
        {
            s.clear();
            for(int i=1;i<=n;i++)
                s.insert(make_pair(d[i],i));
            solve();
        }
    }
    return 0;
}