#10 CF708E & CF643F
这个系列终于上两位数了。
Student's Camp
题目描述
解法
首先考虑一个 \(O(n^5)\) 的 \(dp\),设 \(f(i,l,r)\) 表示考虑到第 \(i\) 行,第 \(i\) 行剩下的格子是 \([l,r]\),并且前 \(i\) 行联通的概率。我们预处理出 \(D(i)={k\choose i}\cdot(\frac{a}{b})^i\cdot (1-\frac{a}{b})^{k-i}\),表示恰好吹倒 \(i\) 个格子的概率。
第一步优化可以考虑正难则反,我们用总数减去不合法的方案数来转移:
那么显然可以使用前缀和优化,设 \(F(i)=\sum_{l'\leq r'}f(i-1,l',r')\),\(L(i,x)=\sum_{l'\leq r'<x}f(i-1,l',r')\),\(R(i,x)=\sum_{x<l'\leq r'} f(i-1,l',r')\),那么转移可以写成:
现在的时间复杂度已经优化到了 \(O(n^3)\),发现瓶颈在于状态量太大,考虑到答案只需要整体求和,我们可以把状态也定义成和式,设 \(S(i,r)=\sum_{l\leq r}f(i,l,r)\),考虑写出这个和式的转移:
由于 \(r\) 是包含在状态里的(相当于已知),所以我们把和 \(r\) 有关的部分提出来:
那么我们用前缀和维护 \(\sum_{l\leq r}D(l-1)\) 和 \(\sum_{l\leq r}D(l-1)\cdot L(i-1,l)\) 即可。代码实现中,我们可以计算 \(S\),然后得到 \(L\),再计算对应的前缀和即可,注意 \(R(i-1,r)\) 可以根据对称性换成 \(L(i-1,m-r)\),这是因为左右是等价的,所以翻折之后对应的值是相等的。
代码实现抄了别人的,其实根本不复杂,时间复杂度 \(O(n^2)\)
总结
对于瓶颈在状态定义的 \(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\) 之间是独立的,所以用加法原理,那么转移:
暴力转移是 \(O(p^2q)\) 的,无法通过本题,优化可以见官方题解,反正我觉得有点难优化。
更好的方法是建立映射关系,把熊的状态的和酒的位置建立双射。那么我们要考察熊的状态是什么,对于每一个位置我们可以写出一个 \(q\times n\) 的矩阵,表示某一天某一头熊是(\(1\))否(\(0\))喝了这个酒。
然后考虑这个矩阵有什么要求呢?首先每一列最多有一个 \(1\),因为这头熊要么喝进医院,要么喝过之后不会再碰它了。其次最多有 \(\min(n-1,p)\) 个 \(1\),要不然床位就可能满\(/\)熊可能都喝没了。那么对这个矩阵进行计数,我们枚举 \(1\) 的个数,确定列之后再确定行:
这个数量显然是位置数的上界,因为每个位置分配的矩阵不同。那么我们再证明这样做的充分的,考虑我们按照矩阵来安排熊去喝酒,得到每头熊是否喝进医院,和对应的天数就可以唯一确定这个位置。
显然可以 \(O(pq)\) 暴力计算,需要预处理组合数,我们可以上下暴力除 \(\tt gcd\) 来 \(O(p^3\log p)\) 计算组合数。
#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);
}