2441:Arrange the Bulls全场最直白

题目大意

题目传送门
有n头牛,每头牛有自己喜欢住的屋子,问使所有牛都住上自己喜欢的屋子有几种可能。

思路

先说几个变量吧

  • S S S:牛棚的集合,如果有四个牛棚, S = 1111 S=1111 S=1111时表示,使用了所有的牛棚(每个二进制位对应一个牛棚)。涉及到集合的整数表示
  • d [ i ] [ j ] d[i][j] d[i][j]:表示牛 i i i喜欢牛棚 j j j,是输入条件之一。
  • d p [ S ] dp[S] dp[S],本来应该是 d p [ i ] [ S ] dp[i][S] dp[i][S]表示用 S S S集合,容纳前 i i i个牛的可能结果数,但是这样数组太大了( ∣ S ∣ = 2 m − 1 |S|=2^m-1 S=2m1),实际上我们只需要一个数组来记录状态即可(稍后会讲原因)。

首先我们肯定需要两个循环

	for (int i = 1; i <= n; i++) {//从一头牛开始到所有牛结束
		for (int j = 1; j < (1 << m) - 1; j++) {//遍历此时每种牛棚的状态

		}
	}

然后我们考虑一下,

  • 什么时候可以进行状态转移?比如现在已经进行到了第四头牛,那么只有 ∣ j ∣ = 3 |j|=3 j=3的时候,我们再安排当前的第4头牛,才能形成一个合理的结果,也就是说if(cnt1(j))==i-1)cnt1函数直接统计 j j j的二进制有几个1.
  • 你可能会说,如果 j = 11010 j=11010 j=11010,我们第四头牛可以放在四号位,也就是说安排之后 j ′ = 11110 j'=11110 j=11110是一种情况。那么如果 j = 11011 j=11011 j=11011肯定也是可以的,是的,但是如果11011你也记录在内,那么肯定会多记录一种情况,因为所有三个1的情况我们都会遍历到。而 11011 11011 11011也就包含了 11010 11010 11010的情况,因此cnt1(j)必须严格等于 i − 1 i-1 i1,而不是大于等于。
  • 如果进行状态转移?上面的条件保证 d p [ S ] dp[S] dp[S]已经分配了三个牛棚出去,但是我们也不知道牛 i i i要使用哪个牛棚,因此我们遍历所有的牛棚for(int k=1;k<=m;k++),如果牛棚 k k k还没有使用而且牛 i i i对该牛棚有意思!((1<<k)&j) && d[i][k],那我们就可以把这个牛分配进去。相应的牛棚集合为 j ∣ ( 1 < < k ) j|(1<<k) j(1<<k),状态转移公式:dp[j|(1<<k)]+=dp[j]

补全for循环

	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < (1 << m); j++) {
			if (cnt1(j) == i - 1) {
				for (int k = 1; k <= m; k++) {
					if (!((1 << k)&j) && d[i][k]) {
						dp[j | (1 << (k - 1))] += dp[j];//移位k-1需要注意
					}
				}
			}
		}
	}

不妨跑一下样例

3 4
2 1 4
2 1 3
2 2 4
i=1: j=0: dp[0001]+=dp[0000] dp[1000]+=dp[0000]
	 dp[1]=1 dp[8]=1
i=2: j=1(0001): dp[0101]+=dp[0001](1用过了可以放到3上)
	 j=2(0010): dp[0011]+=dp[0010] dp[0110]+=dp[0010]
	 j=4(0100): dp[0101]+=dp[0101]3用过了可以放到1上)
	 j=8(1000): dp[1001]+=dp[1000] dp[1100]+=dp[1000]
	 dp[5]=1 dp[9]=1 dp[12]=1
i=3: j=3(0011): dp[1011]+=dp[0011](2号位用了可以用4)
	 j=5(0101): dp[0111]+=dp[0101] dp[1101]+=dp[0101]
	 j=6(0110): dp[1110]+=dp[0110](2号位用了可以用4)
	 j=9(1001): dp[1011]+=dp[1001](4号位用了可以用2)
	 j=10(1010):continue;
	 j=12(1100):dp[1110]+=dp[1100](4用了可以用2)
	 dp[7]=1 dp[13]=1 dp[11]=1 dp[14]=1
一共四种情况

不知道你有没有发现, i i i的值每次变化, j j j都是在不同的区间进行计算的,而且他修改的值没有发生过任何一次覆盖,这就是为甚我们可以只需要一个dp数组,这不是偶然的。。。
最后的问题就是,我们要怎样把所有情况统计出来,可以在dp数组中进行遍历,统计所有 ∣ j ∣ = = n |j|==n j==n的情况之和。

	for (int j = 1; j < (1 << m); j++) {
		if (cnt1(j) == n) res += dp[j];
	}
	cout << res << endl;
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <algorithm>
using namespace std;

#define MAX 22
#define inf 1e9

int n, m, d[MAX][MAX], dp[(1 << MAX) - 1], res = 0;

int cnt1(int a) {
	int ans = 0;
	while (a > 0) {
		if (a & 1) ans++;
		a >>= 1;
	}
	return ans;
}
int main() {
	cin >> n >> m;
	memset(d, 0, sizeof(d));
	for (int i = 1; i <= n; i++) {
		int a, b; cin >> a;
		while (a--) {
			cin >> b; d[i][b] = 1;
		}
	}
	dp[0] = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < (1 << m); j++) {
			if (cnt1(j) == i - 1) {
				for (int k = 1; k <= m; k++) {
					if (!((1 << (k - 1))&j) && d[i][k]) {
						dp[j | (1 << (k - 1))] += dp[j];
					}
				}
			}
		}
	}
	for (int j = 1; j < (1 << m); j++) {
		if (cnt1(j) == n) res += dp[j];
	}
	cout << res << endl;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值