/* 记一串数字真难。 5435 今天比赛又是hjcAK的一天。 今天开题顺序是312,在搞T1之前搞了T3 昨天某谷月赛真是毒瘤。 但是讲评的同学不错,起码T4看懂了... 构造最优状态然后DP的思路真妙 */
Problem A lcp
给出字符串S,m个询问,每个询问含有$l1,r1,l2,r2$求|S|子串$[l1,r1]$和$[l2,r2]$的LCP(最长公共前缀)
对于100%的数据$ 1 \leq |S|,m \leq 10^5 , l1 \leq r1 ,l2 \leq r2$
考虑二分答案套字符串Hash,于是就不用KMP了(我不会KMP)
再说一下Hash的思路吧hash[i]表示S前i个字符的前缀哈希值,设基底为E=$31$,模数mo=$10^9+9$
令hash[0]=0;对于$i \geq 1 计算方法如下 :hash[i]=hash[i-1] \times E+Val(s[i]) $ Val(x)是一个映射把char类型的x映射成一个int类型
所以利用前缀和的思想,如果不计模造成的负数问题$ Hash(l,r)=hash[r]-hash[l-1] \times E^{r-l+1} $
如果考虑模数造成负数问题那么要多mo几次,即 Hash(l,r) = ( (hash[r] - (hash[l-1] * pow[r-l+1] % mo) ) % mo + mo) % mo
得函数Hash(l,r)表示串S的子串[l,r]的哈希值。
那么这样就可以$O(1)$判断两个子串是不是相等了。套个二分就过了。
复杂度$O(m log_2 n)$
# include <bits/stdc++.h> # define int long long # define hash HASH # define pow Pow using namespace std; const int N=1e5+10; const int mo=1e9+7; const int E=51; char s[N]; int n,m,hash[N],pow[N]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } int val(char ch){return ch-'a';} int Hash(int l,int r){ return ((hash[r]-hash[l-1]*pow[r-l+1]%mo)%mo+mo)%mo; } bool check(int len,int l1,int l2){ if ((int) Hash(l1,l1+len-1)==(int) Hash(l2,l2+len-1)) return 1; else return 0; } signed main() { freopen("lcp.in","r",stdin); freopen("lcp.out","w",stdout); n=read();m=read(); scanf("%s",s+1); pow[0]=1; for (int i=1;i<=n;i++) pow[i]=pow[i-1]*E%mo; for (int i=1;i<=n;i++) hash[i]=(hash[i-1]*E+val(s[i]))%mo; int l1,r1,l2,r2; while (m--) { l1=read();r1=read();l2=read();r2=read(); int l=0,r=min(r1-l1+1,r2-l2+1),ans=0; while (l<=r) { int mid=(l+r)>>1; if (check(mid,l1,l2)) ans=mid,l=mid+1; else r=mid-1; } printf("%lld\n",ans); } return 0; }
Problem B Save
现在有n个粮仓,从高到低且间距是1被排在一个山坡上。对于一个粮仓可以进行扑灭和转移操作。
其中扑灭操作的代价是 b[i] , 转移操作的代价是a[i] ( 转移到最近的一个高度较低的已经执行过扑灭操作的粮仓且满足距离差$\Delta d \leq $d[i] )
对于 n 个粮仓执行完毕两个操作中的一个,求最小代价。
对于100%的数据$n \leq 10^5 , a[i],b[i] \leq 100 $
首先考虑一个问题第n个粮仓是一定是需要扑灭的。
那么f[i]就表示为前i个粮仓不被烧毁时候的最小代价,那么显然第i个粮仓的操作是不能为转运。
那么f[i]必然满足第i个粮仓是扑灭操作的。
我们考虑f[i]从f[j]转移过来,其中j应该是一个可以被转移过来的值,即$max(0,i - d[i] - 1) \leq j < i $
那么对于[j+1,i)这么多个粮仓,每一个粮仓只能从上到下转移到第i个粮仓 , 在所有转移中的最小值就是 f[i] 的最优值。
即$f_i = \min\limits_{max(0,i-d_i-1)\leq j<i} \{ f_j + \sum\limits_{k=j+1}^{i-1} a_k + b_j\}$
但是如果是任意一个的话,那么在[j+1,i)这段中可以选择扑灭,也可以选择原地建站,
而由于每一个物品运费是一样的,那么在这个区间中无论运到中间某一个站点,或者直接运到i点对答案贡献影响是一样的。
我们就可以考虑这个贪心,也就是如果这个点运费比扑灭代价更小那么就直接运到i,否则原地建站。
这个时候dp方程就是 $f_i = \min\limits_{max(0,i-d_i-1)\leq j<i} \{ f_j + \sum\limits_{k=j+1}^{i-1} \min \{ a_k , b_k\} + b_j\}$
显然可以前缀和优化到$O(n^2)$
即 $ 令 s_i = \sum\limits _{j=1}^i a_j 或 s_i = \sum\limits _{j=1}^i \min \{ a_j , b_j \} $
转移改写为 $f_i= \min \limits_{max(0,i-d_i-1)\leq j <i} \{ f_j-s_j \} +b_i +s_{i-1} $
观察这个dp式子发现对于一个确定的j,$f_j-s_j$ 是一个定值,而后面和j没有关系只和i有关系,可看做常量,
可以使用数据结构线段树 (或者二分法优化) , 即记录 区间max值,注意下标不能为0所以所有在线段树中下标+1;
这里介绍一种不使用线段树的方法(即二分法优化),单调栈+二分查找,也可以完成类似的操作,
头部不弹出,因为之前用到的虽然不在当前范围内,但是后续可能用到头部被弹出的点,造成答案偏大
尾部可以弹出,保证栈单调递增,因为当前进来的元素比尾部的离下个决策更近且更优,所以前面那个决策就不用了。正确性显然。
然后每一个元素进队1次,出队1次,然后查找最大复杂度是$ O(log_2 n) $ ,整个算法复杂度为$O(n log_2 n)$
标程给的是“最近的”,我被坑了!!!
复杂度$O(n log_2 n)$
# include <bits/stdc++.h> # define fp(i,s,t) for (int i=s;i<=t;i++) # define inf (0x3f3f3f3f) using namespace std; const int N=1e5+10; int a[N],b[N],d[N],s[N],n,f[N],c[N<<2]; inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void update(int x,int l,int r,int pos,int opx) { if (l==r) {c[x]=opx;return;} int mid=(l+r)/2; if (pos<=mid) update(2*x,l,mid,pos,opx); else update(2*x+1,mid+1,r,pos,opx); c[x]=min(c[2*x],c[2*x+1]); } int query(int x,int l,int r,int opl,int opr) { if (opl<=l&&r<=opr) return c[x]; int mid=(l+r)/2; int ret=inf; if (opl<=mid) ret=min(ret,query(2*x,l,mid,opl,opr)); if (opr>mid) ret=min(ret,query(2*x+1,mid+1,r,opl,opr)); return ret; } int main() { freopen("save.in","r",stdin); freopen("save.out","w",stdout); n=read(); s[0]=0; fp(i,1,n) a[i]=read(); fp(i,1,n) b[i]=read(); fp(i,1,n) d[i]=read(),s[i]=s[i-1]+a[i]; memset(c,0x3f,sizeof(c)); memset(f,0x3f,sizeof(f)); f[0]=0; update(1,1,n,1,0); for (int i=1;i<=n;i++) { f[i]=query(1,1,n,max(0,i-d[i]-1)+1,i-1+1)+b[i]+s[i-1]; update(1,1,n,i+1,f[i]-s[i]); } printf("%d\n",f[n]); return 0; }
Problem C Seven
树上路径时7的倍数的有几条?
对于100%的数据 $n \leq 10^5 $
直接淀粉质就行,参考https://www.cnblogs.com/ljc20020730/p/10347198.html
这里贴一下代码,和P2634 [国家集训队]聪聪可可 是一个题目。
复杂度是$O(n log_2 n)$
# include <bits/stdc++.h> # define int long long # define LL long long using namespace std; const int N=2e5+10; struct rec{ int pre,to,w; }a[N<<1]; int n,tot,ans,SIZE,root; int head[N],d[N],size[N],f[N]; int r[7]; bool use[N]; int max(int x,int y){return (x>y)?x:y;} int min(int x,int y){return (x>y)?y:x;} inline int read() { int X=0,w=0; char c=0; while(c<'0'||c>'9') {w|=c=='-';c=getchar();} while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar(); return w?-X:X; } void adde(int u,int v,int w) { a[++tot].pre=head[u]; a[tot].to=v; a[tot].w=w; head[u]=tot; } void Get_Root(int u,int fath) { f[u]=0,size[u]=1; for (int i=head[u];i;i=a[i].pre) { int v=a[i].to; if (v==fath||use[v]) continue; Get_Root(v,u); f[u]=max(f[u],size[v]); size[u]+=size[v]; } f[u]=max(f[u],SIZE-size[u]); if (f[u]<f[root]) root=u; } int cnt; void Get_Dist(int u,int fath,int L) { r[L%7]++; d[++cnt]=L; size[u]=1; for (int i=head[u];i;i=a[i].pre) { int v=a[i].to; if (v==fath||use[v]) continue; Get_Dist(v,u,L+a[i].w); size[u]+=size[v]; } } int Get_Ans(int u,int fath,int L) { cnt=0; int ret=0; memset(r,0,sizeof(r)); Get_Dist(u,fath,L); for (int i=1;i<=cnt;i++) if (d[i]%7==0) ret+=r[0]-1; else if (d[i]%7==1) ret+=r[6]; else if (d[i]%7==2) ret+=r[5]; else if (d[i]%7==3) ret+=r[4]; else if (d[i]%7==4) ret+=r[3]; else if (d[i]%7==5) ret+=r[2]; else if (d[i]%7==6) ret+=r[1]; return ret; } void solve(int u) { use[u]=true; ans+=(LL)Get_Ans(u,0,0); for (int i=head[u];i;i=a[i].pre){ int v=a[i].to; if (use[v]) continue; ans-=(LL)Get_Ans(v,u,a[i].w); SIZE=size[v],root=0; Get_Root(v,u); solve(root); } } signed main() { freopen("seven.in","r",stdin); freopen("seven.out","w",stdout); f[0]=1e9; n=read(); int u,v,w; for (int i=1;i<n;i++) { u=read();v=read();w=read(); adde(u,v,w); adde(v,u,w); } SIZE=n; root=0; Get_Root(1,0); solve(root); printf("%lld\n",ans>>1); return 0; }
我们应该学会一题多解。所以这道题有O(n)的树形dp可以解决。
有些树形dp是不会有重复的,比如这道题目。
我们可以设f[u][i] 其中$i \in [0,6] $表示 u下面的子树 的节点有多少个v和u距离d%7=i的。
我们f[u][i]可以从儿子f[v][i] 那里转移而来
首先设c[i] ($ i\in [0,6] $)数组表示u下面的某个确定儿子$ u_{son} $子树节点有多少个v和$ u_{lson}$ 距离为 $ d_{son} \% 7 =i $
这个c数组可以从$ f[v][i] + w_{now} $ 转移过来,即 $ c[(i+w) \% 7]=f[v][i] $
然后统计答案,也就是说当前c数组知识表示f[u][i]的一部分,而且这一部分是没有被加到f[u][i]中的,那么
已经被加入f[u][i]的答案 到 当前子树的每一个点 可以存在路径 符合条件 从这里 求答案 即 $ ans += f[u][i] \times c[ (7-i) \% 7 ],i\in [0,6] $
然后把 c数组统计到f[u][i]中 $ f[u][i]+=c[i] , i \in [0,6] $
复杂度$ O(n) $
# include <bits/stdc++.h> using namespace std; # define int long long const int N=1e5+10; struct rec{ int pre,to,w; }a[N<<1]; int n,ans,head[N],tot; int f[N][7],c[7]; void adde(int u,int v,int w) { a[++tot].pre=head[u]; a[tot].to=v; a[tot].w=w; head[u]=tot; } void dfs(int u,int fath) { f[u][0]=1; for (int i=head[u];i;i=a[i].pre){ int v=a[i].to; if (v==fath) continue; dfs(v,u); for (int j=0;j<=6;j++) c[(j+a[i].w)%7]=f[v][j]; for (int j=0;j<=6;j++) ans+=c[j]*f[u][(7-j)%7]; for (int j=0;j<=6;j++) f[u][j]+=c[j]; } } signed main() { scanf("%lld",&n); int u,v,w; for (int i=1;i<n;i++) scanf("%lld%lld%lld",&u,&v,&w), adde(u,v,w),adde(v,u,w); dfs(1,0); cout<<ans; return 0; }
( The End )