【训练题24:状压DP+前缀和】邦邦的大合唱站队 | 洛谷 P3694

邦邦的大合唱站队 | 洛谷 P3694

难度

提 高 + / 省 选 − \color{cyan} 提高+/省选- +/

− 1.1 k 3 k -\frac{1.1k}{3k} 3k1.1k

题意

  • N N N 个人站成一列,每个人属于编号为 [ 1 , M ] [1,M] [1,M]之中的一个团队。每个团队都有人。
    你要重新安排队列,使得相同团队的人连续站在一起
  • 你需要选出一些人出列(剩下的人不动),然后出列的人在刚刚出列的空位中任意选择归队位置
  • 问你最少出列的人数为多少,

数据范围

1 ≤ N ≤ 1 0 5 1\le N\le 10^5 1N105
M ≤ 20 M\le 20 M20

思路

  • 注意题意,出列的人只能回到刚刚出列的人的空位中去。
  • 如果我们选择队头到队尾的团队编号的全排列然后进行暴力比较,时间复杂度至少也为 O ( M ! ) O(M!) O(M!),会 T L E TLE TLE
  • 注意到,团队个数很少为 20 20 20,这个数字很适合 O ( 2 M ) O(2^M) O(2M)。这个时候,我们可能会机敏地想到状态压缩正好就是这个时间复杂度
  • 现在假设,从队头开始,我们已经排好了一些团队,他们的集合状态设为 i i i。我想知道,排好这些团队需要的最少出列人数是多少,我们用 d p [ i ] dp[i] dp[i] 表示。
  • 考虑转移。我们已经安排好的这么多团队里面,我们不需要知道这些团队的排列顺序是什么样的,我们只需要知道这个排列中最后一个应该安排哪个团队。因为根据 D P DP DP最优子结构,我们无需考虑前面所有的排列顺序,只要知道他们的排列最优答案即可
  • 集合状态为 i i i , 明显所选的所有团队的人数和 r r r 也是固定的。那么明显,所选团队的位置为从队头到第 r r r 个位置。
  • 我们选择已选团队中的某个团队 j j j,想让它安排在已排队伍的最后面。假设该团队的人数为 r e n [ j ] ren[j] ren[j] ,那易得我们所选团队的位置下标为 [ r − r e n [ j ] + 1 , r ] [r-ren[j]+1,r] [rren[j]+1,r]
  • 那么这个时候的出列人数我们该怎么计算呢?对于这个团队 j j j ,如果团队中的人本来就在 [ r − r e n [ j ] + 1 , r ] [r-ren[j]+1,r] [rren[j]+1,r]这个区间,那么他们不需要移动;反之,他们需要移动。
  • 但是我们无需暴力跑一遍区间内的该团队人数,否则会超时。我们使用前缀数组 p r e [ a ] [ b ] pre[a][b] pre[a][b] 表示位置 [ 1 , a ] [1,a] [1,a]有多少个团队编号为 b b b 的人。这样就可以直接 O ( 1 ) O(1) O(1)计算出了。
  • 写成表达式,即为: d p [ i ] = min ⁡ { d p [ i ⊕ j ] + r e n [ j ] − ( p r e [ r ] [ j ] − p r e [ r − r e n [ j ] ] [ j ] ) } dp[i]=\min\Big\{dp[i\oplus j]+ren[j]-(pre[r][j]-pre[r-ren[j]][j])\Big\} dp[i]=min{ dp[ij]+ren[j](pre[r][j]pre[rren[j]][j])}
    其中 ⊕ \oplus 表示异或,它比减法快一点(但是这里和减法等价)

核心代码

时间复杂度 O ( m × 2 m ) O(m\times 2^m) O(m×2m)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 2e6+50;
int pre[MAX][30];
int ren[30];
int dp[MAX];
int main()
{
    
    
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;++i){
    
    
        int t;scanf("%d",&t);
        for(int j = 1;j <= m;++j)pre[i][j]=pre[i-1][j];
        pre[i][t]++;
        ren[t]++;
    }
    fill(dp,dp+MAX,INF);
    dp[0] = 0;
    int ed = (1<<m);
    for(int i = 1;i < ed;++i){
    
    
        int r = 0;
        for(int j = 1;j <= m;++j){
    
    		/// 计算出目前状态有多少个人
            if(i & (1<<(j-1))){
    
    
                r += ren[j];
            }
        }
        for(int j = 1;j <= m;++j){
    
    
            if(i & (1<<(j-1))){
    
    
                dp[i]=min(dp[i],dp[i^(1<<(j-1))] + ren[j]-(pre[r][j] - pre[r-ren[j]][j]));
            }
        }
    }
    cout << dp[ed-1];
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_45775438/article/details/111754134
今日推荐