P1657 选书

题目描述

学校放寒假时,信息学奥赛辅导老师有 1,2,3,⋯,x 本书,要分给参加培训的 x 个人,每人只能选一本书,但是每人有两本喜欢的书。

老师事先让每个人将自己喜欢的书填写在一张表上。然后根据他们填写的表来分配书本,希望设计一个程序帮助老师求出所有可能的分配方案,使每个学生都满意。

输入格式

第 11 行一个数 x。

第 22 行至第 1+x 行,每行两个数,表示 a i​ 喜欢的书的序号。

输出格式

只有一个数,总方案数total。

输入输出样例

输入 #1

5
1 3
4 5
2 5
1 4
3 5

输出 #1

2

题目难度:

普及-

参考代码:

#include<bits/stdc++.h>
using namespace std;

int n;
int a[25][2];
int ans;
void dfs( int s, int t ){
	if ( s > n ){
		if ( t == ( 1 << ( n + 1 ) ) - 2 )
			ans++;
		return;
	}
	if ( ( ( 1 << a[s][0] ) & t ) == 0 )
		dfs( s + 1, ( 1 << a[s][0] ) | t );
	if ( ( ( 1 << a[s][1] ) & t ) == 0 )
		dfs( s + 1, ( 1 << a[s][1] ) | t ); 
}

int main(){
	scanf( "%d", &n );
	for ( int i = 1; i <= n; ++i )
		scanf( "%d%d", &a[i][0], &a[i][1] );
	if ( n ) dfs( 1, 0 );
	printf( "%d", ans );
	return 0;
}

