bzoj 1879 //1879: [Sdoi2009]Bill的挑战 状压dp/容斥原理

bzoj 1879 //1879: [Sdoi2009]Bill的挑战   //在线测评地址https://www.lydsy.com/JudgeOnline/problem.php?id=1879
//在线测评地址https://www.luogu.com.cn/problem/P2167

为了帮助理解,提供几组样例

样例

输入:

1
3 3
?
?
?

输出:

26

样例

输入:

1
3 3
r
?
?

输出:

1

样例

输入:

1
3 3
??
??
??

输出:

676

方法一:状压dp

Accepted7996 kb572 msC++/Edit1223 B

//1879: [Sdoi2009]Bill的挑战
//在线测评地址https://www.lydsy.com/JudgeOnline/problem.php?id=1879
//在线测评地址https://www.luogu.com.cn/problem/P2167
//竟然没看懂题。如何又跳出个M了?
//此文https://www.cnblogs.com/wo-shi-zhen-de-cai/p/11741891.html代码写得不错。
/*
观察数据范围,显然是状压。
但是如果你将K加进状态中,手推一下就会发现这里要用到容斥。
但我又不是讲容斥的是吧。。。

所以我们尝试不将K加入状态中,而是在最后枚举恰好含有K个元素的子集个数。
我们设f[i][j]表示对于所有集合i中的元素,匹配到第j位时的方案数。
实际上我们涉及到集合的状压转移时如果考虑一个状态由哪里转移来,要枚举合法子集,显然麻烦。
我们可以考虑当前状态可转移到哪里,这样只用造出合法状态即可。
我们同时还要预处理一个Mat[i][j]表示一个集合,其中的所有元素满足,第i位可以匹配到字母j。

有点卡时限,最好不要用memset

*/
//此文https://www.cnblogs.com/OIEREDSION/p/11435789.html思路不错,摘抄如下

方案不会算重 因为到达的顺序一定不一样
//此文https://www.cnblogs.com/AWCXV/p/7632226.html思路不错,摘抄如下
/*
感觉自己的智商不够用了。哎。这个方法太巧妙了。

g[i][j]第i个字符是j或'?'的字符串有哪一些(代表一个集合二进制的0表示不在这个集合中,1表示在)。
f[i][j],前i-1个字符已经全部匹配了,已经匹配了的字符串的集合为j的方案数;
这个集合j里面的字符串。前i-1个字符是一样的。
if (f[i][j]) f[i+1][j&g[i][k]]+=f[i][j]; (k∈[1..26]);
一开始的时候f[0][(1<<n)-1]=1;
然后&这个位运算。可以排除掉那些第i位不是字母k的字符串。保留第i位是字母k的字符串。
然后想想i=i+1的时候。我们再次找到的f[i][j]实际上就是满足第i-1位是一样的了。然后再看看第i为是不是字母k。再把不是....
每次都按照这个规律。那么最后保存的集合j中字符串就全部是一样的了(或者有些不一样,但是可以把?转换成某个字母让他们全都一样)。

好棒的方法。
*/

