题目大意
题目传送门
有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∣=2m−1),实际上我们只需要一个数组来记录状态即可(稍后会讲原因)。
首先我们肯定需要两个循环
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 i−1,而不是大于等于。 - 如果进行状态转移?上面的条件保证
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;
}