不带删尺取小记
前言
期末考考了,当时用倍增+ST表多了个 \(\log\) 做的
适用于双指针删除时复杂度很高,添加时复杂度很低的情形。
大概有两种类型,本质是一样的,只是写法有点区别。
双指针的移动与限制条件有关
CF1548B Integers Have Friends
分析
差分后转化为求区间 \(\gcd>1\) 的最长区间长度
有很多做法,带 \(\log n\) 的就是二分+ST表或者线段树+双指针
双指针就是只要区间 \(\gcd=1\) 左指针就不断右移。
继续考虑双指针,如果能将区间分成两段 \([l,mid],[mid+1,r]\)
前一段为后缀 \(\gcd\),后一段为前缀 \(\gcd\),然后就可以直接拼起来,双指针的移动也非常简单。
问题就是怎样判断后缀 \(\gcd\) 在什么时候计算,有一种办法,
就是当 \(l>mid\) 的时候直接将 \([l,r]\) 全部变成后缀 \(\gcd\),然后将 \(l,mid\) 全部赋值为 \(r\),
每个位置最多进行一次前缀 \(\gcd\) 和一次后缀 \(\gcd\),所以复杂度就是 \(O(n\log {a_i})\)
代码
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
typedef long long lll;
const int N=200011;
int n,l,mid,ans; lll a[N],f[N];
lll iut(){
lll ans=0,f=1; char c=getchar();
while (!isdigit(c)) f=(c=='-')?-f:f,c=getchar();
while (isdigit(c)) ans=ans*10+c-48,c=getchar();
return ans*f;
}
void print(int ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
lll gcd(lll x,lll y){return y?gcd(y,x%y):x;}
int main(){
for (int T=iut();T;--T){
n=iut(),l=mid=ans=0,f[0]=a[0]=1;
for (int i=1;i<=n;++i) a[i]=iut();
for (int i=1;i<n;++i)
if (a[i]<a[i+1]) a[i]=a[i+1]-a[i];
else a[i]-=a[i+1];
--n;
for (int i=1;i<=n;++i){
f[i]=(i-1==mid)?a[i]:gcd(f[i-1],a[i]);
while (l<=mid&&gcd(f[i],f[l])==1) ++l;//后缀与前缀拼接判断gcd
if (l>mid){
f[mid=i]=a[i];
for (int j=mid-1;j>=l;--j) f[j]=gcd(f[j+1],a[j]);
while (l<=mid&&f[l]==1) ++l;//重构之后只需要看f[l]
}
ans=max(ans,i-l+1);
}
print(ans+1),putchar(10);
}
return 0;
}
CF1547F Array Stabilization (GCD version)
分析
首先要断环为链,非常浅显的做法就是二分+ST表,判断当前判定的次数下是否能够使区间 \(\gcd\) 等于所有数的 \(\gcd\)
可以发现,还是可以用线段树+双指针做,指针移动就变成了不等于所有数的 \(\gcd\),
转化题意后和上一题本质是一样的,翻转数组就差不多了,可以利用不带删尺取
代码
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=400011;
int n,l,mid,ans,a[N],f[N];
int iut(){
int ans=0,f=1; char c=getchar();
while (!isdigit(c)) f=(c=='-')?-f:f,c=getchar();
while (isdigit(c)) ans=ans*10+c-48,c=getchar();
return ans*f;
}
void print(int ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
int gcd(int x,int y){return y?gcd(y,x%y):x;}
int main(){
for (int T=iut();T;--T){
n=iut(),l=mid=ans=0,a[0]=0;
for (int i=1;i<=n;++i) a[i]=iut(),a[0]=gcd(a[0],a[i]);
reverse(a+1,a+1+n),f[0]=a[0];
for (int i=1;i<=n;++i) a[i+n]=a[i];
for (int i=1;i<n*2;++i){
f[i]=(i-1==mid)?a[i]:gcd(f[i-1],a[i]);
while (l<=mid&&gcd(f[i],f[l])==a[0]) ++l;
if (l>mid){
f[mid=i]=a[i];
for (int j=mid-1;j>=l;--j) f[j]=gcd(f[j+1],a[j]);
while (l<=mid&&f[l]==a[0]) ++l;
}
ans=max(ans,i-l+1);
}
print(ans),putchar(10);
}
return 0;
}
JZOJ 6358 小ω的仙人掌
小ω有 \(s\) 个物品,每个物品有一定的大小和权值。
她可以从任意第 \(L\) 个物品走到第 \(R\) 个物品,这个区间内的物品可以选或者不选。
她取出的物品大小和必须为 \(w\),权值和必须小于等于 \(k\)。
她想知道这个区间最短是多少。如果无解,请输出“-1”,大样例来自 ?
对于 \(100\%\) 的测试点,保证 \(1\leq s\leq 10^4,1\leq b_i\leq 2\times 10^4,1\leq a_i\leq w\leq 5\times 10^3,1\leq k\leq 10^9\)
分析
还是一样考虑双指针,问题是背包删除不好弄,
那就用不带删尺取变成后缀背包和前缀背包的结合。
注意这里求的是最短区间所以要在指针移动时求最小值
代码
#include <cstdio>
#include <cctype>
using namespace std;
const int N=5011;
int dp[N<<1][N],w[N<<1],c[N<<1],n,m,k,l,mid,ans;
int iut(){
int ans=0; char c=getchar();
while (!isdigit(c)) c=getchar();
while (isdigit(c)) ans=ans*10+c-48,c=getchar();
return ans;
}
int min(int a,int b){return a<b?a:b;}
int main(){
n=iut(),m=iut(),k=iut(),ans=n+1;
dp[0][0]=0;
for (int i=1;i<=m;++i) dp[0][i]=0x3f3f3f3f;
l=mid=0;
for (int i=1;i<=n;++i){
w[i]=iut(),c[i]=iut();
if (i-1==mid){
dp[i][0]=0,dp[i][w[i]]=c[i];
for (int j=1;j<w[i];++j) dp[i][j]=0x3f3f3f3f;
for (int j=m;j>w[i];--j) dp[i][j]=0x3f3f3f3f;
}else{
for (int j=0;j<w[i];++j) dp[i][j]=dp[i-1][j];
for (int j=w[i];j<=m;++j)
dp[i][j]=min(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
}
for (;l<=mid;++l){
int flag=1;
for (int j=0;j<=m&&flag;++j)
if (dp[l][j]+dp[i][m-j]<=k) flag=0;
if (flag) break;
ans=min(ans,i-l+1);
}
if (l>mid){
mid=i;
dp[mid][0]=0,dp[mid][w[mid]]=c[mid];
for (int j=1;j<w[mid];++j) dp[mid][j]=0x3f3f3f3f;
for (int j=m;j>w[mid];--j) dp[mid][j]=0x3f3f3f3f;
for (int _i=mid-1;_i>=l;--_i){
for (int j=0;j<w[_i];++j) dp[_i][j]=dp[_i+1][j];
for (int j=w[_i];j<=m;++j) dp[_i][j]=min(dp[_i+1][j],dp[_i+1][j-w[_i]]+c[_i]);
}
while (l<=mid&&dp[l][m]<=k) ans=min(ans,i-l+1),++l;
}
}
if (ans==n+1) printf("-1");
else printf("%d",ans);
return 0;
}
双指针的移动与所求区间有关
OJ 1-F 矩阵滑窗
题目大意
给定 \(n\) 个 \(k\times k\) 的矩阵 \(A_i\),现在有 \(q\) 组询问,每组询问 \(\prod_{o=l_i}^{r_i}A_o\)
对于 \(100\%\) 的测试点满足 \(n\leq 10^5,k\leq 4,q\leq 10^5,\forall j\leq i,l_j\leq l_i,r_j\leq r_i\)
分析
由于区间是锁定的,所以两个指针有一点区别,\(\_l,\_mid\) 取代了原来的 \(mid,r\)
其实也差不多,\(l>\_l\) 的时候就重构,\(\_mid\) 就是右指针的移动
\([l,\_l]\) 维护的是后缀矩阵积,\([\_l,\_mid]\) 维护的是前缀矩阵积
代码
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=100011;
int n,m,Q,_l,_mid;
struct Matrix{
int p[4][4];
Matrix operator *(const Matrix &T)const{
Matrix C;
for (int j=0;j<4;++j)
for (int k=0;k<4;++k) C.p[j][k]=0;
for (int i=0;i<m;++i)
for (int j=0;j<m;++j)
for (int k=0;k<m;++k)
C.p[i][j]=(C.p[i][j]+1ll*p[i][k]*T.p[k][j])%919260817;
return C;
}
}A[N],B[N],C;
int iut(){
int ans=0; char c=getchar();
while (!isdigit(c)) c=getchar();
while (isdigit(c)) ans=ans*10+c-48,c=getchar();
return ans;
}
void print(int ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
int main(){
n=iut(),m=iut(),Q=iut();
_l=_mid=0;
for (int i=1;i<=n;++i){
for (int j=0;j<m;++j)
for (int k=0;k<m;++k)
A[i].p[j][k]=iut();
}
for (int i=1;i<=Q;++i){
int l=iut(),r=iut();
Matrix C;
if (l>_l){
B[r]=A[r];
for (int j=r-1;j>=l;--j) B[j]=A[j]*B[j+1];
_l=r,_mid=r,C=B[l];
}else{
if (_l==_mid&&_mid<r)
++_mid,B[_mid]=A[_mid];
while (_mid<r) ++_mid,B[_mid]=B[_mid-1]*A[_mid];
C=B[l]*B[r];
}
for (int j=0;j<m;++j)
for (int k=0;k<m;++k)
print(C.p[j][k]),putchar(32);
putchar(10);
}
return 0;
}