//关于 算重,此文https://www.cnblogs.com/ShuraK/p/9380094.html解释得不错,摘抄如下
/*
想法:

又是一个看数据范围想做法的题,我们想到状压dp。
看了题解... ...网上给的状态是f[len][s]表示长度为len满足状态s的字符串个数。
光看状态... ...可能算重啊?!

其实... ...

状态:dp[len][s]表示长度为len,能且只能满足状态为s的字符串个数。

转移:我们先预处理出g[i][c]表示第i位能放字符c的字符串状态,转移就是dp[len][s^g[len][c]]+=dp[len-1][s]表示在dp[len-1][s]的所有方案中所有的字符串后面加上c能满足的字符串。这样仍然满足“能且只能”的条件。

小结:看数据范围想做法其实很实用,比如说我们拿到一道题,如果这个数据范围是卡这正解的数据范围出的话,我们就会往一些比较常见的复杂度上想,加快了解题速度。
*/
//此文https://blog.csdn.net/weixin_34128411/article/details/94049076思路不错,摘抄如下
/*
这题的话...我想了很久但是都不是可行解
刚开始想预处理任意两个串是否可以匹配然后在乱搞,后来发现完全不会写...
然后按照惯例,我会看题解认真的思考...
唔...其实看完题解貌似这题还挺容易的?
我们可以预处理一个数组 g[i,j] 表示 在这 n 个串中前 i 个字符且第 i 个字符匹配为 j (j 是一个字符) 时的一个状态。
这个状态为一个长度为n的 2进制数转为10进制。比如 111 这个状态指 1 串和2 串和 3 串都是可以匹配的。
这个数组就是这个作用。
辣它可以干什么捏。
我萌进入dp部分。
设 f[i,j] 表示 每个串前 i 个字符 状态为 j 的方案数。
初始化就是 f[0,1 << n-1]=1
答案就是 sum(f[n,j]) 这里的 j 状态要满足 二进制1 的个数为 k。
j 这个状态指 n 个串中选了哪些串。
f[i,j & g[i,x]+=f[i-1,j]
 枚举一个 x 字符,对于 ‘a’-‘z’ 这些字符都可以是第 i 位的。
然后枚举前继状态 j  辣么 对于要更新的状态就是    j & g[i,x]
为什么是&?  因为如果能转移到的必须要满足   g[i,x] 中能匹配这个串 同时前继状态也要有。
而 & 就是只有两个都是1 的时候才为 1 ,所以 & 后就是可以转移的一个状态。
这样打完之后捏,我兴高采烈的交了上去。TLE!!!
算了一下效率,似乎是卡着的呀QAQ
怀疑兔生的我优化了常数,以为是常数的锅。
结果还是 TLE! TLE!TLE!
然后怀疑兔生的又看了一次题解 开始思考原因
发现题解里加了优化的QAQ 但是并没有说...
所以要加一个优化咯。
这样考虑对于 f[i-1,j]=0 的情况 实际上可以不去转移,这样可以省掉很多时间。
*/

//以下位置写错,查了好久
/*
for(j=1;j<=len;j++)//位置错写在第2个循环
    for(i=0;i<(1<<n);i++)//位置错写在第1个循环
*/
//样例通过,提交AC. 2019-12-5

#include <stdio.h>
#include <string.h>
#define mod 1000003
char S[18][55];
int f[1<<15][55],mat[55][28],cnt[1<<15];
int main(){
	int i,j,c,t,n,k,len,x,ans;
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&k);
		for(i=1;i<=n;i++)scanf("%s",S[i]+1);
		len=strlen(S[1]+1);
		for(i=1;i<(1<<n);i++){
			x=i;
			while(x)x-=x&-x,cnt[i]++;//计算i中1的个数
		}
		for(i=1;i<=len;i++)
			for(c=0;c<26;c++)
				for(j=1;j<=n;j++)
					if(S[j][i]=='?'||S[j][i]==c+'a')mat[i][c]|=(1<<(j-1));
		f[(1<<n)-1][0]=1;
		for(j=1;j<=len;j++)//位置错写在第2个循环
			for(i=0;i<(1<<n);i++)//位置错写在第1个循环
				if(f[i][j-1]&&cnt[i]>=k)//mat[j][c]&i<=i
					for(c=0;c<26;c++)f[mat[j][c]&i][j]=(f[mat[j][c]&i][j]+f[i][j-1])%mod;
		ans=0;
		for(i=0;i<(1<<n);i++)
			if(cnt[i]==k)ans=(ans+f[i][len])%mod;
		printf("%d\n",ans);
		for(i=0;i<(1<<n);i++)
			for(j=0;j<=len;j++)f[i][j]=0;//此处错写成for(j=0;j<len;j++)
		for(i=0;i<=len;i++)//此处错写成for(i=0;i<=n;i++)
			for(j=0;j<26;j++)mat[i][j]=0;//此处错写成for(j=0;j<len;j++)mat[i][j]=0;
		for(i=0;i<(1<<n);i++)cnt[i]=0;
	}
	return 0;
}

方法二:容斥原理

Accepted824 kb252 msC++/Edit1913 B

//此文https://blog.csdn.net/strangedddf/article/details/88200573思路不错,摘抄如下

