【DP-思维+组合数学】BZOJ4498 魔法的碰撞+TC12996 TaroCheckers

BZOJ4498 魔法的碰撞

【题目】
原题地址
数轴上有 L L L个整点,分别是 0 … L − 1 0\dots L-1 0L1,现在有 n n n个人,每个人有一个安全范围 r i r_i ri,表示在它两边距离 &lt; r i &lt;r_i <ri的点上不能放人。问将所有人放在整点上的方案数。 L ≤ 1 0 5 , n , r i ≤ 40 L\leq 10^5,n,r_i\leq 40 L105,n,ri40

【解题思路】
一道十分玄学的 DP \text{DP} DP题,确实是道好题。
我们首先考虑暴力,枚举一个放的排列,那么令 w = ∑ i = 1 n − 1 max ⁡ ( r i , r i + 1 ) w=\sum_{i=1}^{n-1}\max(r_i,r_{i+1}) w=i=1n1max(ri,ri+1),贡献就是 ( L − w n ) L-w\choose n (nLw)。实际上这个 w w w就是安全范围所产生的不可以选择的空位数。

现在不能枚举排列,而 ∑ r i \sum r_i ri很小我们考虑对于每个 w w w算出有多少种方案,然后再乘组合数得到贡献。

首先将人按 r i r_i ri从大到小排序,那么考虑将这些人一个个放到数轴上。

我们考虑这样一个 DP \text{DP} DP状态 f i , j , k f_{i,j,k} fi,j,k表示当前放到第 i i i个人,有 j j j个空位可以放, w w w值现在为 k k k

这个空位是什么意思呢?假设一个人左边不再会放人,我们就说他左边没有空位,否则有,右边亦然。由于是从大到小进行放置,那么只有当这个人左右还会放人的时候,这个人的 r i r_i ri才会产生贡献。

那么现在我们就有三种转移:

  • 左右都不再放人,减少一个空位,不产生贡献
  • 只有一边放人,空位数不变,产生 d i d_i di的贡献
  • 左右两边都要放人,空位数 + 1 +1 +1,产生 2 d i 2d_i 2di的贡献

转移方程分别对应( f i , j , k f_{i,j,k} fi,j,k加上):

  • f i − 1 , j + 1 , k × ( j + 1 ) f_{i-1,j+1,k}\times (j+1) fi1,j+1,k×(j+1)
  • f i − 1 , j , k − r i × 2 j f_{i-1,j,k-r_i}\times 2 j fi1,j,kri×2j
  • f i − 1 , j − 1 , k − 2 r i × ( j − 1 ) f_{i-1,j-1,k-2r_i}\times (j-1) fi1,j1,k2ri×(j1)

具体意义显然,实现上略有差距。

【参考代码】

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

typedef long long ll;
const int N=2e5+10,M=42,P=1e4+10,mod=1e9+7;
int L,n,ans,sum;
int fac[N],ifac[N],r[M],f[M][M][P];

int read()
{
	int ret=0;char c=getchar();
	while(!isdigit(c)) c=getchar();
	while(isdigit(c)) ret=ret*10+(c^48),c=getchar();
	return ret;
}

int qpow(int x,int y)
{
	int ret=1;
	for(;y;y>>=1,x=(ll)x*x%mod) if(y&1) ret=(ll)ret*x%mod;
	return ret;
}
int C(int x,int y)
{
	if(x<y) return 0;
	return (ll)fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}
void up(int &x,int y){x+=y;if(x>=mod)x-=mod;}
bool cmp(int x,int y){return x>y;}

int main()
{
#ifndef ONLINE_JUDGE
	freopen("BZOJ4498.in","r",stdin);
	freopen("BZOJ4498.out","w",stdout);
#endif
	fac[0]=ifac[0]=1;
	for(int i=1;i<N;++i) fac[i]=(ll)fac[i-1]*i%mod,ifac[i]=qpow(fac[i],mod-2);
	int T=read();
	while(T--)
	{
		L=read();n=read();
		//cerr<<T<<" "<<L<<" "<<n<<endl;
		for(int i=1;i<=n;++i) r[i]=read()-1;
		sort(r+1,r+n+1,cmp);
		memset(f,0,sizeof(f));f[0][1][0]=1;sum=0;
		for(int i=1;i<=n;++i)
		{
			sum+=r[i];
			for(int j=0;j<=i+1;++j) for(int k=0;k<=2*sum;++k)
			{
				up(f[i][j][k],(ll)f[i-1][j+1][k]*(j+1)%mod);
				if(k>=r[i]) up(f[i][j][k],(ll)f[i-1][j][k-r[i]]*j*2%mod);
				if(j && k>=2*r[i]) up(f[i][j][k],(ll)f[i-1][j-1][k-2*r[i]]*(j-1)%mod);
			}
		}
		ans=0;
		for(int i=0;i<=2*sum;++i) up(ans,(ll)f[n][0][i]*C(L-i,n)%mod);
		printf("%d\n",ans);
	}

	return 0;
}

