基本的状压DP

状压DP

给出一个 \(n\timesm\) 要求棋子不相邻,求方案数

\(n < 100\)

\(m < 8\)

\(f[i][s] = \sum{f[i-1][s_2]||s\&s_2 = 0}\)(为零表示不相邻)

\(s\) 表示当前行中棋子的情况

解析:我们会发现一个问题,上述式子确实可以保证上下行不相邻,但是左右呢,对于我这个刚认识状压的菜鸡来说,很懵逼,答:状态压缩的本质:存在一个维度是在进制级别表达多组状态,这个维度的枚举时值得我们思考

状压维度的枚举十分重要,而在这题中,对于同一行不相邻的情况我们可以单独枚举出来,没有代码,但可以口胡

枚举一行中所有可能,做个预处理,用 \(cnt\) 记录即可,在转移方程时就可以按照 \(cnt\) 来枚举,且每个状态都是当前行的合法状态,对于航与航之间的处理,就出现了上述的转移方程式,

for (int i = 1; i <= (m << 1); i++)
    if(legitimate) tmp[++cnt] = a[i];//当前行合法状态
for (int i = 1; i <= n; i++)
	for (int s = 1; j <= cnt; j++)
	{
		....//一些细节
		for (int s2 = 1; s2 <= cnt; s2++)
		{
			if(a[s] & a[s2] == 0)//是否冲突
			f[i][s] += f[i - 1][s2];//合法状态 	
		}
	}

BZOJ 1087 互不侵犯

\(d[i][j][s] = \sum{dp[i][j - cnt[s]][s_2]}\)

cnt[s] 表示 s 中有多少个一,\(s_2\) 表示上一行的状态

\((s<<1|s>>1|s)\&s_2 = 0\) 表示斜方向不冲突

保证这一行没有相邻的,预处理

预处理: 枚举该行所有情况,并记录用的国王数,是否冲突和上边方法一样

int n, k, f[11][160][160], num[160], cnt, s[162], ans;
void pre()
{
  cnt = 0;
  for (int i=0;i<(1<<n);i++)
    {
      if (i&(i<<1)) continue;
      int sum = 0;
      for (int j=0;j<n;j++)
        if(i&(1<<j)) ++sum;
      s[++cnt]=i;
      num[cnt]=sum;
    }
    return;
}

void dp()
{
  f[0][1][0]= 1;
  for (int i=1;i<=n;i++)
    for (int j=1;j<=cnt;j++)
      for (int l=0;l<=k;l++)
      {
        if(l>=num[j])
        {
          for (int t=1;t<=cnt;t++)
          {
            if(!(s[j]&s[t])&&!(s[t]&(s[j]>>1))&& !(s[t]&(s[j]<<1)))
              f[i][j][l]+=f[i-1][t][l-num[j]];
            }
          }
      }
  for (int i=1;i<=cnt;i++)
    ans += f[n][i][k];
  cout<<ans;
  return;
}
 main() {
  scanf("%lld%lld", &n,&k);
  pre();
  dp();
  return 0;
}


位运算

  1. (s&(1<<i)) 判断第 \(i\) 位是不是 \(1\)

  2. s=s|(1<<i) 把第 \(i\) 为设置成 \(1\)

  3. s=s&(~(1<<i)) 把第 \(i\) 位设置成 \(0\)

  4. s^(1<<i) 把第 \(i\) 为的值取反

  5. s=s&(s-1) 把一个数字 \(s\) 二进制下最靠右的第一个 \(1\) 去掉

  6. for (s0=s;s0;s0=(s0-1)&s) 依次枚举 \(s\) 的子集

    for (int s=0;s<=mx;s++)
    	for (int s0=s;s0;s0=(s0-1)&s)
    
    

    我们枚举一下

    \(s=1010\) 此时 \(s_0=1010\)

    减一得 \(s_0=1001\)

    \(s\) 与得 \(s_0=1000\)

    减一得 \(s_0=0111\)

    \(s\) 与得 \(s_0=0010\)

    减一得 \(s_0=0001\)

    \(s\) 与得 \(s_0=0000\)

    这样我们得到了集合 \(s\) 的所有子集 \(s_0\)\(\{11,10,01,00\}\),特别神

    那么时间复杂度是什么

    \(O(3^N)\)

    推导:对于有着 \(k\)\(1\) 的二进制数字,枚举子集需要的时间复杂度为 \(2^k\) , 拥有 \(k\)\(1\) 的数字的数量用组合数学可知:\(\dbinom{n}{k}\)

    那么总的时间复杂度为:\(\sum_{k=0}^nC(n,i)\times2^k\)

    这里给出二项式定理

    \((x+y)^n=\sum_{k=0}^n\dbinom{n}{k}\times x^{(n-k)}\times y^k\)

    我们将这里的 \(x\) 默认为 \(1\), 则 \(y\) 就等于 \(2\)

    那么时间复杂度就为 \((1+3)^n=O(3^n)\)

交并子集 类问题

P5911

子集类问题,大概思路就是

预处理出每种情况的限制条件

在转移时,是先枚举维度状态,在枚举维度状态的子集,将维度状态分成子集合并的形式,这样好理解了,和普通的DP断点是差不多的。

这题的转移方程

\(f[i]=min\{f[j]+T[i\ xor\ j]\}\)

条件是

\(W[i\ xor\ j] <= a\)

int a,b,t[1<<B],w[1<<B],f[1<<B], mt[1<<B], mw[1<<B];

