难度
提 高 + / 省 选 − \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
1≤N≤105
M
≤
20
M\le 20
M≤20
思路
- 注意题意,出列的人只能回到刚刚出列的人的空位中去。
- 如果我们选择队头到队尾的团队编号的全排列然后进行暴力比较,时间复杂度至少也为 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] [r−ren[j]+1,r]
- 那么这个时候的出列人数我们该怎么计算呢?对于这个团队 j j j ,如果团队中的人本来就在 [ r − r e n [ j ] + 1 , r ] [r-ren[j]+1,r] [r−ren[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[i⊕j]+ren[j]−(pre[r][j]−pre[r−ren[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;
}