#10 CF708E & CF643F

这个系列终于上两位数了

Student's Camp

题目描述

点此看题

解法

首先考虑一个 O(n5)dp,设 f(i,l,r) 表示考虑到第 i 行,第 i 行剩下的格子是 [l,r],并且前 i 行联通的概率。我们预处理出 D(i)=(ki)(ab)i(1ab)ki,表示恰好吹倒 i 个格子的概率。

第一步优化可以考虑正难则反,我们用总数减去不合法的方案数来转移:

f(i,l,r)=D(l1)D(mr)(lrf(i1,l,r)lr<lf(i1,l,r)r<lrf(i1,l,r))

那么显然可以使用前缀和优化,设 F(i)=lrf(i1,l,r)L(i,x)=lr<xf(i1,l,r)R(i,x)=x<lrf(i1,l,r),那么转移可以写成:

f(i,l,r)=D(l1)D(mr)(F(i1)L(i1,l)R(i1,r))

现在的时间复杂度已经优化到了 O(n3),发现瓶颈在于状态量太大,考虑到答案只需要整体求和,我们可以把状态也定义成和式,设 S(i,r)=lrf(i,l,r),考虑写出这个和式的转移:

S(i,r)=lD(l1)D(mr)(F(i1)L(i1,l)R(i1,r))

由于 r 是包含在状态里的(相当于已知),所以我们把和 r 有关的部分提出来:

S(i,r)=D(mr)((F(i1)R(i1,r))lrD(l1)lrD(l1)L(i1,l))

那么我们用前缀和维护 lrD(l1)lrD(l1)L(i1,l) 即可。代码实现中,我们可以计算 S,然后得到 L,再计算对应的前缀和即可,注意 R(i1,r) 可以根据对称性换成 L(i1,mr),这是因为左右是等价的,所以翻折之后对应的值是相等的。

代码实现抄了别人的,其实根本不复杂,时间复杂度 O(n2)

总结

对于瓶颈在状态定义的 dp,可以考察答案的形式,改变状态的定义,在原状态基础上转移。

本题拆分法的应用也是一个亮点,在推有关左右端点的式子时可以考虑把他们拆开来。

#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
const int M = 1505;
const int MOD = 1e9+7;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a,b,k;
struct node
{
	int x;
	node(int X=0) : x(X) {}
	void operator = (int b) {x=b;}
	node operator + (node b) {return node((x+b.x)%MOD);}
	node operator - (node b) {return node((x-b.x+MOD)%MOD);}
	node operator * (node b) {return node(x*b.x%MOD);}
	node operator ^ (int b)
	{
		int r=1,a=x;
		while(b) {if(b&1) r=r*a%MOD;a=a*a%MOD;b>>=1;}
		return node(r);
	}
	node operator / (node b) {return (*this)*(b^(MOD-2));}
}x,p[M],q[M],f[M][M],s[M][M],g[M][M];
node C(int n,int m)
{
	node a=1,b=1;
	for(int i=1;i<=m;i++)
		a=a*(n-i+1),b=b*i;
	return a/b;
}
signed main()
{
	n=read();m=read();a=read();b=read();k=read();
	x=(node)a/b;
	for(int i=0;i<=min(m,k);i++)
		p[i]=C(k,i)*(x^i)*((node(1)-x)^(k-i));
	for(int i=1;i<=m;i++) q[i]=q[i-1]+p[i-1];
	f[0][m]=s[0][m]=node(1);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			f[i][j]=p[m-j]*(q[j]*(s[i-1][m]-s[i-1][m-j])-g[i-1][j]);
			s[i][j]=s[i][j-1]+f[i][j];
			g[i][j]=g[i][j-1]+p[j-1]*s[i][j-1];
		}
	printf("%lld\n",s[n][m].x);
}

Bears and Juice

题目描述

点此看题

解法

我自己有一个 dp 的想法,设 dp[x][y] 表示还剩 x 张床,还剩 y 天,桶的数量最多是多少。考虑一天的转移,考虑这些熊睡觉 / 不睡觉的状态可以帮助我们确定集合 S(酒就存在于集合 S 中),发现集合 S 的大小其实就是子问题。

那么我们考虑枚举有 i 头熊喝醉了,有 i 头熊喝醉的方案对应着相同大小的 S,所以需要用乘法原理。而不同的 i 之间是独立的,所以用加法原理,那么转移:

dp[x][y]=i=0xdp[xi][y1](n(px)i)

暴力转移是 O(p2q) 的,无法通过本题,优化可以见官方题解,反正我觉得有点难优化。


更好的方法是建立映射关系,把熊的状态的和酒的位置建立双射。那么我们要考察熊的状态是什么,对于每一个位置我们可以写出一个 q×n 的矩阵,表示某一天某一头熊是(1)否(0)喝了这个酒。

然后考虑这个矩阵有什么要求呢?首先每一列最多有一个 1,因为这头熊要么喝进医院,要么喝过之后不会再碰它了。其次最多有 min(n1,p)1,要不然床位就可能满/熊可能都喝没了。那么对这个矩阵进行计数,我们枚举 1 的个数,确定列之后再确定行:

i=0min(n1,p)(ni)qi

这个数量显然是位置数的上界,因为每个位置分配的矩阵不同。那么我们再证明这样做的充分的,考虑我们按照矩阵来安排熊去喝酒,得到每头熊是否喝进医院,和对应的天数就可以唯一确定这个位置。

显然可以 O(pq) 暴力计算,需要预处理组合数,我们可以上下暴力除 gcdO(p3logp) 计算组合数。

#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 205;
#define int unsigned int
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,p,q,ans,a[M];
int gcd(int a,int b) {return !b?a:gcd(b,a%b);}
int calc(int m)
{
	vector<int> a,b;int r=1;
	for(int i=n;i>n-m;i--) a.push_back(i);
	for(int i=1;i<=m;i++) b.push_back(i);
	for(int &x:a) for(int &y:b)
	{
		int d=gcd(x,y);
		x/=d;y/=d;
	}
	for(int &x:a) r*=x;
	return r;
}
signed main()
{
	n=read();p=min(read(),n-1);q=read();
	for(int i=0;i<=p;i++) a[i]=calc(i);
	for(int i=1;i<=q;i++)
	{
		int nw=0;
		for(int j=0,k=1;j<=p;j++,k*=i)
			nw+=a[j]*k;
		ans^=nw*i;
	}
	printf("%u\n",ans);
}
posted @   C202044zxy  阅读(299)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
历史上的今天:
2021-03-11 [NOI2007] 货币兑换
点击右上角即可分享
微信分享提示