Codeforces 1326F Wise Men (容斥原理、状压 DP、划分数)

题目链接

F1: https://codeforces.com/contest/1326/problem/F1
F2: https://codeforces.com/contest/1326/problem/F2

题解

好题。
考虑容斥,对每个 01 串求满足串中为 \(1\) 的位置必须为 \(1\)、串中为 \(0\) 的位置 \(0\)\(1\) 均可的排列的个数。最后把超集和还原回来即可。
这样的好处是,本质不同的状态只有拆分数 \(P(n)\) 个,即一个状态的答案只和所有连续的 \(1\) 的长度构成的可重集合有关。于是考虑 DFS 枚举划分数。
先预处理 \(f_S\) 表示 \(S\) 点集内有多少条哈密尔顿回路经过的边全是 \(1\).
对于一个状态,假设每一段的长度是 \(a_1,a_2,...,a_l\). 那么就相当于我们要找 \(l\) 个状态 \(s_1,S_2,...,s_l\), 满足 \(\forall i, \text{bitcnt}(s_i)=a_i\)\(\text{or}^l_{i=1}s_i=2^n-1\),贡献为 \(\prod^l_{i=1}f_{s_i}\). 写成集合幂级数的形式,设 \(g_i\) 是一个集合幂级数,满足有且仅有在 \(\text{bitcnt}(s)=i\) 的位置有值,值为 \(f_s\),则这个状态的总方案数等于 \(g_{a_1},g_{a_2},...,g_{a_l}\) 的子集卷积。于是可以直接使用子集卷积计算,可以做到 \(O(2^nP(n)n^2)\) 左右的复杂度。但是还是不行。
注意到 \(\sum^l_{i=1}a_i=n\),且 \(g_{a_i}\) 仅仅在 \(\text{bitcnt}(a_i)\) 处有值。也就是说我们其实根本不需要使用子集卷积——子集卷积的方法是给每个集合幂级数增加一维长度的限制,但是这里我们已经对长度进行了限制!假设有任何两个 \(s_i\) 有交,那么所有 \(s_i\) 的并的大小就不可能为 \(n\). 于是直接对 \(g_{a_i}\) 这些集合幂级数作 or 卷积即可。
枚举划分后,计算 or 卷积的时间复杂度为所有划分方案的总长度,足以通过。但是我们可以边 DFS 边维护 or 卷积,复杂度变成了搜索树的节点个数乘以 \(2^n\).
划分数搜索时,比较好的方法是从大到小搜索,每次放的数不超过上次放的。这时只要上次放的数不小于 \(2\),每个节点分叉数就一定大于 \(1\). 假设剩下一堆 \(1\) 是一起放的,那么节点数显然为 \(O(P(n))\);否则据 EI 爷说是 \(O(P(n)\sqrt n)\) 的。这里可以预处理 \(g_1\) 的幂来做到前者的复杂度。
总时间复杂度 \(O(2^n(n^2+T(n))\),其中 \(T(n)=O(P(n))\)\(O(P(n)\sqrt n)\)\(O(\text{sum of length of all partitions})\).

代码

#include<bits/stdc++.h>
#define llong long long
#define mkpr make_pair
#define x first
#define y second
#define iter iterator
#define riter reversed_iterator
#define y1 Lorem_ipsum_dolor
using namespace std;

inline int read()
{
	int x = 0,f = 1; char ch = getchar();
	for(;!isdigit(ch);ch=getchar()) {if(ch=='-') f = -1;}
	for(; isdigit(ch);ch=getchar()) {x = x*10+ch-48;}
	return x*f;
}

const int mxN = 18;
int bitcnt[(1<<mxN)+3];
llong f[mxN+3][(1<<mxN)+3];
llong dp[(1<<mxN)+3][mxN+3];
llong g[mxN+3][(1<<mxN)+3];
int part[mxN+3],aux[mxN+3];
llong h[(1<<mxN)+3];
char a[mxN+3][mxN+3];
int n,m;

void dfs(int rst,int lst)
{
	if(rst==0)
	{
		llong ret = 0ll;
		for(int i=0; i<(1<<n); i++)
		{
			ret += ((n-bitcnt[i])&1)?-g[m][i]:g[m][i];
		}
//		printf("("); for(int i=1; i<=m; i++) printf("%d ",part[i]); printf("): %I64d\n",ret);
		for(int i=1; i<=m; i++) {aux[i] = part[m-i+1];}
		do
		{
			int pos = 0,sta = 0;
			for(int i=1; i<=m; i++)
			{
				for(int j=1; j<aux[i]; j++,pos++) {sta|=(1<<pos);}
				pos++;
			}
//			printf("sta=%d\n",sta);
			h[sta] += ret;
		} while(next_permutation(aux+1,aux+m+1));
		return;
	}
	for(int i=1; i<=rst&&i<=lst; i++)
	{
		part[++m] = i;
		for(int j=0; j<(1<<n); j++) {g[m][j] = g[m-1][j]*f[i][j];}
		dfs(rst-i,i);
		m--;
	}
}

int main()
{
	for(int i=1; i<(1<<mxN); i++) bitcnt[i] = bitcnt[i>>1]+(i&1);
	n = read(); for(int i=0; i<n; i++) {scanf("%s",a[i]); for(int j=0; j<n; j++) a[i][j] -= 48;}
	for(int i=0; i<n; i++) dp[1<<i][i] = 1ll;
	for(int i=1; i<(1<<n); i++) for(int j=0; j<n; j++) if(i&(1<<j))
	{
		llong x = dp[i][j];
		for(int k=0; k<n; k++) if(a[j][k]&&!(i&(1<<k)))
		{
			dp[i|(1<<k)][k] += x;
		}
	}
	for(int i=0; i<(1<<n); i++) for(int j=0; j<n; j++) if(i&(1<<j))
	{
		f[bitcnt[i]][i] += dp[i][j];
	}
//	for(int i=0; i<(1<<n); i++) printf("%I64d ",f[bitcnt[i]][i]); puts("");
	for(int i=0; i<n; i++) for(int j=0; j<(1<<n); j++) if(j&(1<<i))
	{
		for(int k=0; k<n; k++) {f[k][j] += f[k][j^(1<<i)];}
	}
	for(int i=0; i<(1<<n); i++) g[0][i] = 1ll; dfs(n,n);
	for(int i=0; i<n-1; i++) for(int j=0; j<(1<<n-1); j++) if(j&(1<<i))
	{
		h[j^(1<<i)] -= h[j];
	}
	for(int i=0; i<(1<<n-1); i++) printf("%I64d ",h[i]); puts("");
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
区间DP是一种动态规划的方法,用于解决区间范围内的问题。在Codeforces竞赛中,区间DP经常被用于解决一些复杂的字符串或序列相关的问题。 在区间DP中,dp[i][j]表示第一个序列前i个元素和第二个序列前j个元素的最优解。具体的转移方程会根据具体的问题而变化,但是通常会涉及到比较两个序列的元素是否相等,然后根据不同的情况进行状态转移。 对于区间长度为1的情况,可以先进行初始化,然后再通过枚举区间长度和区间左端点,计算出dp[i][j]的值。 以下是一个示例代码,展示了如何使用区间DP来解决一个字符串匹配的问题: #include <cstdio> #include <cstring> #include <string> #include <iostream> #include <algorithm> using namespace std; const int maxn=510; const int inf=0x3f3f3f3f; int n,dp[maxn][maxn]; char s[maxn]; int main() { scanf("%d", &n); scanf("%s", s + 1); for(int i = 1; i <= n; i++) dp[i][i] = 1; for(int i = 1; i <= n; i++) { if(s[i] == s[i - 1]) dp[i][i - 1] = 1; else dp[i][i - 1] = 2; } for(int len = 3; len <= n; len++) { int r; for(int l = 1; l + len - 1 <= n; l++) { r = l + len - 1; dp[l][r] = inf; if(s[l] == s[r]) dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]); else { for(int k = l; k <= r; k++) { dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]); } } } } printf("%d\n", dp[n]); return 0; } 希望这个例子能帮助你理解区间DP的基本思想和应用方法。如果你还有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值