tencent 04 26 C 状压DP入门

题目

n张牌,正反两面都有数字,给初始序列。

一次操作交换相邻的两张牌并翻转这两张牌。

求问能否最后所有朝上的牌构成非减序列

2<= n<=18

题解

显然数值范围没意义,因为就算很大,离散一下也就最多36个

显然根据交换规则,把i放到j后,它的正反面是确定的

所以排列的状态数是n!,18的阶乘就不要想枚举了

这题的边界感觉也能很难想,所以有想过的一个骗分方向是迭代加深,但是估计只能骗分答案比较小,或者n比较小的点


有人贪心100%了?? 不知道腾讯出题的人在干嘛?这样明显一堆hack点的能100%,叫那些写状压的没满的怎么想


前置知识:冒泡排序操作数 = 逆序对个数。

这题目只看操作过程中坐标的变化,实际就是在进行冒泡排序,所以我们的步数也就是 最初顺序标号变成最后顺序标号的逆序对个数


状态设计[前i个,使用的元素的state,第i个用的是下标j]

注意到state用0/1来表示是否选择,1的个数就是i,所以可以省略为[state,j]

这个状态对应的值 dp[state,j]为 已经选出的元素的逆序对的最小值

转移方程为

dp[state][j] = min(dp[state 去掉 j][for剩余可选] + state剩余部分和j构成的逆序对)

注意到state内部顺序不影响state剩余部分 和 j构成的逆序对的值

= min(dp[state 去掉 j][for剩余可选]) + state剩余部分和j构成的逆序对

所以 一个状态的最小,是从去掉最后一个的最小来的

(感觉这里局部最优/最小性证明有点绕,或者可以用归纳证?

注意保证题目限制非减序列别忘了

Code

不是我写的,链接在下方,加了点注释

1
2
3
作者:qin_peng
链接:https://www.nowcoder.com/discuss/417957?type=0&order=0&pos=77&page=1
来源:牛客网
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<bits/stdc++.h>
using namespace std;
#define me(a,x) memset(a,x,sizeof(a))
#define sc scanf
#define pr printf
#define IN freopen("in.txt","r",stdin);
#define OUT freopen("out.txt","w",stdout);
typedef long long ll;
typedef unsigned long long ull;
const int N=1e6+6;
const int mod=1e9+7;
int O(){putchar('\n');return 0;}template<typename T,typename... Ty>
int O(const T& a,const Ty&... b){cout<<a<<' ';return O(b...);}
void I(){}template<typename T,typename... Ty>void I(T& a,Ty&... b){cin>>a;I(b...);}
template<typename T>void db(T *bg,T *ed){while(bg!=ed)cout<<*bg++<<' ';pr("\n");}
inline ll mul_64(ll x,ll y,ll c){return (x*y-(ll)((long double)x/c*y)*c+c)%c;}
inline ll ksm(ll a,ll b,ll c){ll ans=1;for(;b;b>>=1,a=a*a%c)if(b&1)ans=ans*a%c;return ans;}
int n,a[20],b[20];
int dp[1<<20][20];
const int INF=1e9;
int main(){
I(n);
for(int i=0;i<n;i++)sc("%d",&a[i]);
for(int i=0;i<n;i++)sc("%d",&b[i]);
for(int i=1;i<n;i+=2)swap(a[i],b[i]);
for(int i=0;i<(1<<n);i++){
for(int j=0;j<n;j++){
dp[i][j]=INF; // 不可达
}
}
for(int i=0;i<n;i++)dp[1<<i][i]=0; // 选单个 逆序对为0
for(int i=0;i<(1<<n);i++){ // 保证子状态先于父状态计算
for(int j=0;j<n;j++){
if(dp[i][j]==INF)continue;
for(int k=0;k<n;k++){
if(i>>k&1)continue;
int x=__builtin_popcount(i)&1; // 内置计算1的个数
if(x){ // 非减 限制
if(b[k]<a[j])continue;
}else{
if(a[k]<b[j])continue;
}
int ans=0;
for(int l=k+1;l<n;l++){ // 计算 state补上k以后 state与k之间新形成的逆序对
if(i>>l&1)ans++;
}
dp[i|1<<k][k]=min(dp[i|1<<k][k],dp[i][j]+ans); // 递推感觉局部最小性有点奇怪?只有我?但是从上向下看是没问题的。所以逻辑一致没问题
}
}
}
int ans=INF;
for(int i=0;i<n;i++)ans=min(ans,dp[(1<<n)-1][i]);
if(ans==INF)O(-1);
else O(ans);
}

总结

很早看到18 就条件反射的想2的n次方

看到交换两个也想到了逆序对和冒泡

但没想出最后的状态以及转移,果然设计状态有点难啊,我好菜哇

大概技术总结的话应该是 除了最简单的状态,还有一种是[state][末尾选择],这样的话能保证性质就行,如这里的末尾选择就是用来保证 非减少的