【题解】P3694 邦邦的大合唱站队(状压 DP,前缀和)

【题解】P3694 邦邦的大合唱站队

状压 DP 好题。

题目链接

P3694 邦邦的大合唱站队

题意概述

\(n\) 个偶像排成一列,他们来自 \(m\) 不同的团队队,每个团队都至少有一个偶像。

现在要求重新安排队列,使来自同一乐队的偶像连续的站在一起。重新安排的办法是,让若干偶像出列(剩下的偶像不动),然后让出列的偶像一个个归队到原来的空位,归队的位置任意。问最少需要让多少个偶像出队。

数据范围

对于 \(20\%\) 的数据,\(N\le 20, M=2\)

对于 \(40\%\) 的数据,\(N\le 100, M\le 4\)

对于 \(70\%\) 的数据,\(N\le 2000, M\le 10\)

对于全部数据,\(1\le N\le 10^5, M\le 20\)

思路分析

首先这个数据范围很显然的状压 DP。

我们可以考虑用 \(dp_{sta}\) 表示把 \(sta\) 这个状态归位的最小步数。

考虑转移。

我们每一次可以枚举下一次选择要归位的是哪个团队。

那么新产生的贡献怎么算呢?

似乎比较常规的思路是考虑当前情况下,一个团队连续最多的“段”在哪,然后把其它剩下的人都扔到那个“段”里面去。但是不好转移。。

(上面这句话看不懂无所谓因为只是我自己瞎 yy 的一种思路)

我们可以考虑直接每次把新的要归位的团队放在当前已经归位的团队后面。

换句话说,假如我们现在已经把前 \(i-1\) 个团队归位了并且已经占用了从 \(1\)\(k\) 的所有位置,那么我们就直接从 \(k+1\) 开始归位这个新的团队,把新的团队全部放在后面即可。

我刚开始思考的时候,觉得这样直接转移可能会导致不是最优解,因为这样按顺序排不一定是最优解。但是其实由于我们在枚举 \(sta\)\(i\) 的时候把所有可能的顺序都算进去了,所以最终答案一定是最优解。

根据这个思路我们需要维护以下数组:

  • \(sum_{i,j}\) 表示在原来的队伍中从 \(1\)\(j\) 有多少个属于第 \(i\) 个团队的人。那么 \(sum_{i,r}-sum_{i,l-1}\) 就表示区间 \([l,r]\) 内有多少个属于第 \(i\) 个团队的人。这个数组可以直接枚举 \(i,j\) 然后前缀和预处理出来。
  • \(cnt_i\) 表示队伍中有多少个第 \(i\) 个团队的人。可以直接预处理出来。
  • \(val_{sta}\) 表示 \(sta\) 这个状态总共有多少个人。也可以直接枚举每个状态然后预处理出来。

有了上面三个个数组,我们就可以轻松的转移 \(dp\) 数组啦!

那么有:

\[dp_{sta|(1<<i)}=\min(dp_{sta|(1<<i)},dp_{sta}+cnt_i-(sum[i][cnt_i+val_{sta}]-sum[i][val_{sta}]) \]

总时间复杂度:\(O(mn+2^mm)\)

实现代码

//luoguP3694
//2022.9.23 11:16
#include<iostream>
#include<cstdio>
#include<cstring>
//#define debug
using namespace std;
const int maxn=1e5+10;
const int maxm=21;
int a[maxn],cnt[maxm],sum[maxm][maxn],dp[1<<maxm],val[1<<maxm];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

int main()
{
	int n,m;
	n=read();m=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		cnt[a[i]]++;
	}
	for(int sta=0;sta<(1<<m);sta++)
	{
		for(int i=1;i<=m;i++)
		{
			if(sta&(1<<(i-1)))val[sta]+=cnt[i];
		}
	}
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=n;j++)
		{
			sum[i][j]=sum[i][j-1]+(a[j]==i);
		}
	}
	memset(dp,0x3f3f3f,sizeof(dp));
	dp[0]=0;
	for(int sta=0;sta<(1<<m);sta++)
	{
		for(int i=1;i<=m;i++)
		{
			dp[sta|(1<<(i-1))]=min(dp[sta|(1<<(i-1))],dp[sta]+cnt[i]-(sum[i][cnt[i]+val[sta]]-sum[i][val[sta]]));
		}
	}
	cout<<dp[(1<<m)-1]<<'\n';
	return 0;
}
/*
  cnt[i] 表示第 i 个队有多少人。
  sum[i][j] 表示第 i 个队从 1 到 j 有多少人。
  val[sta] 表示 sta 这个状态总共有多少人。
  dp[sta] 表示 sta 这状态最少需要几次归位。
 */
posted @ 2022-09-23 16:10  向日葵Reta  阅读(43)  评论(0编辑  收藏  举报