[Luogu]P3694 邦邦的大合唱战队

Before the Beginning

转载请将本段放在文章开头显眼处,如有二次创作请标明。
原文链接:https://www.codein.icu/lp3694/

题意

题目传送门

\(N\) 个人,分别属于 \(M\) 个组,要求同组的人站在一起。
每个人可以出队,出队后留出空位,出队后的人可以在任意空位归队。
求最少出队人数。

解法

看到 \(M\) 的数据范围极小,猜测可能是状压DP。
但没有想出来到底是如何状压,于是重新审视题意。

全排列

可以发现,最后同组的人站在一起,一定会形成组的顺序,例如:

111111,222222,333333
222222,111111,333333
333333,111111,222222
....

类似如此,那么可以全排列枚举组的顺序。
而确定组的顺序后,每个组的范围就确定下来了。
而对于每个人,如果他的位置恰好属于他组的范围,他便不用出队,否则他一定要出队。
依次检验,取最小值,即可得到答案。
复杂度为 \(O(NM!)\),只能通过四个点。

考虑进行优化,可以发现这个 \(O(N)\) 能够用预处理前缀和的方式去掉。
维护 \(s_{i,j}\) 代表前 \(i\) 人中属于 \(j\) 组的人数,即可用 \(O(1)\) 的时间计算出一组的范围内应有多少人出队。
复杂度降低到 \(O(M!)\),可以通过七个点。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
template<typename T>void read(T &r){static char c;r=0;for(c=getchar();c>'9'||c<'0';c=getchar());for(;c>='0'&&c<='9';r=(r<<1)+(r<<3)+(c^48),c=getchar());}
const int maxn = 1e5 + 10;
const int maxm = 21;
int n,m;
int a[maxn];
int num[maxm];
int order[maxm];
int s[maxn][maxm];
int main()
{
    read(n);
    read(m);
    for(int i = 1;i<=n;++i)
        read(a[i]),num[a[i]]++,s[i][a[i]]++;
    for(int i = 1;i<=n;++i)
        for(int j = 1;j<=m;++j)
            s[i][j] += s[i-1][j];
    for(int i = 1;i<=m;++i)
        order[i] = i;
    int ans = 1 << 30;
    do
    {
        int res = 0;
        int l = 0,r = 0;
        for(int i = 1;i<=m;++i)
        {
            l = r + 1;
            r = l + num[order[i]] - 1;
            res += num[i] - (s[r][order[i]] - s[l-1][order[i]]);
        }
        ans = min(ans,res);
    } while (next_permutation(order + 1, order + 1 + m));
    printf("%d",ans);
    return 0;
}

状压DP

想要将 \(O(M!)\) 的复杂度降低到 \(O(2^M)\),使用状压DP的方法。
有数据范围的提示,很容易定义出状态:
状态 \(S\) 每位代表该组是否已处理完成。
如何转移呢?可以枚举排在最后的组。
计算出当前队列的长度,枚举排在最后的组,已知排在最后组的长度,即可确定排在最后组的范围。
确定组范围后,即可计算出该范围内应有多少人出队。
枚举当前状态每个可能的最后组,取最小值进行转移。

dp[status] = min(dp[status],dp[status ^ (1<<(i-1))] + num[i] - (s[r][i] - s[l-1][i]));

实质上,这个状压DP也是在枚举顺序。
不同于全排列,状压转移时并不知道子状态中的顺序,只知道子状态已完成的组,而将当前组加在子状态队列最后。
或许借此剪去了部分无用的枚举。

笔者使用了记忆化搜索的写法,预处理了每个状态的队列长度。

#include <cstdio>
#include <cstring>
using namespace std;
template<typename T>
inline T min(const T &a,const T &b){return a<b?a:b;}
template<typename T>
void read(T &r){static char c;r=0;for(c=getchar();c>'9'||c<'0';c=getchar());for(;c>='0'&&c<='9';r=(r<<1)+(r<<3)+(c^48),c=getchar());}
const int maxn = 1e5 + 10;
const int maxm = 21;
int n,m;
int a[maxn];
int num[maxm];
int s[maxn][maxm];
int len[1<<maxm];
int dp[1<<maxm];
int dfs(int status)
{
    if(dp[status] != 1<<30)
        return dp[status];
    for (int i = 1; i <= m; ++i)
        if(status & (1<<(i-1)))
        {
            int l = len[status] - num[i] + 1,r = len[status];
            dp[status] = min(dp[status],dfs(status ^ (1<<(i-1))) + num[i] - (s[r][i] - s[l-1][i]));
        }
    return dp[status];
}
int main()
{
    read(n);
    read(m);
    for(int i = 1;i<=n;++i)
        read(a[i]),num[a[i]]++,s[i][a[i]]++;
    for(int i = 1;i<=n;++i)
        for(int j = 1;j<=m;++j)
            s[i][j] += s[i-1][j];
    int maxs = (1<<m) - 1;
    for(int i = 1;i<=maxs;++i)
        dp[i] = 1<<30;
    for(int i = 1;i<=maxs;++i)
        for(int j = 1;j<=m;++j)
            if(i & (1<<(j-1)))
                len[i] += num[j];
    printf("%d",dfs((1<<m)-1));
    return 0;
}
posted @ 2020-04-12 20:31  *Clouder  阅读(156)  评论(0编辑  收藏  举报