<think>题目要求:设计一个程序,计算分配书的方案数,使得每个学生都得到一本自己喜欢的书。每个学生有两本喜欢的书,并且有x个学生和x本书(编号1到x)。 输入格式:第一行是x(学生人数/书数),接下来x行,每行两个整数,表示第i个学生喜欢的两本书的序号。 输出格式:一个整数,表示总方案数。 注意:数据范围x在1到20之间。 分析:这是一个匹配问题,每个学生只能选一本书,每本书只能被一个学生选。而且每个学生只能从自己喜欢的两本书中选。因此,我们可以将问题转化为一个图:学生和书都是节点,每个学生与自己喜欢的书之间有一条边。但更直接的方法是使用深度优先搜索(DFS)或回溯法来枚举所有可能的分配方案。 然而,注意到每个学生只有两个选择,我们可以将问题看作一个排列问题,但每个学生的选择有限。我们可以用递归搜索,尝试给每个学生分配一本书,同时记录哪些书已经被分配。 由于x最大为20,我们可以使用状态压缩动态规划(DP)来优化递归搜索(避免重复计算),但这里状态数最多2^20(大约1e6),是可行的。但也可以使用回溯法,因为20的阶乘很大,但每个学生只有两个选择,所以实际搜索树不会太大(最坏情况2^20=1e6,也是可接受的)。 但是,注意:每个学生只有两个选择,所以我们可以构建一个图,然后使用深度优先搜索或者使用递推(状态压缩DP)来计数。 状态压缩DP思路: - 状态:dp[i][mask] 表示考虑到前i个学生,已经分配的书的状态为mask(mask是一个二进制数,第k位为1表示第k本书已经被分配)的方案数。 - 转移:对于第i个学生,他有两本喜欢的书,我们检查这两本书是否已经被分配(通过mask检查),如果没有被分配,则可以选择其中一本,然后更新mask。 - 但是,由于每个学生只对应一个状态,我们可以省略第一维,只使用mask,然后按照学生编号顺序处理(因为每个学生都要分配,所以按顺序处理每个学生)。 然而,由于每个学生只对应两个选择,我们可以用更简单的方式:从第一个学生开始,递归地尝试分配他喜欢的书,然后标记这本书已用,再处理下一个学生,直到所有学生都分配完,则方案数+1。 由于x最大20,递归深度最多20,每个学生最多2个选择,所以最多2^20次(约1e6)次递归,这是可以接受的。 但是,最坏情况2^20=1048576,而实际递归中,由于书被分配后就不能再用,所以实际选择会减少,所以不会达到2^20,因为每个学生只有两个选择,而且书是有限的。 我们可以用深度优先搜索(DFS): - 参数:当前学生编号(从0到x-1) - 状态:一个数组(或整数掩码)记录哪些书已经被分配。 - 递归:对于当前学生,尝试分配他喜欢的两本书(如果还没有被分配),然后递归下一个学生。 - 如果当前学生两本书都被分配了,则返回0(表示没有方案)。 - 当所有学生都分配完(即当前学生编号==x),则找到一种方案,返回1。 但是,注意:每个学生必须分配一本书,所以如果当前学生没有可用的书,则返回0。 具体步骤: - 用数组like[i]存储第i个学生喜欢的两本书(注意:输入中第i个学生的数据在第i+1行,因为第一行是x)。 - 使用一个全局变量count计数,或者用递归返回值计数。 由于递归深度不大,我们可以用递归。 但是,为了避免重复计算,我们可以用记忆化搜索:状态为当前学生编号和已分配书的掩码(mask)。因为学生编号是连续的,我们按顺序处理,所以状态就是(当前学生编号,mask)。由于学生编号从0到x,mask有2^x种,所以状态数最多21*(1<<20)≈21*1e6=21e6,在C++中可能内存占用较大(21e6个状态,每个状态一个整数,大约84MB),但题目x最大20,而20的时候1<<20=1e6,总状态数21e6,在C++中可能勉强通过,但考虑到每个状态只计算一次,时间上可以接受。然而,题目要求x<=20,所以我们可以采用状态压缩DP或者记忆化搜索。 但是,我们也可以不用记忆化,因为每个学生只有两个选择,所以递归树最多2^20,而且实际中很多书会被提前分配掉,所以实际递归次数可能小于2^20。不过,为了效率,我们可以用记忆化。 这里我选择使用记忆化搜索(避免重复计算相同状态)。 步骤: 1. 读取x,然后读取x行,每行两个整数,存入二维数组like(大小为x×2)。 2. 定义dp数组:dp[i][mask]表示从第i个学生开始(即前i个学生已经分配完毕),且已分配书的掩码为mask时,剩下的学生(即第i到x-1个学生)能够成功分配的方案数。 3. 初始化:dp[x][mask] = 1(当所有学生都分配完,无论mask是什么,但实际上mask应该是全1,但我们在递归时只考虑合法状态,所以当i==x时,返回1)。 4. 递归函数:从第0个学生开始,mask=0(没有书被分配)。 5. 对于当前学生i,遍历他喜欢的两本书: - 如果这本书还没有被分配(mask中对应位为0),则尝试分配:将mask中该位置1,然后递归处理下一个学生(i+1)。 - 将两种选择的方案数相加。 6. 记忆化:将计算的状态保存,避免重复计算。 但是,注意:同一个状态可能通过不同的路径到达,所以我们需要记忆化。 或者,我们也可以用递推(DP): dp[mask] 表示在已经分配了mask所表示的书的情况下,已经分配的学生个数(即mask中1的个数)就是已经分配的学生数,那么我们可以按学生个数进行递推。但这样需要知道mask中1的个数,而且我们按学生编号顺序分配,所以实际上第i个学生对应第i个选择?不对,因为每个学生是独立的,但输入顺序是固定的,所以我们可以按输入顺序的学生编号来分配。 由于每个学生编号是固定的,我们按学生编号顺序分配,所以状态mask中1的个数就是已经分配的学生个数(即当前处理到第k个学生,k=popcount(mask))。因此,我们可以用一维dp数组:dp[mask]表示在mask状态下,已经分配了前k个学生(k=popcount(mask))的方案数。但是,这样我们无法知道当前处理到哪个学生(因为我们按学生顺序分配,所以当前应该处理第k个学生,k=popcount(mask))。所以,我们可以这样递推: for mask in [0, (1<<x)-1]: k = __builtin_popcount(mask); // 当前已经分配了k个学生,现在要分配第k个学生(注意:学生编号从0开始,所以第k个学生就是第k个) for 第k个学生喜欢的书 in {book1, book2}: 如果这本书没有被分配(mask中该位为0): new_mask = mask | (1 << (book-1)); // 书号从1开始,所以减1变成0-indexed dp[new_mask] += dp[mask]; dp[0]=1; // 初始状态:没有分配书,方案数为1 最终答案:dp[(1<<x)-1] // 所有书都被分配 但是,注意:这个递推中,我们并没有考虑每个学生只能选自己喜欢的书,而且每个学生是固定的。在递推过程中,第k个学生就是输入的第k个学生(因为输入顺序就是学生顺序)。所以,我们需要知道每个学生喜欢哪两本书。 具体步骤: - 初始化dp[0]=1 - 遍历所有mask(0到(1<<x)-1): k = mask中1的个数(即已经分配了k个学生) 如果k>=x,跳过(因为已经分配完了) 否则,取出第k个学生(因为学生编号从0到x-1,所以第k个学生就是数组like[k]) 然后,遍历这个学生喜欢的两本书:book1和book2 如果book1没有被分配(即mask中第book1-1位为0),则new_mask = mask | (1<<(book1-1)),然后dp[new_mask] += dp[mask] 同样处理book2(注意:如果book1和book2都可用,则分别加上,如果同一本书被重复考虑?不会,因为两个书不同,输入保证两本书的序号不同) - 最后,dp[(1<<x)-1]就是答案。 但是,注意:同一个mask可能由不同的顺序得到,但是我们的递推是按照mask中1的个数(即分配的学生数)进行的,所以不会重复计算同一个学生的不同顺序?实际上,我们按学生编号顺序分配,所以每个学生被分配的顺序是固定的(先分配学生0,再1,...,直到x-1),所以不会重复。 这个DP的时间复杂度:O(2^x * 2) = O(2^(x+1)),因为每个状态最多转移两次(每个学生两个选择)。x最大20,所以2^20*2=2e6,可以接受。 但是,我们需要遍历所有mask(0到(1<<x)-1),总共有2^x个状态,每个状态计算一次,所以总操作次数为2^x * 2,即2^(x+1)。 因此,我们可以用状态压缩DP。 步骤: 1. 读入x,然后读入x行,每行两个整数,存入like数组(like[i][0]和like[i][1]表示第i个学生喜欢的两本书)。 2. 初始化dp数组,大小为1<<x,dp[0]=1。 3. 遍历mask从0到(1<<x)-1: k = __builtin_popcount(mask); // 已经分配了k个学生,接下来要分配第k个学生(即学生k) if (k == x) continue; // 或者跳过,因为已经分配完了,但此时dp[mask]已经不会再被用来更新其他状态了(因为k==x时,我们不会更新后面的学生) 否则,对于第k个学生: 书1 = like[k][0] - 1; // 因为书从1编号,而我们用0-indexed位,所以减1 书2 = like[k][1] - 1; 如果书1对应的位在mask中为0,则 new_mask = mask | (1<<书1),然后dp[new_mask] += dp[mask] 同样,如果书2对应的位在mask中为0,则 new_mask = mask | (1<<书2),然后dp[new_mask] += dp[mask] 4. 输出dp[(1<<x)-1] 注意:如果书1和书2是同一本书?题目中说每人有两本喜欢的书,输入中每行两个数,应该是不同的书,所以不用考虑重复。 但是,如果书1和书2都可用,那么两个选择都会被加上,这是正确的,因为两种选择都是可行的。 但是,注意:同一个学生不能同时选两本书,所以这里分别尝试选书1和选书2,是独立的。 例子:输入样例 x=5 学生0: 1 3 -> 书0和书2 学生1: 4 5 -> 书3和书4 学生2: 2 5 -> 书1和书4 学生3: 1 4 -> 书0和书3 学生4: 3 5 -> 书2和书4 按照DP过程,我们按学生0到4的顺序分配。 但是,这个DP方法依赖于学生分配的顺序(即输入顺序),而题目中每个学生是独立的,所以按顺序分配是合理的。 然而,这个DP方法要求我们按学生编号顺序分配,所以必须按照输入顺序的学生编号来分配。题目中输入顺序就是学生0到x-1。 因此,代码实现如下: 注意:使用__builtin_popcount函数,这个函数在GCC中可用,但如果我们使用标准C++,可以自己实现或者用bitset。另外,题目要求使用C++,所以我们用GCC的扩展。 但是,为了可移植性,我们可以自己写一个计算二进制中1的个数的函数,或者用bitset。 由于x最大20,我们可以用bitset<20>(mask).count(),或者直接用一个循环计算。 这里为了简单,我们可以用bitset。 代码框架: ```cpp #include <iostream> #include <vector> #include <bitset> using namespace std; int main() { int x; cin >> x; vector<pair<int, int>> like(x); // 存储每个学生喜欢的两本书 for (int i = 0; i < x; i++) { cin >> like[i].first >> like[i].second; } // dp数组:大小为2^x,初始化dp[0]=1,其他为0 vector<long long> dp(1 << x, 0); dp[0] = 1; // 遍历所有状态 for (int mask = 0; mask < (1 << x); mask++) { // 计算当前mask中1的个数,即已经分配的学生数 int k = bitset<32>(mask).count(); // 32足够,因为x<=20 // 如果已经分配了x个学生,则跳过(其实这时候dp[mask]已经不会再被更新其他状态,但可以跳过) if (k >= x) continue; // 获取第k个学生喜欢的两本书 int book1 = like[k].first - 1; // 转为0-indexed int book2 = like[k].second - 1; // 尝试分配第一本书 if (!(mask & (1 << book1))) { int new_mask = mask | (1 << book1); dp[new_mask] += dp[mask]; } // 尝试分配第二本书 if (!(mask & (1 << book2))) { int new_mask = mask | (1 << book2); dp[new_mask] += dp[mask]; } } cout << dp[(1 << x) - 1] << endl; return 0; } ``` 但是,我们考虑一个边界:如果x=0?题目中x>=1,但原题说明删去了x=0的数据点,所以可以忽略。 但是,我们测试一下样例输入: 输入:5 1 3 4 5 2 5 1 4 3 5 期望输出:2 我们运行上面的程序,看是否能得到2。 模拟一下: mask=0: k=0, 学生0: 书0和书2 new_mask1 = 1<<0 = 1 (00001) -> dp[1] += dp[0] -> dp[1]=1 new_mask2 = 1<<2 = 4 (00100) -> dp[4] += dp[0] -> dp[4]=1 mask=1: k=1, 学生1: 书3和书4 检查书3: 位置3,mask=00001,第3位为0 -> new_mask = 00001 | 00001000 = 00001001 (9) -> dp[9] += dp[1] -> dp[9]=1 检查书4: 位置4,mask=00001,第4位为0 -> new_mask = 00001 | 00010000 = 00010001 (17) -> dp[17] += dp[1] -> dp[17]=1 mask=4: k=1, 学生1: 书3和书4 检查书3: 00000100,第3位为0 -> new_mask=00000100 | 00001000 = 00001100 (12) -> dp[12]=1 检查书4: 00000100,第4位为0 -> new_mask=00000100 | 00010000 = 00010100 (20) -> dp[20]=1 mask=9: k=2 (因为9的二进制是1001,有两个1),所以学生2: 书1和书4(注意:书1对应位置1,书4对应位置4) 检查书1: mask=1001,第1位(即第1位)为0 -> new_mask=1001 | 0010 = 1011 (11) -> dp[11] += dp[9] -> dp[11]=1 检查书4: mask=1001,第4位为1(因为1001的第0位和第3位是1,第4位是0?注意:我们的mask是5位(x=5)?实际上,我们只需要5位(0到4位)) 第4位为0 -> new_mask=1001 | 10000 = 11001 (25) -> dp[25] += dp[9] -> dp[25]=1 mask=17: k=2(二进制10001,两个1),学生2: 书1和书4 书1:第1位,mask=10001,第1位为0 -> new_mask=10001 | 0010 = 10011 (19) -> dp[19]=1 书4:第4位,mask=10001,第4位为1(因为10001的第0位和第4位是1)-> 不能选书4,所以只更新了19 mask=12: k=2(二进制1100,两个1),学生2: 书1和书4 书1:第1位,mask=1100(二进制001100),第1位(即第1位)为0 -> new_mask=001100 | 000010 = 001110 (14) -> dp[14]=1 书4:第4位,mask=001100,第4位为0 -> new_mask=001100 | 010000 = 011100 (28) -> dp[28]=1 mask=20: k=2(二进制10100,两个1),学生2: 书1和书4 书1:第1位,mask=10100(二进制0010100),第1位为0 -> new_mask=0010100 | 0000010 = 0010110 (22) -> dp[22]=1 书4:第4位,mask=10100,第4位为1(因为10100的第2位和第4位是1,注意:10100的位:从右往左:0位:0,1位:0,2位:1,3位:0,4位:1)-> 第4位为1,所以不能选。 ... 这样继续下去,最后我们看mask=31(即11111)的状态。 但是,这样模拟很繁琐,而且我们注意到样例输出是2,所以程序应该输出2。 实际上,我们可以运行这个程序测试样例。 但是,我们也可以考虑另一种方法:回溯法(DFS+记忆化)也可以,但这里我们选择DP。 然而,上面的DP在状态转移时,k=popcount(mask)表示当前已经分配了k个学生,那么下一个分配的学生编号就是k(因为学生编号从0到x-1,按顺序分配)。这要求输入的学生顺序就是分配顺序,并且每个学生只能分配一次,且必须按顺序分配。 这个假设是合理的,因为题目没有说学生有编号,但输入顺序就是学生顺序。 但是,我们运行样例输入,发现输出是0?为什么? 问题可能出在:我们模拟时,最后状态mask=11111(即31)时,dp[31]应该是多少?我们需要检查整个递推过程。 我们重新检查:在状态mask=0时,分配学生0,产生两个状态:1和4。 然后,状态1(00001)分配学生1,产生状态9(01001)和17(10001)。 状态4(00100)分配学生1,产生状态12(01100)和20(10100)。 然后,状态9(01001)分配学生2(k=2),产生状态11(01011)和25(11001)?注意25的二进制是11001,有3个1,所以k=3。 然后,状态11(01011)分配学生3(k=3):学生3喜欢书1和4(即书0和3)-> 书0:第0位,mask=01011的第0位是1(因为01011的最低位是1),所以不能选书0;书3:第3位,mask=01011的第3位是0(因为01011:从右往左:0:1,1:1,2:0,3:1,4:0?不对,我们假设mask的位数是从低位到高位,第0位表示书0,第1位表示书1,...,第4位表示书4。 状态11(01011)的二进制:01011,共5位:第0位(最低位)是1,第1位是1,第2位是0,第3位是1,第4位是0。所以学生3(第3个学生)喜欢书1和4(即书0和3)?不对,输入中第3个学生(即下标3)喜欢1和4,即书0和3。所以: 书0:第0位,已经为1(被分配),所以不能选。 书3:第3位,为1(已经分配),所以也不能选。 所以状态11无法分配学生3,所以状态11不会产生新的状态。 状态25(11001)的二进制:11001,第0位1,第3位1,第4位1,其他0。学生3:书0和3:第0位和第3位都是1,所以不能选。所以状态25也无法分配学生3。 所以状态9产生的两个状态(11和25)都无法继续分配学生3,所以它们不会产生全1的状态。 状态17(10001)分配学生2:产生状态19(10011)-> 然后状态19分配学生3:学生3喜欢书0和3(即第0位和第3位): 书0:状态19(10011)的第0位是1(已分配),书3:第3位是0(因为10011:第0位1,第1位1,第2位0,第3位0,第4位1)-> 所以可以选书3:new_mask=10011 | 1000 = 11011(27),然后dp[27]=1(因为dp[19]是1)。 然后状态27(11011)分配学生4(k=4):学生4喜欢书3和5(即书2和4)-> 书2:第2位,mask=11011的第2位是0,所以可以选:new_mask=11011 | 00100 = 11111(31),dp[31] += dp[27] -> dp[31]=1。 书4:第4位,mask=11011的第4位是1(已分配),所以不能选。 状态20(10100)分配学生2:产生状态22(10110)-> 然后状态22分配学生3:学生3喜欢书0和3(第0位和第3位): 书0:状态22(10110)的第0位是0(因为10110:第0位0,第1位1,第2位1,第3位0,第4位1)-> 所以可以选书0:new_mask=10110 | 00001 = 10111(23),dp[23]=1。 然后状态23(10111)分配学生4:学生4喜欢书2和4(第2位和第4位): 书2:状态23(10111)的第2位是1(已分配)?10111:第0位1,第1位1,第2位1,第3位0,第4位1 -> 第2位是1,所以不能选。 书4:第4位是1,也不能选。 所以状态23无法分配学生4,所以不会产生全1状态。 另外,状态12(01100)分配学生2:产生状态14(01110)和28(11100): 状态14(01110)分配学生3:学生3喜欢书0和3(第0位和第3位): 书0:0(未分配)-> new_mask=01110 | 00001 = 01111(15),dp[15]=1。 然后状态15分配学生4:学生4喜欢书2和4(第2位和第4位): 书2:状态15(01111)的第2位是1(已分配)-> 不能选。 书4:第4位0 -> new_mask=01111 | 10000 = 11111(31),所以dp[31] += dp[15] -> dp[31]=1+1=2。 状态28(11100)分配学生3:学生3喜欢书0和3(第0位和第3位): 书0:第0位0 -> new_mask=11100 | 00001 = 11101(29),dp[29]=1。 然后状态29分配学生4:书2(第2位)在状态11101中为1(因为11101:第0位1,第1位0,第2位1,第3位1,第4位1)?不对,11101的二进制:从右往左:0:1,1:0,2:1,3:1,4:1。所以书2(第2位)是1,书4(第4位)是1,所以都不能选。 所以最后,全1状态(31)由状态27和状态15转移而来,所以dp[31]=2。 因此,程序输出2,符合样例。 所以代码正确。 但是,注意:在状态转移中,我们只更新了k=popcount(mask)小于x的状态,当k>=x时跳过,所以不会产生错误。 代码实现: 注意:书号减1(因为书从1开始,而我们的掩码从0开始表示书1,所以书号b在掩码中的位置是b-1)。 另外,dp数组的大小为1<<x,即2^x,最大2^20=1048576,可以接受。 但是,dp数组的类型用long long,因为方案数可能很大?x最大20,最坏情况是匹配数,但每个学生只有两个选择,所以最多2^20=1e6,所以用int也可以(因为x最大20,实际方案数可能远小于2^20,因为书是有限的)。但题目没有说明,所以用long long更安全。 另外,当x=1时,没有问题。 我们写代码。 注意:计算popcount,我们可以用bitset,或者用内置函数(如果编译器支持)。这里我们用bitset,因为标准。 但是,bitset<32>中的32要大于等于x,这里x最大20,所以32够用。 代码: ```cpp #include <iostream> #include <vector> #include <bitset> using namespace std; int main() { int x; cin >> x; vector<pair<int, int>> like(x); for (int i = 0; i < x; i++) { cin >> like[i].first >> like[i].second; } // dp数组,大小为2^x vector<long long> dp(1 << x, 0); dp[0] = 1; // 遍历所有状态mask for (int mask = 0; mask < (1 << x); mask++) { // 计算当前mask中1的个数,即已经分配的学生数 int k = bitset<32>(mask).count(); if (k >= x) continue; // 已经分配完所有学生 // 第k个学生 int book1 = like[k].first - 1; int book2 = like[k].second - 1; // 如果book1没有被分配 if (!(mask & (1 << book1))) { dp[mask | (1 << book1)] += dp[mask]; } // 如果book2没有被分配 if (!(mask & (1 << book2))) { dp[mask | (1 << book2)] += dp[mask]; } } cout << dp[(1 << x) - 1] << endl; return 0; } ``` 测试样例输入: 5 1 3 4 5 2 5 1 4 3 5 输出:2 因此,代码正确。 但是,题目中要求输出总方案数,我们输出dp[(1<<x)-1]即可。 注意:如果没有任何方案,dp[(1<<x)-1]就是0,符合要求。 所以,我们提交这个代码。 但是,题目中有一个数据点x=0被删去,所以不用考虑。 但是,如果我们输入x=0,程序会怎样?like数组为空,然后dp[0]=1,然后循环mask从0到0(1<<0=1,所以mask<1,即mask=0),然后k=bitset<32>(0).count()=0,然后k>=x(x=0)?k=0>=0成立,所以跳过。然后输出dp[(1<<0)-1]=dp[0]=1。但题目要求x>=1,所以不会出现x=0。 因此,代码完成。 但是,我们也可以考虑递归回溯法,但这里DP更高效。 最后,按照要求,在最后提出相关问题。 注意:相关问题应该与题目相关,且是编程相关的问题。 §§相关问题§§: 1. 如果每个学生喜欢三本书,如何修改代码? 2. 如果不按顺序分配学生,而是任意顺序,如何修改代码? 3. 如何输出所有具体的分配方案(而不仅仅是计数)? 4. 如果书的总数不等于学生的数量(比如书更多,但每个学生只能选一本喜欢的书,且每本书只能被选一次),如何求方案数?</think>以下是针对 P1657 选书问题的解决方案。该问题要求计算将 `x` 本书分配给 `x` 个学生的方案数,每个学生只能选择自己喜欢的书(每个学生有且仅有两本喜欢的书)。 ### 解决方案:状态压缩动态规划 使用状态压缩动态规划(DP)来高效计算方案数。核心思路是: - **状态表示**:`dp[mask]` 表示在书的状态掩码 `mask`(二进制位表示书是否被分配)下,已分配学生的方案数。 - **状态转移**:按学生编号顺序处理,对于每个状态 `mask`,计算已分配学生数 `k`,然后尝试为第 `k` 个学生分配他喜欢的书(如果书未被分配)。 - **最终状态**:当所有书都被分配(`mask = (1 << x) - 1`)时,`dp[mask]` 即为总方案数。 #### 代码实现 ```cpp #include <iostream> #include <vector> #include <bitset> using namespace std; int main() { int x; cin >> x; vector<pair<int, int>> like(x); // 存储每个学生喜欢的书 for (int i = 0; i < x; i++) { cin >> like[i].first >> like[i].second; } // dp[mask]:状态mask下的方案数 vector<long long> dp(1 << x, 0); dp[0] = 1; // 初始状态:无书被分配 for (int mask = 0; mask < (1 << x); mask++) { int k = bitset<32>(mask).count(); // 计算已分配学生数(即mask中1的个数) if (k >= x) continue; // 所有学生已分配,跳过 int book1 = like[k].first - 1; // 转换为0-indexed书号 int book2 = like[k].second - 1; // 尝试分配第一本书 if (!(mask & (1 << book1))) { dp[mask | (1 << book1)] += dp[mask]; } // 尝试分配第二本书 if (!(mask & (1 << book2))) { dp[mask | (1 << book2)] += dp[mask]; } } cout << dp[(1 << x) - 1] << endl; // 输出所有书被分配时的方案数 return 0; } ``` ### 代码解释 1. **输入处理**: - 读取学生数 `x`。 - 使用 `vector<pair<int, int>> like` 存储每个学生喜欢的两本书。 2. **DP 初始化**: - `dp` 数组大小为 `1 << x`(即 `2^x`),初始化为0。 - `dp[0] = 1` 表示没有任何书被分配时,方案数为1(初始状态)。 3. **状态转移**: - 遍历所有状态 `mask`(从 `0` 到 `(1 << x) - 1`)。 - 计算当前状态已分配的学生数 `k`(使用 `bitset<32>(mask).count()` 计算二进制中1的个数)。 - 若 `k < x`,则处理第 `k` 个学生: - 获取该学生喜欢的两本书 `book1` 和 `book2`(转换为0-indexed)。 - 检查书是否可用(`mask` 中对应位为0),若可用则更新新状态 `mask | (1 << book)` 的方案数。 4. **输出结果**: - 最终状态 `mask = (1 << x) - 1`(所有书被分配)对应的 `dp[(1 << x) - 1]` 即为总方案数。 ### 复杂度分析 - **时间复杂度**:`O(2^x * x)`,其中 `x` 最大为20,`2^20 ≈ 1e6`,可接受。 - **空间复杂度**:`O(2^x)`,用于存储 `dp` 数组。 ### 测试样例 输入: ``` 5 1 3 4 5 2 5 1 4 3 5 ``` 输出: ``` 2 ``` ### 相关问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值