【题解】P8865 [NOIP2022] 种花(二分答案,前缀和)
【题解】P8865 [NOIP2022] 种花
场外 VP 选手。唯一场切的一道题,写篇题解纪念一下。(
顺便提一嘴:e 我是真的菜,,其他人&题解这道题都是 \(O(nm)\) 的,就我是 \(O(nm\log n)\)。。~~无事凭空造 \(\log\) ~~~
题目链接
题意概述
有一个 \(n\times m\) 的网格图,要在网格图上种花。
有两种种花方案。
第一种是 \(\texttt C-\) 形:
如果存在 \(x_1, x_2 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 行的第 \(y_0\) 到第 \(y_1\) 列、第 \(x_2\) 行的第 \(y_0\) 到第 \(y_2\) 列以及第 \(y_0\) 列的第 \(x_1\) 到第 \(x_2\) 行都不为土坑,且只在上述这些位置上种花。
第二种是 \(\texttt F-\) 形:
如果存在 \(x_1, x_2, x_3 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2 < x_3\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 行的第 \(y_0\) 到第 \(y_1\) 列、第 \(x_2\) 行的第 \(y_0\) 到第 \(y_2\) 列以及第 \(y_0\) 列的第 \(x_1\) 到第 \(x_3\) 行都不为土坑,且只在上述这些位置上种花。
求给定网格图有多少种 \(\texttt C-\) 和 \(\texttt F-\) 的种花方案。
答案输出 \(\texttt C-\) 的方案数乘给定常数 \(c\) 和 \(\texttt F-\) 的方案数乘给定常数 \(f\) 对 \(998244353\) 取模的结果即可。
数据范围
对于所有数据,保证:\(1 \leq T \leq 5\),\(1 \leq n, m \leq 10^3\),\(0 \leq c, f \leq 1\),\(a_{i,j} \in \{0, 1\}\)。
测试点编号 | \(n\) | \(m\) | \(c=\) | \(f=\) | 特殊性质 | 测试点分值 |
---|---|---|---|---|---|---|
\(1\) | \(\leq 1000\) | \(\leq 1000\) | \(0\) | \(0\) | 无 | \(1\) |
\(2\) | \(=3\) | \(=2\) | \(1\) | \(1\) | 无 | \(2\) |
\(3\) | \(=4\) | \(=2\) | \(1\) | \(1\) | 无 | \(3\) |
\(4\) | \(\leq 1000\) | \(=2\) | \(1\) | \(1\) | 无 | \(4\) |
\(5\) | \(\leq 1000\) | \(\leq 1000\) | \(1\) | \(1\) | A | \(4\) |
\(6\) | \(\leq 1000\) | \(\leq 1000\) | \(1\) | \(1\) | B | \(6\) |
\(7\) | \(\leq 10\) | \(\leq 10\) | \(1\) | \(1\) | 无 | \(10\) |
\(8\) | \(\leq 20\) | \(\leq 20\) | \(1\) | \(1\) | 无 | \(6\) |
\(9\) | \(\leq 30\) | \(\leq 30\) | \(1\) | \(1\) | 无 | \(6\) |
\(10\) | \(\leq 50\) | \(\leq 50\) | \(1\) | \(1\) | 无 | \(8\) |
\(11\) | \(\leq 100\) | \(\leq 100\) | \(1\) | \(1\) | 无 | \(10\) |
\(12\) | \(\leq 200\) | \(\leq 200\) | \(1\) | \(1\) | 无 | \(6\) |
\(13\) | \(\leq 300\) | \(\leq 300\) | \(1\) | \(1\) | 无 | \(6\) |
\(14\) | \(\leq 500\) | \(\leq 500\) | \(1\) | \(1\) | 无 | \(8\) |
\(15\) | \(\leq 1000\) | \(\leq 1000\) | \(1\) | \(0\) | 无 | \(6\) |
\(16\) | \(\leq 1000\) | \(\leq 1000\) | \(1\) | \(1\) | 无 | \(14\) |
特殊性质 A:\(\forall 1 \leq i \leq n, 1 \leq j \leq \left\lfloor \frac{m}{3} \right\rfloor\),\(a_{i, 3 j} = 1\);
特殊性质 B:\(\forall 1 \leq i \leq \left\lfloor \frac{n}{4} \right\rfloor, 1 \leq j \leq m\),\(a_{4 i, j} = 1\);
思路分析
注:我们用 \(a_{i,j}\) 表示网格图上第 \(i\) 行第 \(j\) 列的数。
对于这种问题,可以考虑从某一个角度开始来思考它。
首先以 \(\texttt C-\) 形为例:
我们可以分别枚举位置 \((x,y)\),然后考虑以 \((x,y)\) 为 \(\texttt C-\) 形图案的左上角时,有多少种种花方案。
其实此时的种花方案数取决于三点:
- 从 \((x,y)\) 向右有多少个点种花了;
- 从 \((x,y)\) 向下走有多少个点 \((x,z)\) 能够成为 \(\texttt C-\) 形图案的左下角;
- \(\texttt C-\) 形图案的左下角向右有多少个点种花了;
我们定义 \(sum1_{i,j}\) 表示第 \(i\) 行 \(a_{i,1}\) 到 \(a_{i,j}\) 的前缀和。那么从 \((x,y)\) 开始,向右最远能种花的位置就是从 \((x,y)\) 往右走第一个 \(1\) 的位置,即第一个 \(sum1_{i,k}-sum2_{i,j}>0\) 的位置。发现这个东西是满足单调性的,那么我们可以二分求解。
我们将从 \((x,y)\) 开始向右最远能种花的位置记为 \(pos1_{x,y}\)。
定义 \(sum2_{i,j}\) 表示第 \(i\) 列 \(a_{1,i}\) 到 \(a_{j,i}\) 的前缀和。那么从 \((x,y)\) 向下走,同理最远能够成为左下角的点就是第一个 \(sum2_{j,k}-sum2_{j,i}>0\) 的位置,这个东西同样满足单调性,也可以二分求解。将从 \((x,y)\) 开始向下走最远能成为左下角的点的位置记为 \(pos2_{x,y}\)。
那么对于一个点 \((x,y)\),满足题意的方案数就是分别考虑,当这个点成为左上角时,所有能够成为这个 \(\texttt C-\) 形左下下角的点的方案数之和乘上 \(pos1_{x,y}-x\)。
我们发现,对于每一个能成为左下角的点 \((x_0,y)\),它的方案数是 \(pos1_{x_0,y}-x_0\)。
那么总的答案就是对于所有 \((x_0,y)\) 且 \(x \le x_0\le pos2_{x,y}\) 的方案数求和。
可以用前缀和来预处理出来一个 \(sum_{i,j}\) 表示第 \(i\) 列从 \((j,1)\) 到 \((j,i)\) 的 \(pos1_{j,i}-i\) 之和。
那么我们就可以直接每次用 \(sum[j][pos2[i][j]]-sum[j][i+1]\) 就是能够成为这个 \(\texttt C-\) 形左下角的点的方案数之和。最后再给它乘上 \(pos1_{x,y}-x\) 即可。
对于 \(\texttt F-\),我们可以类比 \(\texttt C-\),即当一个点成为左上角时,所有能够成为 \(\texttt F-\) 形左下角的方案数之和是 \(suml[j][pos2[i][j]]-suml[j][i+1]\),其中 \(suml_{i,j}\) 表示的是第 \(i\) 列从 \((j,1)\) 到 \((j,i)\) 的 \((pos1_{j,i}-i)\times (pos2_{j,i}-j)\) 之和。
最后直接枚举每个 \(a_{i,j}= 0\) 的点作为左上角然后直接将所有方案数相加即可。
时间复杂度 \(O(nm \log n)\)。
代码实现
//luoguP8865
//A
#include<iostream>
#include<cstdio>
#include<cstring>
#define int long long
using namespace std;
const int mod=998244353;
const int maxn=1005;
int sum1[maxn][maxn],sum2[maxn][maxn],a[maxn][maxn],pos1[maxn][maxn],pos2[maxn][maxn];
int pos[maxn][maxn],cnt[maxn],sum[maxn][maxn],suml[maxn][maxn];
int n,m,c,f;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void Clear()
{
memset(sum1,0,sizeof(sum1));
memset(sum2,0,sizeof(sum2));
memset(cnt,0,sizeof(cnt));
memset(sum,0,sizeof(sum));
memset(suml,0,sizeof(suml));
}
signed main()
{
int T,id;
T=read();id=read();
while(T--)
{
n=read();m=read();c=read();f=read();
Clear();
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
for(int j=1;j<=m;j++)
{
a[i][j]=s[j-1]-'0';
sum1[i][j]=sum1[i][j-1]+a[i][j];
}
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
sum2[i][j]=sum2[i][j-1]+a[j][i];
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(a[i][j]==1){pos1[i][j]=j,pos2[i][j]=i;continue;}
int now=j;
for(int step=(1<<10);step>=1;step>>=1)
{
if(now+step<=m&&sum1[i][now+step]-sum1[i][j]==0)now+=step;
}
pos1[i][j]=now;
now=i;
for(int step=(1<<10);step>=1;step>>=1)
{
if(now+step<=n&&sum2[j][now+step]-sum2[j][i]==0)now+=step;
}
pos2[i][j]=now;
}
}
for(int i=1;i<=m;i++)
{
cnt[i]=1;
for(int j=1;j<=n;j++)
{
(sum[i][j]=sum[i][j-1]+pos1[j][i]-i)%=mod;
(suml[i][j]=suml[i][j-1]+(pos2[j][i]-j)*(pos1[j][i]-i)%mod)%=mod;
}
}
int ansc=0,ansf=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(pos2[i][j]>i+1)(ansc+=(pos1[i][j]-j)*((sum[j][pos2[i][j]]-sum[j][i+1]+mod)%mod)%mod)%=mod;
if(pos2[i][j]>i+1)(ansf+=(pos1[i][j]-j)*((suml[j][pos2[i][j]]-suml[j][i+1]+mod)%mod)%mod)%=mod;
}
}
cout<<ansc*c%mod<<" "<<ansf*f%mod<<'\n';
}
}
虽说我的做法时间复杂度没传统正解优秀,而且思路上被别人说也挺大便的。。但毕竟是我自己想出来的,而且没有写挂一遍 AC,所以还是记录下来吧。