翻牌游戏(dfs/状压/暴力)

毛景曜
2023-12-01

此题来源于同学笔试腾讯的一个题。
n n n张卡牌,第 i i i张正面为 a i a_i ai,反面为 b i b_i bi,一开始所有牌都正面朝上,现在可以执行若干操作,每次操作可以选择相邻的牌,交换位置,再翻转它们。求最少的操作次数,使得牌上数字从左到右非降。如果不可做到,输出-1。 n < = 18 n<=18 n<=18
传统做法是递归,同学现场递归代码如下。理论复杂度是 n ! n! n!,剪剪枝就可以过。

#include <iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<cmath>
#include<unordered_map>
#include<map>
#include<queue>
#include<cmath>
#include<set>

using namespace std;
int n;
int a[20];
int b[20];
int ans = 0x3f3f3f3f;
set<int>ss;


void dfs(int pos, int pre, int sum)
{
    if(pos == n){
        //cout <<"gg"<<endl;
        ans = min(ans,sum);
        return ;
    }
    int i = 0;
    set<int>::iterator it,it1;
    it1 = ss.begin();
    it1++;
    for(it = ss.begin(); it !=ss.end();++i){
        int tmp = *it;
        if(tmp%2==pos%2){
            if(a[tmp]>=pre){
                ss.erase(it);
                dfs(pos+1,a[tmp],sum+i);
                ss.insert(tmp);
                it = it1;
                it1++;
            }else{
                ++it;
                ++it1;
            }
        }else{
            if(b[tmp]>=pre){
                ss.erase(it);
                dfs(pos+1,b[tmp],sum+i);
                ss.insert(tmp);
                it = it1;
                it1++;
            }else{
                ++it;
                ++it1;
            }
        }
    }
}

void solve()
{
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for(int i = 0; i < n; i++)
        scanf("%d", &b[i]);
    for(int i = 0; i < n; i++)
        ss.insert(i);
    dfs(0,-0x3f3f3f3f,0);

    if(ans == 0x3f3f3f3f){
        printf("-1\n");
    }else{
        printf("%d\n",ans);
    }
}

int main()
{
    solve();
    return 0;
}

自己写了状压暴力枚举做法,时间复杂度是 n ∗ n ∗ 2 n n*n*2^n nn2n记录下。

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
const int maxn = 10010;
/*
由于每种牌最终显示结果只有2种情况,要么正面,要么反面
再结合数据范围n = 18,考虑状压dp,枚举每一种情况,时间为2^n

【状压引入】 
对于当前的s = 0100101 (用二进制来看)
那么其表示第2、5、7张牌为反面(因为其对应的bit为1),其余都为正面。
 
根据当前的s可以推出最终每张牌的显示结果(正/反) ,且它们是已经有序的 (我们记录其状态为s1) 
那么现在问题就是判断初始的所有都为正面的牌(我们记录其状态为s0) ,能否通过排序得到最终的状态s1
具体怎么判断?我们可以看每张在s0的位置,和在s1的位置的差值。
如果相差为偶数次,说明在排序过程,他翻转了偶数次,最终显示结果为正面 ;
如果相差为奇数次,说明在排序过程,他翻转了奇数次,最终显示结果为反面 ;

我们要做的,就是根据其差值是奇数还是偶数,来看能否和s1中的相应牌的正、反面 对应,如果不能对应,
说明状态s0到s1是不可达的。
如果s0和s1是可达的,那么我们就更新ans。

 
*/ 
int n;
struct node {
	int x[2];
}a[20];
struct node2 {
	int val,pos;
	int p;//0-正 1-反 
}b[20];
bool cmp(const node2 &a,const node2 &b) {
	return a.val < b.val;
}
int main() {
	
	scanf("%d",&n);
	for(int i = 0;i < n;++i)
		scanf("%d",&a[i].x[0]);
	for(int i = 0;i < n;++i)
		scanf("%d",&a[i].x[1]);
	
	//状压
	int all = 1<<n;
	int ans = -1;//-1表示无解 
	/*枚举所有情况,0表示正面,1表反面*/
	for(int s = 0; s < all;++s) {
		for(int i = 0;i < n;++i) {
			if(s&(1<<i)) {
				b[i].val = a[i].x[1];
				b[i].p = 1;//反面 
			}
			else {
				b[i].val = a[i].x[0];
				b[i].p = 0;//正面 
			}
			b[i].pos = i;
		}
		sort(b,b+n,cmp);
		bool flag = 1;//用于判断排序前和排序后,每个元素的翻转次数是否可以和正/反面匹配 
		int tmp = 0;
		for(int i = 0;i < n;++i) {
			int x = abs(i-b[i].pos);
			//正面说明翻了偶数次,反面说明翻了奇数次 
			//翻了多少次,走了多少步 
			if(x%2 != b[i].p) {//illegal
				flag = 0;
				break;
			}
			for(int j = i+1;j < n;++j) {//求逆序对数 
				if(b[i].val < b[j].val && b[i].pos > b[j].pos) 
					++tmp;
			}
		}
		if(flag) {
			if(ans == -1) ans = tmp;
			else if(ans > tmp) ans = tmp;
		}
	} 
	printf("%d\n",ans); 
}
 类似资料: