【状压DP】

【状压DP】

O(2^n)
n<20时常用(1e6)
状态压到一个二进制数里

TSP问题
拓展:轮廓线dp,插头dp

概念

利用整型status表示状态,利用status位信息
->某个样本是否还能使用,然后利用信息进行尝试
写出尝试的递归函数->记忆化搜索->严格位置依赖的动态规划->空间压缩等优化

状态

一般是2^k
status范围:(0-2^k-1)
->一般样本量在20个以内

例题

我能赢嘛

https://leetcode.cn/problems/can-i-win/description/

题目大意

image

思路

※任何动态规划都只关注最关键的可变参数,被决定的可变参数不用管!不重要

两个可变参数
status:还有哪些数字可以使用
rest:被status决定 m-已经被挑走的部分就是rest
->只对status挂缓存表(dp数组)

代码

int n,m;
    
    /*
        f:数字范围1-n,当前的先手面对status给定的数字状态
          在累加和还剩rest的情况下
          当前的先手能不能赢
        
        状态表示:
        刚开始是全1(0位不用)
        1 1 1 1 1 1 1 1
        7 6 5 4 3 2 1 0
        有用过就置0
        1 0 1 1 0 1 1 1
        7 6 5 4 3 2 1 0
        
        dp[status]==0 没算过
        dp[status]==1 算过,答案为true
        dp[status]==-1 算过,答案为false
    */
    vector<int> dp;
    bool f(int status,int rest){
        if(rest<=0) return false;
        if(dp[status]!=0) return (dp[status]==1);
        bool ans=0;
        for(int i=1;i<=n;i++){
            //注意这里递归是怎么调的:对手做先手的情况下把这个位数调走了 但没赢->我调走了我能赢->break
            if((status&(1<<i))!=0 && !f((status^(1<<i)),rest-i)){
                ans=1;
                break;
            }
        }
        dp[status]=(ans?1:-1);
        return ans;
    }
       
    bool canIWin(int maxChoosableInteger, int desiredTotal){
        n=maxChoosableInteger;
        m=desiredTotal;
        dp.resize((1<<(n+1))+5,0);
        if(desiredTotal==0) return true;
        if((maxChoosableInteger+1)*maxChoosableInteger/2<desiredTotal) return false;
        return f(((1<<(maxChoosableInteger+1))-1),desiredTotal);
    }

题目积累

https://ac.nowcoder.com/acm/contest/75174/F
https://www.acwing.com/problem/content/description/293/

小红的陡峭值(五)(easy)

https://ac.nowcoder.com/acm/contest/103152/F

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const ll MOD=1e9+7;
/*
【思路】
n = 10 -> n!
n = 15~25 -> 2^n可接受 ->状态压缩 -> 状压dp

【状压dp介绍】
o o o o o o o o o o o o o o o o o o
1 0 1 1 0 0 0 0 1 0 0 0 0 0 1 1 0 1 <= (1<<19)-1
->S
dp[S] -> a[1]选了 a[2]没选 a[3]a[4]选了 ......

dp[S]这个状态下选择a[17] -> dp[S|(1<<17)]

dp[S][a[16]] -> dp[S|(1<<17)][a[17]] 代价:多了个陡峭值abs(a[16]-a[17]) * f[cnt-2](乘上去掉这两个数的贡献) 除了16,17外的其他数字的排列个数

【状压dp】
dp[i,j] : i是状态(2^n) dp[1<<n][n]
dp[i,j] : 用了的数字的状态是 i , 用的最后一个数字是 a[j](a[j]总贡献)

状态并没有展示出顺序

最终状态:dp[1<<(n+1)-1][j]

*/
//概率问题:/n阶乘->乘逆元即可
ll qmi(ll a,ll k,ll p){
	ll res=1;
	while(k){
		if(k&1) res=res*a%p;
		k>>=1;//删去k的末位 
		a=a*a%p;
	}
	return res;
}
int n;
void solve(){
      cin>>n;
      vector<ll> a(n);
      vector<ll> f(n+1,1);//求阶乘
      vector<vector<ll>> dp( 1<<n ,vector<ll>(n));
      for(int i=0;i<n;i++){
            cin>>a[i];
            f[i+1]=f[i]*(i+1)%MOD;
      }
      for(int i=0;i<1<<n;i++){
            int c=0;
            for(int j=0;j<n;j++){//i的二进制中1的个数
                  if((i>>j)&1) c++;
            }
            for(int j=0;j<n;j++){
                  if((i>>j)&1){//i的二进制第j位是1 -> dp[i][j]:已用状态是i,结尾数字式a[j]
                        for(int k=0;k<n;k++){
                              if((i>>k)&1) continue;//i二进制第k位不能为1
                              //考虑去哪里:dp[i,j] -> dp[i|(1<<k)][k]
                                                                    //注意这里为什么不是c-2:该点还没被算进去
                              dp[i|(1<<k)][k]+=(dp[i][j]+abs(a[j]-a[k])*f[c-1]%MOD)%MOD;
                              dp[i|(1<<k)][k]%=MOD;
                        }
                  }
            }
      }
      ll ans=0;
      //计算总贡献
      for(int j=0;j<n;j++){
            ans+=dp[(1<<n)-1][j];
            ans%=MOD;
      }
      //算概率:求逆元即可
      ans=(ans*qmi(f[n],MOD-2,MOD))%MOD;
      cout<<ans;
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      solve();
      return 0;
}
posted @ 2025-03-20 00:49  White_ink  阅读(13)  评论(0)    收藏  举报