题意
有 \(n\) 个数,你需要给每个数涂上红色或蓝色,使得任意两个红色的数不小于一个常数 \(A\),且任意两个蓝色的数不小于一个常数 \(B\)。求方案数。
\(n\le 10^5\)
\(1\le A,B,a_i\le 10^{18}\)
\(a_i\lt a_{i+1}\)
题解
solution 1
首先有个小学生都会写的 \(30\) 分暴力 \(\text{dp}\):设 \(dp(j,k)\) 表示涂完前 \(i\) 个数后,最后一个红数在第 \(j\) 位,最后一个蓝数在第 \(k\) 位。
显然状态中不用记 \(i\),因为 \(\max(j,k)=i\)。
#include
#define ll long long
#define N 2005
#define mod 1000000007
using namespace std;
inline ll read(){
ll x=0; bool f=1; char c=getchar();
for(;!isdigit(c); c=getchar()) if(c=='-') f=0;
for(; isdigit(c); c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
if(f) return x;
return 0-x;
}
ll n,a,b,p[N];
int dp[N][N];
int main(){
n=read(), a=read(), b=read();
if(n>2000){puts("0"); return 0;}
for(int i=1; i<=n; ++i) p[i]=read();
dp[0][1]=dp[1][0]=1;
for(int i=2; i<=n; ++i){
if(i-1==0 || p[i]-p[i-1]>=a) for(int j=0; j<=i-2; ++j) (dp[i][j]+=dp[i-1][j])%=mod;
if(i-1==0 || p[i]-p[i-1]>=b) for(int j=0; j<=i-2; ++j) (dp[j][i]+=dp[j][i-1])%=mod;
for(int j=0; j<=i-2; ++j){
if(!j || p[i]-p[j]>=a) (dp[i][i-1]+=dp[j][i-1])%=mod;
if(!j || p[i]-p[j]>=b) (dp[i-1][i]+=dp[i-1][j])%=mod;
}
}
int ans=0;
for(int i=0; i<=n; ++i) (ans+=((dp[n][i]+dp[i][n])%mod))%=mod;
cout<
然后我们发现这个转移好像就是一堆数组平移。
我们把 \(dp(j,k)\) 的矩阵画出来
一种颜色圈的区间对应一个 \(i\)。\(j=i\) 和 \(k=i\) 对应两个不相交的一维数组。
所以我们可以把 \(\text{dp}\) 的两维拆成两个数组分开处理,一个数组满足 \(j=i\),然后下标表示 \(k\);一个数组满足 \(k=i\),然后下标表示 \(j\)。
考虑预处理出每个前缀 \([1,i]\) 中最后一个满足 \(a_i-a_r\ge A\) 的 \(r\) 和最后一个满足 \(a_i-a_b\ge B\) 的 \(b\),则暴力代码可以改写为
\(query(x,y)\) 表示求 \(x\) 数组第 \(0\) 到 \(y\) 位的和。
我们发现问题可以简化为支持两个数组的整体复制、求前缀和、前缀清零、尾部加数。
主席树维护即可。复杂度 \(O(n\log n)\)。
啊呸,前缀清零直接打 \(\text{tag}\) 就行了,然后就可以求前缀和了。复杂度 \(O(n)\)。
(其实细节很好写的,为啥我这么弱智想了半天)
#include
#define ll long long
#define N 100002
#define mod 1000000007
using namespace std;
inline ll read(){
ll x=0; bool f=1; char c=getchar();
for(;!isdigit(c); c=getchar()) if(c=='-') f=0;
for(; isdigit(c); c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
if(f) return x;
return 0-x;
}
int n; ll A,B,x[N];
int r[N],b[N],dp[2][N],sum[2][N],lst[2];
int getSum(int c, int x){
if(x=A) ++r[i];
while(x[i]-x[b[i]+1]>=B) ++b[i];
}
dp[0][0]=dp[1][0]=1, sum[0][0]=sum[1][0]=1;
for(int i=2; i<=n; ++i){
(dp[0][i-1]+=getSum(1,min(r[i],i-2)))%=mod;
(dp[1][i-1]+=getSum(0,min(b[i],i-2)))%=mod;
if(r[i]!=i-1) lst[0]=i-1;
if(b[i]!=i-1) lst[1]=i-1;
sum[0][i-1]=(sum[0][i-2]+dp[0][i-1])%mod;
sum[1][i-1]=(sum[1][i-2]+dp[1][i-1])%mod;
}
//int ans=0;
//for(int i=lst[0]; i<=n; ++i) (ans+=dp[0][i])%=mod;
//for(int i=lst[1]; i<=n; ++i) (ans+=dp[1][i])%=mod;
cout<<(getSum(0,n-1)+getSum(1,n-1))%mod<
solution 2
一个与暴力无关的做法。
考虑从大到小钦定每个数为红色还是蓝色。
若 \(a_n\) 涂了红色,那么 \(a_{n-1},a_{n-2},\cdots\) 等数就必须涂蓝色。
比如有 \(6\) 个数:3 4 7 8 9 11
\(A=3,\space B=4\)
如果把 \(11\) 涂成红色,那么 \(8,9\) 就必须涂成蓝色。
而把 \(8,9\) 涂成蓝色,\(7\) 就必须涂成红色。
但是 \(7\) 和 \(8\) 都影响不到 \(4\) 的颜色。
所以设 \(dp(i,0/1)\) 表示涂完前 \(i\) 个数的方案数,那么 \(7,8,9,11\) 这些数单独组成一个影响连通块,这一块对 \(7\) 以前的数的颜色没有任何影响,可以有 \(dp(6)+=dp(2)\)。
现在我们要预处理出每个数最多往前影响多少位。
设 \(pos(i,0/1)\) 表示第 \(i\) 个数涂红/蓝色,它往前最近的影响不到的数在哪一位。
则 \(pos\) 可以递推,比如上例中,\(pos(6,0)=pos(4,1)=pos(3,0)=2\)。
于是用 \(dp\) 的递推式 \(dp(i)=dp(pos(i,0/1))\) 推出 \(dp(n)\) 即可。
当然,这个做法需要特判无解的情况。
比如上例中 \(11\) 涂成红色,\(8,9\) 就必须涂成蓝色,但 \(8,9\) 两个数差 \(1\),\(B=4\),所以不能同时涂成蓝色。这时 \(pos(6,0)=-1\)。最后计算 \(dp(i)\) 时跳过 \(pos(i,j)=-1\) 的情况。
我们需要用 \(\text{ST}\) 表、线段树等数据结构维护区间最小差分值。
复杂度 \(O(n\log n)\)。
code