CF722E Research Rover
Research Rover
题面翻译
有一个n*m的网格图,图中有k个特殊点。
初始时你有一个权值s,并且只能向下或向右走,
每经过一个特殊点会使得你的权值/2(向上取整)。
求从(1,1)走到(n,m)时拥有权值的期望(mod 1e9+7)。
题目描述
Unfortunately, the formal description of the task turned out to be too long, so here is the legend.
Research rover finally reached the surface of Mars and is ready to complete its mission. Unfortunately, due to the mistake in the navigation system design, the rover is located in the wrong place.
The rover will operate on the grid consisting of \(n\) rows and \(m\) columns. We will define as \((r,c)\) the cell located in the row \(r\) and column \(c\) . From each cell the rover is able to move to any cell that share a side with the current one.
The rover is currently located at cell \((1,1)\) and has to move to the cell \((n,m)\) . It will randomly follow some shortest path between these two cells. Each possible way is chosen equiprobably.
The cargo section of the rover contains the battery required to conduct the research. Initially, the battery charge is equal to \(s\) units of energy.
Some of the cells contain anomaly. Each time the rover gets to the cell with anomaly, the battery looses half of its charge rounded down. Formally, if the charge was equal to \(x\) before the rover gets to the cell with anomaly, the charge will change to .
While the rover picks a random shortest path to proceed, compute the expected value of the battery charge after it reaches cell \((n,m)\) . If the cells \((1,1)\) and \((n,m)\) contain anomaly, they also affect the charge of the battery.
输入格式
The first line of the input contains four integers \(n\) , \(m\) , \(k\) and \(s\) ( \(1<=n,m<=100000\) , \(0<=k<=2000\) , \(1<=s<=1000000\) ) — the number of rows and columns of the field, the number of cells with anomaly and the initial charge of the battery respectively.
The follow \(k\) lines containing two integers \(r_{i}\) and \(c_{i}\) ( \(1<=r_{i}<=n\) , \(1<=c_{i}<=m\) ) — coordinates of the cells, containing anomaly. It's guaranteed that each cell appears in this list no more than once.
输出格式
The answer can always be represented as an irreducible fraction . Print the only integer \(P·Q^{-1}\) modulo \(10^{9}+7\) .
样例 #1
样例输入 #1
3 3 2 11
2 1
2 3
样例输出 #1
333333342
样例 #2
样例输入 #2
4 5 3 17
1 2
3 3
4 1
样例输出 #2
514285727
样例 #3
样例输入 #3
1 6 2 15
1 1
1 5
样例输出 #3
4
提示
In the first sample, the rover picks one of the following six routes:
- , after passing cell \((2,3)\) charge is equal to \(6\) .
- , after passing cell \((2,3)\) charge is equal to \(6\) .
- , charge remains unchanged and equals \(11\) .
- , after passing cells \((2,1)\) and \((2,3)\) charge equals \(6\) and then \(3\) .
- , after passing cell \((2,1)\) charge is equal to \(6\) .
- , after passing cell \((2,1)\) charge is equal to \(6\) .
Expected value of the battery charge is calculated by the following formula:
.
Thus \(P=19\) , and \(Q=3\) .
\(3^{-1}\) modulo \(10^{9}+7\) equals \(333333336\) .
\(19·333333336=333333342 (mod 10^{9}+7)\)
Solution
模拟赛的 T4,赛时就写了个 \(\texttt{DFS}\) 的暴力就了事了,听学长评讲结果学长又把自己讲迷糊了,所以就来看是不是 CF 的原题。学习了一下题解,并按照自己的写法把代码给写了出来,希望能有帮助。
首先先来想一想最基础的东西:在 \(n\times m\) 的方格图中,从左上到右下有多少种合法路径,很显然可以想到用递推的方式求解。以 \(3\times 4\) 的方格图为例,可以得出以下的结果:
如果将这张表格顺时针旋转 \(45^\circ\) 的话,不难发现,这张表格变成了杨辉三角的一部分,也就是说,\(n\times m\) 方格图的路径数是可以用杨辉三角来计算的,路径数为 \(\displaystyle \begin{pmatrix}n+m-2\\n-1\end{pmatrix}\)。我们可以将这个公式推广成为方格图中两个点之间的路径数,即方格图中两个点(坐标分别为 \((x_1,y_1) ,(x_2,y_2)\) 的两个点,假设可以从前者到达后者)之间的路径数为 \(\displaystyle \begin{pmatrix}x_2-x_1+y_2-y_1\\x_2-x_2\end{pmatrix}\),如果将第一个点表示为 \(p_1\),第二个点表示为 \(p_2\),那么为了方便方程的书写,我把这个结果记作 \(way(p_1,p_2)\)。
注意题目的数据范围,因为 \(n,m\) 的范围均在 \(1e5\) 这一级别,所以肯定不能使用 \(\mathcal O(nm)\) 这类的算法来解决,同时注意到 \(k\) 的范围仅有 \(2e3\),所以考虑将障碍点作为状态转移的位置。为了满足无后效性原则,先将所有特殊点按照先 \(x\) 后 \(y\) 的顺序从小到大进行排序。同时可以发现 \(S\) 的范围是 \(1e6\),并且每经过一个障碍点会变成 \(\displaystyle \lceil \frac{S}{2} \rceil\),这也就意味着只要我们知道当前路径经过了多少特殊点,就可以知道目前的 \(S\) 值为多少,同时 \(S\) 的取值最多只有 \(\lceil \log S \rceil\) 个,因此可以尝试考虑一种时间复杂度为 \(\mathcal O(k^2 \log S)\) 的做法。
先考虑计算从起点出发不经过其他特殊点到达第 \(i\) 个特殊点的方案数,记作 \(f_i\),会发现直接计算的话状态并不好转移,所以考虑用容斥的方法来计算。记起点为 \(st\),那么可以得出 \(f_i\) 的转移方程:
解释下方程的含义:先计算出不做任何限制到达第 \(i\) 个特殊点的方案数,然后容斥掉经过其他特殊点的方案数,这些特殊点的方案数总和就是 \(\displaystyle \sum \limits_{j=1}^{i-1} f_j\times way(j,i)\)。
那么根据这种做法,可以推广到从起点出发恰好经过 \(j\) 个特殊点到达第 \(i\) 个特殊点的方案数,记作 \(f_{i,j}\),那么用上面方程同样的推导方式,可以推出 \(f_{i,j}\) 的转移方程:
同样来解释方程的含义:与上面的方程类似,先计算出不做任何限制到达第 \(i\) 个特殊点的方案数,减去上一个点经过了其他特殊点的方案数 \(\displaystyle \sum \limits _{t=1}^{i-1} f_{t,j-1} \times way(t,i)\),而最后一部分是因为要保证恰好,所以要将前面经过 \([1,j-1]\) 个特殊点的情况减去,并且可以注意到这一部分的值是一个前缀和,所以维护这个前缀和可以省下这部分的枚举时间。
此外需要注意的是,因为需要从起点出发并到达终点,所以在起点和终点不是特殊点的时候将这俩加入特殊点,并且在统计答案的时候还有另外需要注意的(待会代码注释里会讲)。因为 \(S\) 的取值只有 \(\lceil \log S \rceil\) 种情况,所以上面方程中 \(j\) 的枚举只需要枚举 \(\lceil \log S \rceil\) 个即可,所以上面方程的时间复杂度就是 \(\mathcal O(k^2\log S)\)。
计算组合数和答案的时候要用到除法,所以需要计算逆元,用费马小定理计算即可。需要知道的是,组合数计算的时候用到的逆元都是某个数阶乘的逆元,所以这部分的逆元可以预处理来减多一个 \(\log\) 的时间。
还有一些小细节会在代码中提到。
Code
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
k=0;T flag=1;char b=getchar();
while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
while (isdigit(b)) {k=k*10+b-48;b=getchar();}
k*=flag;
}
void write(int x) {if (x<0) putchar('-'),write(-x);if (x>9) write(x/10);putchar(x%10+'0');}
void writewith(int x,char c) {write(x);putchar(c);}//快读快输模板
const int barrierSize=2000,mod=1e9+7,mapSize=1e5;
int n,m,k,S;
struct POINT{//存点,按照x,y的顺序排序
int x,y;
bool operator<(const POINT &a) const {return (x!=a.x)?x<a.x:y<a.y;}
}bar[barrierSize+5];
int Fpow(int x,int y)//快速幂,用于求逆元
{
int res=1,base=x%mod;
while (y)
{
if (y&1) res=res*base%mod;
base=base*base%mod,y>>=1;
}
return res;
}
int Inv[(mapSize<<1)+5];
int inv(int x) {return Inv[x];}
int fac[(mapSize<<1)+5];
void preWork()//预处理阶乘和逆元
{
fac[0]=1;
for (int i=1;i<=(mapSize<<1);i++) fac[i]=fac[i-1]*i%mod;//阶乘
Inv[mapSize-1]=Fpow(fac[mapSize-1],mod-2);
for (int i=mapSize-1;i;i--) Inv[i-1]=Inv[i]*i%mod;//用递推的方法求阶乘的逆元,即Inv[i]表示i!的逆元
}
int C(int x,int y) {return fac[x]*inv(y)%mod *inv(x-y)%mod;}//计算组合数,直接调用提前计算好的阶乘和逆元即可
int way(POINT b,POINT a) {return C(a.x-b.x+a.y-b.y,a.x-b.x);}//计算从b到a的路径数
int f[barrierSize+5][barrierSize+5];//存答案,第二维可以只开到25
bool flag1=0,flag2=0;//flag1表示起点是否是特殊点,flag2表示终点是否是特殊点
signed main()
{
preWork();
read(n),read(m),read(k),read(S);
for (int i=1;i<=k;i++)
read(bar[i].x),read(bar[i].y),
flag1=(bar[i].x==1 && bar[i].y==1)?1:flag1,flag2=(bar[i].x==n && bar[i].y==m)?1:flag2;//维护flag1,flag2
if (!flag1) bar[++k]=(POINT){1,1};flag1^=1;//如果起点非特殊点,则加入特殊点内,flag1取反的含义在统计答案的时候会提
if (!flag2) bar[++k]=(POINT){n,m};flag2^=1;//将终点加入特殊点
sort(bar+1,bar+k+1);
int maxBar=ceil(log2(S))+1;//最大可能的j,直接取21也可以
int sum=0;
for (int i=1;i<=k;i++)
{
sum=0;//用于优化掉转移方程中的最后一部分
for (int j=1;j<=maxBar;j++)
{
f[i][j]=C(bar[i].x+bar[i].y-2,bar[i].x-1);//不加限制的总路径
for (int t=1;t<=i-1;t++)
if (bar[t].x<=bar[i].x && bar[t].y<=bar[i].y) f[i][j]=((f[i][j]-f[t][j]*way(bar[t],bar[i]))%mod+mod)%mod;//如果可以从t到i,那么就将这部分重复的减去
f[i][j]=((f[i][j]-sum)%mod+mod)%mod;//减去最后一部分,注意处理负数的情况
sum=(sum+f[i][j])%mod;//更新前缀和
}
}
int all=C(n+m-2,n-1),answer=0;//因为超过了log2(S)的部分的体力值都为1,所以这部分对答案的贡献就是这部分的方案数
for (int i=flag1+flag2;i<=maxBar;i++)//因为f[i][j]表示的是经过了j个特殊点的方案数,而我们将起点和终点给作为了特殊点处理,所以需要减去起点和终点多算的特殊点数量,这部分数量就是flag1+flag2
{
answer=(f[k][i]*S+answer)%mod;//计算总体力值,即方案数*对应体力值
S-=(S/2);//ceil(S/2)
all=((all-f[k][i])%mod+mod)%mod;//all用于统计体力值为1的部分对答案的贡献,所以需要将前面这部分的方案数减去
}
answer=(answer+all)%mod;//加上体力值为1的情况
writewith(answer*Fpow(C(n+m-2,n-1),mod-2)%mod,'\n');//需要注意的是总方案数的逆元是不能调用Inv数组的,因为Inv数组是阶乘的逆元,所以这里用快速幂重新算逆元来记录答案
return 0;
}
一道很有意思的题目,值得多想一想。