//上述公式中,减去部分的理解,摘自https://www.cnblogs.com/Miracevin/p/9585609.html
/*
至于后面减去的部分。就是容斥的内容了。
大家可以自己画一个韦恩图理解一下。

这里有一个例子:n=4
现在我们要算ans[2],也就是恰好匹配2个的T的方案数
就是黄色的部分。
红色的数字是这个区域被算cal(i)的次数。
可见,三个点的重复区域,由于有C(3,2)种方法选到,所以会被算C(3,2)次。
所以减去所有的ans[3]即可。
其他情况同理。
最后输出ans[1]
组合数打表。

理论复杂度:
O(n×len×2^15)
*/
最多枚举数C(15,7)=13*11*5*9=6435  C(15,7)*15*50*10=4.8*10^7 dfs不会超时
//样例通过,提交AC。2019-12-7 12:33

#include <stdio.h>
#include <string.h>
#define LL long long
#define mod 1000003
char S[18][55];
int up,n,k,mem[18],cnt=0,len;//up需要匹配的数量,mem[]记录当前用到的串号
LL ans[18],tot,C[18][18];
void dfs(int x,int has){//x选到的字符串,has已经匹配的数量   dfs计算tot该题的第2个核心算法,是搜索,深搜真的写得化境了。
	int i,j,las,lp;
	if(x==n+1){//x当前所搜到的串的序号
		if(has!=up)return;//has找到的匹配的字串数量,up目标需要匹配的字串数量
		lp=1;
		for(i=1;i<=len;i++){//按字串的位进行操作,一位一位进行处理
			las=-1;//之前位对应的字符//在进行每位搜索前的初始化,las用来定位,当前搜索之前,该位的最新状态。
			for(j=1;j<=has;j++)//只搜索匹配的字串
				if(S[mem[j]][i]!='?'){//mem[i]用来记忆匹配的字串位置。
					if(las==-1)las=S[mem[j]][i]-'a';//此处错写成if(las=-1)las=S[mem[j]][i]-'a';查了10分钟//更新las
					if(las!=S[mem[j]][i]-'a')return;//两个字符不一样,无合法方案
				}
			if(las==-1)lp=(lp*26)%mod;//如果都是‘?’可以随便填,否则只有一种   匹配字串搜索下来,该位全是'?'
		}
		tot=(tot+lp)%mod;//此处错写成tot=(tot*lp)%mod;//统计符合条件的数量
		return ;
	}
	if(has<up){//获取不同组合   //搜索到的数量 小于 需要匹配的数量
		mem[++cnt]=x;//记录匹配到的串的位置
		dfs(x+1,has+1);//准备搜索第x+1号的字串作为第has+1个的匹配。
		mem[cnt--]=0;//回溯
	}
	if(n-x>=up-has)dfs(x+1,has);//n-x>=up-has表明x+1->n中还有足够的字串提供给has->up进行选择,准备搜索第x+1号的字串作为第has个的匹配。//可从其他位置开始搜索,此处写法最难,可以看出原作者的功力。
}
int main(){
	int t,i,j,sum;
	for(i=0;i<=15;i++){
		C[i][0]=1;
		for(j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;//此处错写成for(j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
	}
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&k);
		for(i=1;i<=n;i++)scanf("%s",S[i]+1);
		if(n<k){//写错位置,将其写在for(i=1;i<=n;i++)scanf("%s",S[i]+1);之上了
			printf("0\n");
			continue;
		}
		len=strlen(S[1]+1);
		for(i=n;i>=k;i--){//ans[i]计算 
			tot=0,sum=0,up=i;//此处错写成tot=1,sum=0,up=i;
			dfs(1,0);//从第1串开始搜,匹配的串的数量是0
			for(j=i+1;j<=n;j++)sum=(sum+C[j][i]*ans[j])%mod;//此处错写成for(j=k+1;j<=n;j++)sum=(sum+C[j][k]*ans[j])%mod;
			ans[i]=((tot-sum)%mod+mod)%mod;//容斥的处理 
		}
		printf("%lld\n",ans[k]);
	}
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值