int main() {
//  freopen(".in", "r", stdin);
//  freopen(".out", "w", stdout);
  cin>>a>>b;
  int mx=(1<<b)-1;
  for (int i=1;i<=b;i++) cin>>t[i]>>w[i];
  for (int i=0;i<=mx;i++)
  {
    for (int j=1;j<=b;j++)
      if(i&(1<<(j-1))){
        mt[i]=max(mt[i], t[j]);
        mw[i]+=w[j];
      }
  }
  memset (f,0x3f,sizeof(f));
  f[0]=0;
  for (int i=0;i<=mx;i++)
  {
    for (int j=i;;j=(j-1)&i)
    {
      if(mw[i^j]<=a) f[i]=min(f[i],f[j]+mt[i^j]);
      if(!j) break;
    }
  }
  cout<<f[mx];
  return 0;
}

P3226 [HNOI2012]集合选数


const int A = 1e5 + 11;
const int N = 1e5 + 10;
const int mod = 1e9 + 1;
const int inf = 0x3f3f3f3f;

int n, vis[N], num[N], cnt[1 << 21];
ll ans = 1, f[2][1 << 21];

void sol(int start) {
    int len = 0, height = 0;
    for(int i = start ; i <= n ; i *= 2) {
        ++ height;
        num[height] = 0;
        for(int j = i ; j <= n ; j *= 3) {
            vis[j] = 1;
            ++ num[height];
        }
        len = max(len, num[height]);
    }
    int p = 0;
    int mxs = 1 << len;
    for(int i = 0 ; i < mxs ; ++ i) f[p][i] = 0;
    f[p][0] = 1;
    for(int i = 1 ; i <= height ; ++ i) {
        p ^= 1;
        for(int i = 0 ; i < mxs ; ++ i) f[p][i] = 0;
        for(int s = 0 ; s < mxs ; ++ s) {
            if((s & (s << 1)) == 0 && cnt[s] <= num[i]) {
                for(int t = 0 ; t < mxs ; ++ t) {
                    if((t & (t << 1)) == 0 && cnt[t] <= num[i - 1]) {
                        if((s & t) == 0) {
                            (f[p][s] += f[p ^ 1][t]) %= mod;
                        }
                    }
                }
            }
        }
    }
    
    ll res = 0;
    for(int s = 0 ; s < mxs ; ++ s)
        (res += f[p][s]) %= mod;
        
    (ans *= res) %= mod;
}

int main() {
    for(int s = 0 ; s < (1 << 21) ; ++ s) {
        for(int i = 20 ; i ; -- i) {
            if((s >> (i - 1)) & 1) {
                cnt[s] = i;
                break;
            }
        }
    }
    scanf("%d", &n);
    for(int i = 1 ; i <= n ; ++ i) {
        if(!vis[i]) {
            sol(i);
        }
    }
    printf("%lld\n", (ans % mod + mod) % mod);
}


拓扑序个数问题

给出拓扑图,求不同的拓扑序的方案数

$d[s] $ 表示前s集合的点中都已经在拓扑序中的方案数,转移考虑一下下一个点选什么,下一个选的点药满足他在S中的点选完后的入读0,

木听懂

放松题

1 表示这一个竖着的骨牌的上半部分,即下半部分需要接

0 表示下面不需要接

1的话必须为0

0的话可以为1或0

预处理两个状态是否可以转移,这样可\(O(1)\) 转移

NOIP 愤怒的小鸟

  1. 抛物线的数量为N^2

不同 状态的转移不同,决策是枚举抛物线,

重复计算优化,看课件(没写)

强制编号最小的则就会固定两点,则枚举另一个点n次

优化了一个n

bzoj 3900(有趣)

3次方靠谱,枚举子集

不知道具体是谁的脚,进而不止到和谁交换脚

!对于一个可能合法的配置,那么他们一定保证排序 排序以后他们之间的限制小于 C ,可以不断交换鹿角,得到相邻的奇偶对,(判断是否存在合法解)

合法解判断的上限: n - 1

因为贪心的强制满足鹿的交换,那么最多 n - 1 次

希望把鹿分成组,每组内交换龙角的条件,之前是将所有的鹿角交换,这样就会快许多。

选择尽量的多的合法组使得组合成满足条件全集 S

\[dp[i]= max\{dp[j],dp[i^j]|j是i的一个子集,切实一个合法解的划分方案\} \]

\(dp[i]\) 表示 集合为 i 的并起来是一个合法集合最小数

Hdu3001 放松题

从任意点出发走过

两位压成四位,代替三维

LIS 问题2

给出1-n排列的其中一种最长上升子序列,求原序列可能的种树

n <= 15

旅行商问题

TSP问题

时间复杂度

20 一般是 2^n, 2^n *n

N <= 16 枚举子集

还有一个没写

总结

选出不相交集合合并起来形成全集,而且总权值最大或最小, 最大方案数

可以枚举子集来做,分成子结构

注意:枚举子集如果控制最小的选的话,那么可以减少一点的重复的计算。(愤怒的小鸟)

(。。)实际上不需要所有枚举,以内抛物线不一定经过所有的子集,因此我们枚举的是抛物线的个数。

转移 Dp基本就是这样

特点 n = 16 \(3^n\)

TSP问题,对三进制状态的初探

posted @ 2021-02-17 06:28  zxsoul  阅读(73)  评论(0编辑  收藏  举报