[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;
}
posted on 2020-10-21 12:43  TEoS  阅读(230)  评论(0编辑  收藏  举报