TC12996 TaroCheckers

【题目】
原题地址
给定一个 n × m n\times m n×m的格子,你需要在上面放 2 n 2n 2n个棋子,需要满足:

  • i i i行的前 l i l_i li个和后 r i r_i ri个格子中分别有且只有一个棋子
  • 每一列只能放一个棋子。
    求合法方案数,答案对 1 e 9 + 7 1e9+7 1e9+7取模。
    n ≤ 50 , m ≤ 200 , l i + r i ≤ m n\leq 50,m\leq 200,l_i+r_i\leq m n50,m200,li+rim

【解题思路】
这又是一个神奇的 DP \text{DP} DP
一个思路是我们从左往右考虑是否放棋子:

由于所有 l i l_i li在一开始都可以放,因此实际上我们只需要考虑分配了多少个列给 l i l_i li,但当遇到一个边界 l i l_i li时,我们可以分配(且必须分配)之前任意一列可用的给这一行。

当我们遇到一个 r i r_i ri时,相当于新加入一行,我们之后可以任意分配列给当前可分配的 r i r_i ri行。

这就是左边的行匹配列,右边用列匹配行。(意会一下)

那么我们设 f i , j , k f_{i,j,k} fi,j,k表示当前到第 i i i列,还有 j j j个未分配的列,有 k k k个未分配的 r i r_i ri且当前可以分配。

转移时枚举当前列放在哪个 r i r_i ri内(或者不放),若有 l i l_i li在当前列结束我们考虑用哪一个剩余的列分配给它即可。

复杂度 O ( n 2 m ) O(n^2m) O(n2m)

【参考代码】

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

typedef long long ll;
const int mod=1e9+7,N=205;
int n,m,cnt,p1[N],p2[N],a[N<<2];
ll fac[N],C[N][N],f[N][N][N];
vector<int>le,ri;

class TaroCheckers
{
	public:
	int getNumber(vector<int>,vector<int>,int);
};
void up(ll &x,ll y){x+=y;x%=mod;}
ll upm(ll x){return x>=mod?x-mod:x;}
void init()
{
	fac[0]=1;for(int i=1;i<N;++i)fac[i]=fac[i-1]*i%mod;
	for(int i=0;i<N;++i)
	{
		C[i][0]=C[i][i]=1;
		for(int j=1;j<i;++j) C[i][j]=upm(C[i-1][j-1]+C[i-1][j]);
	}
}
int TaroCheckers::getNumber(vector<int>L,vector<int>R,int m)
{
	n=L.size();init();
	memset(p1,0,sizeof(p1));memset(p2,0,sizeof(p2));memset(f,0,sizeof(f));
	for(int i=0;i<n;++i) p1[L[i]]++,p2[m-R[i]+1]++;
	f[0][0][0]=1;int sum=n;
	for(int i=0;i<m;++i)
	{
		int x=p1[i+1],y=p2[i+1];sum+=y;
		for(int j=0;j<=i;++j)
		{
			for(int k=0;k<=n;++k) if(f[i][j][k])
			{
				if(j+1>=x) up(f[i+1][j-x+1][k+y],f[i][j][k]*C[j+1][x]%mod*fac[x]%mod);
				if(j>=x) up(f[i+1][j-x][k+y],f[i][j][k]*C[j][x]%mod*fac[x]%mod*(n-sum+1)%mod);
				if(j>=x && k+y>0) up(f[i+1][j-x][k+y-1],f[i][j][k]*C[j][x]%mod*fac[x]%mod*(k+y)%mod);
			}
		}
		sum-=x;
	}
	return f[m][0][0];
}

/*int main()
{
	freopen("TC12996.in","r",stdin);
	freopen("TC12996.out","w",stdout);

	while(~scanf("%d",&a[cnt++]));--cnt;
	for(int i=0;i<cnt/2;++i) le.pb(a[i]);
	for(int i=cnt/2;i<cnt-1;++i) ri.pb(a[i]);
	printf("%d\n",getNumber(le,ri,a[cnt-1]));
}*/

【总结】
两道十分不错的 DP \text{DP} DP题,反正我模拟的时候都没想到。
尤其是问题的转化以及不要想岔
TC的数据交互方式不是传统型,巨坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值