(一道Div2E不会,我太难了)
题意:
给你一个长度为$n$的颜色序列$A$,每次操作可以选择两个相邻元素交换,求把序列交换成“相同颜色挨在一起”所需的最少操作数。
按颜色排序:设颜色$col$在序列中出现的最左处为$l$,最右处为$r$,则$A_{l},\cdots , A_{r}=col$
$n\leq 4\times 10^5,A_{i}\leq 20$
题解:
根据那个20的范围我们可以考虑一个状压dp的做法。
这是,一般人定义状态都是设$dp(s)$表示排好s中的颜色所需要的最少步数,转移时将其他颜色视为空位。
但这样发现还需要记录状态s的位置,才能计算答案。
这道题计算答案的方法比较巧妙,它的状态是$dp(s)$表示当这个序列中只有s中的颜色时将这个序列变得合法所需要的最少步数。
有什么区别?一个是将其他颜色视为空位,一个是直接抹除其他颜色以及它们所在的位置。
而通过归纳法得知这样转移也是正确的。
那么我们预处理$cnt(i,j)$表示当序列中只有i,j两种颜色时,将颜色i交换到颜色j前面所需的最少步数。
画个图发现转移时累加的答案为$\sum {cnt(i,k)}$
并不难写,复杂度$O(400\times n)$
(我根本就没学明白dp)
代码:
#include<bits/stdc++.h> #define maxn 400005 #define maxm 25 #define inf 0x7fffffff #define ll long long using namespace std; int N,M,A[maxn]; ll dp[1<<maxm-5],cnt[maxm][maxm]; //cnt[i][j]:put i infront j queue<int> q; bool vis[maxm]; inline int read(){ int x=0,f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') f=-1; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f; } int main(){ N=read(),M=20; for(int i=1;i<=N;i++) A[i]=read(); for(int x=1;x<=M;x++) for(int y=1;y<=M;y++){ while(!q.empty()) q.pop(); for(int i=1;i<=N;i++){ if(A[i]==x){ if(q.empty()) continue; int u=q.front(); cnt[x][y]+=(ll)q.size(); q.pop(),q.push(i); } else if(A[i]==y) q.push(i); else continue; } } memset(dp,63,sizeof(dp)); dp[0]=0; for(int s=0;s<(1<<M);s++){ memset(vis,0,sizeof(vis)); for(int i=1;i<=M;i++){ if(s&(1<<i-1)) vis[i]=1; else vis[i]=0; } for(int i=1;i<=M;i++){ if(s&(1<<i-1)) continue; ll ans=0; for(int j=1;j<=M;j++) if(vis[j]) ans+=cnt[i][j]; dp[s|(1<<i-1)]=min(dp[s|(1<<i-1)],dp[s]+ans); } } printf("%I64d\n",dp[(1<<M)-1]); return 0; }