LOJ 6435 「PKUSC2018」星际穿越——DP+倍增 / 思路+主席树
题目:https://loj.ac/problem/6435
题解:https://www.cnblogs.com/HocRiser/p/9166459.html
自己要怎样才能想到怎么做呢……
dp[ t ][ i ] 表示从 [ i , n ] 这些点出发,走 2t 步最左能走到哪。
sm[ t ][ i ] 表示从 [ i , n ] 出发,走到 [ dp[ t ][ i ] , i-1 ] 的最小步数和;比如一个终点 x 贡献的就是 [ i , n ] 里离 x 最近的那个点到 x 的距离。
sm[ t ][ i ] = sm[ t-1 ][ i ] + sm[ t-1 ][ dp[ t-1 ][ i ] ] + ( dp[ t-1 ][ i ] - dp[ t ][ i ] ) * 2t-1
后面那个乘 2t-1 的部分是这样考虑:
新扩展出来的部分是 [ dp[ t ][ i ] , dp[ t-1 ][ i ] ] 这部分。考虑从 [ i , n ] 走到其中一点 x 的长度,一定是 2t-1 再加上一个到 x 具体的步数。
走 2t-1 步之后的步数总和,一定恰好就是 sm[ t-1 ][ dp[ t-1 ][ i ] ] ;
不存在一个在 [ dp[ t ][ i ] , dp[ t-1 ][ i ] ] 里的点 x ,满足 sm[ t-1 ][ dp[ t-1 ][ i ] ] 里包含的步数已经包括了一些 “前 2t-1 步” 的部分。因为如果除去包含在 sm[ t-1 ][ dp[ t-1 ][ i ] ] 里的部分之后,剩余的部分不足 2t-1 步,那么 dp[ t-1 ][ i ] 还可以更多地扩展。
回答询问也可以类似地考虑。把询问 ( [ l , r ] , x ) 拆成 ( [ l , x-1 ] , x ) 和 ( [ r+1 , x-1 ] , x ) ,考虑从 x 开始往左尝试倍增。先走 2t 步走到一个位置 k (k >= l) ,然后可以把 x 移动到 k 那个位置接着尝试倍增;不过这次操作之后的所有路径都要先走 2t 步了,所以此时给答案贡献的是 sm[ t ][ x ] + ( dp[ t ][ x ] - l ) * 2t 。
但是要特别考虑有没有先向右走了一步。
可以先向左走一步,设从 x 走到 x',可以认为之后是可以 “从 [ x' , n ] 出发 ” 而不是 “ 从 x 点出发 ”了。
因为如果接下来继续向左走,合法。如果接下来从 [ x'+1 , n ] 的一个点开始走,那么相当于刚才 “向左走” 的一步其实是向右走或者走到了 [ x'+1 , x ] 的一个点上。
如果是相当于向右走的话,目标点一定是一步可以走到的。因为在最优决策里,下一步是要走到比 x 再往左的一个点,所以目标点一定也能从 x 一步走到。
#include<cstdio> #include<cstring> #include<algorithm> #define ll long long using namespace std; int rdn() { int ret=0;bool fx=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')fx=0;ch=getchar();} while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar(); return fx?ret:-ret; } int Mn(int a,int b){return a<b?a:b;} const int N=3e5+5,K=20; int n,c[N],bin[K],lm,dp[K][N]; ll sm[K][N]; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} ll cal(int l,int r) { if(l>=c[r])return r-l; ll ret=r-l; r=c[r];//ret=r-l for all based 1 for(int t=18;t>=0;t--) if(dp[t][r]>=l) { ret+=sm[t][r]; r=dp[t][r]; ret+=(ll)(r-l)*bin[t]; } if(r!=l)ret+=r-l; return ret; } int main() { n=rdn(); for(int i=2;i<=n;i++)c[i]=rdn(); bin[0]=1;for(lm=1;bin[lm-1]*2<=n;lm++)bin[lm]=bin[lm-1]*2; lm--; dp[0][n+1]=n+1; for(int i=n;i;i--) { dp[0][i]=Mn(dp[0][i+1],c[i]);//dp[0][1]=0 sm[0][i]=i-dp[0][i]; } for(int t=1;t<=lm;t++) for(int i=1;i<=n;i++) { if(!dp[t-1][i])continue; dp[t][i]=dp[t-1][dp[t-1][i]]; sm[t][i]=sm[t-1][i]+sm[t-1][dp[t-1][i]]+ (ll)(dp[t-1][i]-dp[t][i])*bin[t-1]; } int Q=rdn(),l,r,x; while(Q--) { l=rdn();r=rdn();x=rdn(); ll a=cal(l,x)-cal(r+1,x),b=r-l+1; ll g=gcd(a,b); printf("%lld/%lld\n",a/g,b/g); } return 0; }
还有主席树做法。感觉有些难理解……并且(对自己来说)很难想到。
题解:https://www.cnblogs.com/skylee03/p/9160594.html
https://blog.csdn.net/lvzelong2014/article/details/83211203
https://blog.csdn.net/As_A_Kid/article/details/80726327?utm_source=blogxgwz8
https://www.cnblogs.com/Khada-Jhin/p/9629188.html
想了半天它是什么意思、正确性之类的。下面用 c[ i ] 表示题面里的 l[ i ] 。
流程:1.每个点 i 找一个 mn[i] ,表示 [ i , n ] 里面最小的 c[ i ] (注意是 [ i , n ] 不是 [ i+1 , n ]);
2.每个点 i 开一个线段树维护 ” [ i-1 , n ] 的每个点到 i 的距离 “ ; 认为 ” 到 i 的距离 “ = ” 到 mn[ i ] 的距离+1 “ ,如果是在 [ mn[ i ] + 1 , i-1 ] 的点,认为到 i 的距离是 1 ;
所以 i 号点的线段树与 mn[ i ] 的线段树的不同就是 [ 1 , i-1 ] 的值 + 1 ;用主席树实现;
3.查询的时候, [ c[ i ] , i-1 ] 与询问区间 [ l , r ] 相交的部分的答案是 1 ;
4.剩余部分的答案就在线段树上查,认为到 i 的距离就是线段树上存的到 c[ i ] 的距离 + 1 ;
线段树里存的 ” 每个点到 i 的距离 “ 其实指的是 ”迂回“ 到 i 的距离;
比如 x 到 i 的路径就是 x 走到 ” c[ k ] = mn[ i ] 的那个点 k “ ,然后再走到 i ;线段树上的值没有计算最后 ”拐回去走到 i “ 的那一步。
像上述一样做出主席树,可以使得求出的 ”每个点迂回到 i 的步数“ 最小;正确性不是很清楚……
对于这步的过程的一个理解,可以考虑一个点 x ,在 mn[ i ] 的线段树上存了 x ”迂回“ 到 i 的距离;
因为发出 mn[ i ] 的点可以是 i 自己,所以这个 ”迂回“ 也包括 x 直接跳到了 i 点;
线段树上,从 mn[ i ] +1 就变成到 i 的距离,考虑 x 可以 ”迂回“ 到 mn[ i ] ,那么计入步数的最后一步一定是走到了 >=mn[ i ] 的一个点(那个点是发出 mn[ mn[i] ] 的那个点);
从那个点可以一步走到发出 mn[ i ] 的点的(因为该点 >=mn[ i ] ,所以可以走到发出 mn[ i ] 的点)。
查询的时候,在 [ c[ i ] , i-1 ] 之外的询问点,是用了线段树上存的到 c[ i ] 的值,然后 +1 作为到 i 的距离;
表示一个询问点 x ,先 ”迂回“ 地走到 c[ i ] ,但没有走最后一步,而是把最后一步改成走到 i 号点;
这是可以做到的,因为是 ”迂回“ ,所以走最后一步之前一定走到了 >= c[ i ] 的点;如果是 <= i-1 的,那么可以直接走到 i ;如果是 > i 的,因为可以跳到 c[ i ] ,所以可以一步跳到 i ;
这个最后一步不可能正好已经跳到了 i 点,因为 ”迂回到 i “ 是借道发出 mn[ i ] 的那个点的;如果倒数第二步跳到 i 点,说明 mn[ c[ i ] ] = c[ i ] ,但 mn[ i ] < i ,因为 mn[ i ] 对 c[ i ] 取了 min 。
但不太明白这为什么是最优的……
#include<cstdio> #include<cstring> #include<algorithm> #define ll long long #define ls Ls[cr] #define rs Rs[cr] using namespace std; int rdn() { int ret=0;bool fx=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')fx=0;ch=getchar();} while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar(); return fx?ret:-ret; } int Mn(int a,int b){return a<b?a:b;} int Mx(int a,int b){return a>b?a:b;} const int N=3e5+5,M=40*N; int n,c[N],pr[N]; int rt[N],tot,Ls[M],Rs[M],tg[M]; ll sm[M]; ll gcd(ll a,ll b){return b?gcd(b,a%b):a;} int nwnd(int pr) { int cr=++tot; ls=Ls[pr]; rs=Rs[pr]; tg[cr]=tg[pr]; sm[cr]=sm[pr]; return cr; } void mdfy(int l,int r,int &cr,int pr,int L,int R) { cr=nwnd(pr); sm[cr]+=Mn(r,R)-Mx(l,L)+1;// if(l>=L&&r<=R){ tg[cr]++;return;} int mid=l+r>>1; if(L<=mid)mdfy(l,mid,ls,Ls[pr],L,R); if(mid<R)mdfy(mid+1,r,rs,Rs[pr],L,R); } ll qry(int l,int r,int cr,int L,int R) { if(!cr)return 0;// if(l>=L&&r<=R)return sm[cr]; ll ret=(Mn(r,R)-Mx(l,L)+1)*tg[cr]; int mid=l+r>>1; if(L<=mid)ret+=qry(l,mid,ls,L,R); if(mid<R)ret+=qry(mid+1,r,rs,L,R); return ret; } int main() { n=rdn(); for(int i=2;i<=n;i++)c[i]=rdn(); pr[n]=c[n]; for(int i=n-1;i;i--) pr[i]=Mn(pr[i+1],c[i]); for(int i=2;i<=n;i++) mdfy(1,n,rt[i],rt[pr[i]],1,i-1); int Q=rdn(),l,r,x; while(Q--) { l=rdn();r=rdn();x=rdn(); ll a=r-l+1,b=a; r=Mn(r,c[x]-1); if(l<=r) { ll d=qry(1,n,rt[c[x]],l,r); //c[x]!!! not pr[x] / x!!! a+=d; } ll g=gcd(a,b); printf("%lld/%lld\n",a/g,b/g); } return 0; }