8.10 训练总结
A. Intervals
个人认为比较水的一道题。
首先我们看数据范围:\(1\le l\le r\le N\le 2\times10^5\),那显然我可以直接在这个 01 串上面计算答案。既然我都可以直接在 01 串上做了,那直接使用 DP。
设 \(f_i\) 表示第 \(i\) 个位置是最后一个 \(1\) 时的最优答案。显然有转移 \(f_i=\max_{j=0}^{i-1}\{f_j+w(j,i)\}\)。
我们考虑这个 \(w(i,j)\) 怎么算。首先我们要加上 \(i\) 位置是 \(1\) 时带来的贡献,显然就是 \(\sum_{l_k\le i\le r_k}a_k\),但是有部分贡献我们可能前面已经算过了,因此我们要减去 \(\sum_{l_k\le j\lt i\le r_k}a_k\)。
为啥直接减掉这个就可以了呢?显然如果你一条线段穿过了 \(j\) 前面的某个 \(1\),那必然也会穿过 \(j\) 位置的 \(1\),所以所有重复贡献的东西就等同于同时穿过 \(i\) 和 \(j\) 做的贡献。
我们重写一下转移方程:\(f_i=\max_{j=0}^{i-1}\{f_j+\sum_{l_k\le i\le r_k}a_k-\sum_{l_k\le j\lt i\le r_k}a_k\}\),我们就来考虑如何快速维护这俩玩意儿。
显然 \(\sum_{l_k\le i\le r_k}a_k\) 可以反过来看作 \(a_k\) 对 \([l_k,r_k]\) 作了 \(a_k\) 的贡献,这个贡献是可以差分做的。
后面那堆东西稍微有点难搞:我们先考虑满足 \(l_k\le i\le r_k\) 怎么做。显然你从前往后扫的过程就是一个扫描线,因此我们可以在 \(l_k\) 和 \(r_k+1\) 两个位置分别标记一下。现在的条件就变成了 \(\max_{j=0}^{i-1}\{f_j-\sum_{l_k\le j\le r_k}a_k\}\),后面那个东西根前面的很像,但是因为我们没法直接用差分,所以我们可以使用线段树优化,于是这就变成了一个区间修改、区间查询问题。
代码:
const int N=2e5+6;
struct seg_tree{
struct Node{
int l,r,mx,lt;
}node[N<<2];
void build(int l,int r,int pos)
{
node[pos].l=l,node[pos].r=r,node[pos].mx=node[pos].lt=0;
if(l==r)
{
return;
}
int mid=l+r>>1;
build(l,mid,pos<<1);
build(mid+1,r,pos<<1|1);
}
void pushdown(int pos,int ls,int rs)
{
if(node[pos].lt)
{
node[ls].mx+=node[pos].lt,node[rs].mx+=node[pos].lt;
node[ls].lt+=node[pos].lt,node[rs].lt+=node[pos].lt;
node[pos].lt=0;
}
}
void pushup(int pos,int ls,int rs)
{
node[pos].mx=max(node[ls].mx,node[rs].mx);
}
void update(int pos,int l,int r,int x)
{
if(node[pos].l>=l&&node[pos].r<=r)
{
node[pos].lt+=x;
node[pos].mx+=x;
return;
}
pushdown(pos,pos<<1,pos<<1|1);
int mid=node[pos].l+node[pos].r>>1;
if(l<=mid)
{
update(pos<<1,l,r,x);
}
if(r>mid)
{
update(pos<<1|1,l,r,x);
}
pushup(pos,pos<<1,pos<<1|1);
}
int query(int pos,int l,int r)
{
if(l>r)
{
return 0;
}
if(node[pos].l>=l&&node[pos].r<=r)
{
return node[pos].mx;
}
pushdown(pos,pos<<1,pos<<1|1);
int mid=node[pos].l+node[pos].r>>1,ans=-1e18;
if(l<=mid)
{
ans=max(ans,query(pos<<1,l,r));
}
if(r>mid)
{
ans=max(ans,query(pos<<1|1,l,r));
}
return ans;
}
}tr;
int n,m,f[N],sum[N];
vector<tuple<int,int,int>>v[N];
signed main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int l=read(),r=read(),s=read();
sum[l]+=s,sum[r+1]-=s;
v[l].push_back(make_tuple(l,r,-s));
v[r+1].push_back(make_tuple(l,r,s));
}
for(int i=1;i<=n;i++)//差分
{
sum[i]+=sum[i-1];
}
tr.build(1,n,1);
for(int i=1;i<=n;i++)
{
for(auto j:v[i])//扫描线
{
tr.update(1,get<0>(j),get<1>(j),get<2>(j));
}
f[i]=sum[i]+max(tr.query(1,1,i-1),0ll);//注意:因为 j 是可以等于 0 的,此时 f[j]=0,所以要和 0 取最大
tr.update(1,i,i,f[i]);
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans=max(ans,f[i]);
}
write(ans);
return 0;
}
B. Dynamic Diameter
思路很清奇的一道题。
我们考虑求直径的常用方法:
-
dfs:根据直径的性质,县找到离根节点最远的点,再找到离这个点最远的点,之间的距离就是直径。在此题中显然不可用。
-
DP:设 \(f_x\) 表示离 \(x\) 最远的点与 \(x\) 的距离,\(g_x\) 表示 \(x\) 子树内的直径,然后转移。我不知道动态 DP 好不好维护,不过标签给了那应该是可以写的。
-
点分治:每次找到 \(x\) 的所有子树中离 \(x\) 最远且不在同一子树内的两个点,将这两点之间的距离与当前的直径比较,更大的为直径。这个应该可以用点分树维护,不过有点难写所以先暂时不考虑。
-
暴力(当别的做法都不好做时回归本质似乎更简单):显然直径的定义是 \(\max_{1\le x,y\le n\land x\not=y}\{\operatorname{dis}(x,y)\}\),写开就是 \(\max_{1\le x,y\le n\land x\not=y}\{dis_x+dis_y-2\times dis_{\operatorname{LCA}(x,y)}\}\)。
我们考虑第四种方法能不能实现时间复杂度正确的情况下还能动态维护。
因为要求 LCA,所以我们再考虑怎么在求 LCA 的同时维护这个东西。
求 LCA 的常用方法:
-
倍增:通过倍增跳父节点的方法找 LCA,这里显然无法维护。
-
树剖:这里不多讲,或许真的可以通过树剖来维护这个东西,没试过但理论可以。
-
欧拉序:将整棵树拍成一个欧拉序然后将求 LCA 问题转化为求 RMQ 问题。此时上面的式子可以被转化为 \(\max_{1\le x\le y\le 2n-1}\{dis_x+dis_y-2\times\min_{x\le l\le y}dis_l\}\)。
你会发现这个东西看着很像能维护的样子啊。所以我们考虑如何维护这个东西。
显然,区间修改加询问,我们肯定使用线段树。所以只需要维护 \(\max dis_x,\min dis_x,\max\{dis_x-2\times\min dis_l\},\max\{dis_y-2\times dis_l\},\max\{dis_x+dis_y-2\times\min dis_l\}\),然后你就会发现维护上面那些式子就是线段树经典操作,这里不多讲。
如果修改了一条边,显然只会对这条边中靠下的那个点的子树内的所有 \(dis\) 产生影响,放在欧拉序上就是一个区间修改。所以时间复杂度:\(O(q\log n)\)。
代码:
const int N=1e5+6;
struct edge{
int x,y,s;
}e[N];
int n,q,w,cnt,dfn[N<<1],L[N],R[N],dis[N];
vector<pair<int,int>>v[N];
struct seg_tree{
struct Node{
int l,r,ans,dis1,dis2,mn,mx,lt;
}node[N<<3];
void pushup(int pos,int ls,int rs)
{
node[pos].ans=max({node[ls].ans,node[rs].ans,node[ls].dis1+node[rs].mx,node[rs].dis2+node[ls].mx});
node[pos].dis1=max({node[ls].dis1,node[rs].dis1,node[ls].mx-2*node[rs].mn});
node[pos].dis2=max({node[ls].dis2,node[rs].dis2,node[rs].mx-2*node[ls].mn});
node[pos].mx=max(node[ls].mx,node[rs].mx);
node[pos].mn=min(node[ls].mn,node[rs].mn);
}
void build(int l,int r,int pos)
{
node[pos].l=l,node[pos].r=r,node[pos].dis1=node[pos].dis2=node[pos].mn=node[pos].mx=0;
if(l==r)
{
node[pos].dis1=node[pos].dis2=-dis[dfn[l]];
node[pos].mn=node[pos].mx=dis[dfn[l]];
return;
}
int mid=l+r>>1;
build(l,mid,pos<<1);
build(mid+1,r,pos<<1|1);
pushup(pos,pos<<1,pos<<1|1);
}
void pushdown(int pos,int ls,int rs)
{
if(node[pos].lt)
{
node[ls].dis1-=node[pos].lt,node[rs].dis1-=node[pos].lt;
node[ls].dis2-=node[pos].lt,node[rs].dis2-=node[pos].lt;
node[ls].mn+=node[pos].lt,node[rs].mn+=node[pos].lt;
node[ls].mx+=node[pos].lt,node[rs].mx+=node[pos].lt;
node[ls].lt+=node[pos].lt,node[rs].lt+=node[pos].lt;
node[pos].lt=0;
}
}
void update(int pos,int l,int r,int x)
{
if(node[pos].l>=l&&node[pos].r<=r)
{
node[pos].dis1-=x;
node[pos].dis2-=x;
node[pos].mn+=x;
node[pos].mx+=x;
node[pos].lt+=x;
return;
}
pushdown(pos,pos<<1,pos<<1|1);
int mid=node[pos].l+node[pos].r>>1;
if(l<=mid)
{
update(pos<<1,l,r,x);
}
if(r>mid)
{
update(pos<<1|1,l,r,x);
}
pushup(pos,pos<<1,pos<<1|1);
}
}tr;
void dfs(int x,int fa)
{
dfn[++cnt]=x;
L[x]=cnt;
for(auto i:v[x])
{
if(i.first==fa)
{
continue;
}
dis[i.first]=dis[x]+i.second;
dfs(i.first,x);
dfn[++cnt]=x;
}
R[x]=cnt;
}
signed main()
{
n=read(),q=read(),w=read();
for(int i=1,x,y,s;i<n;i++)
{
x=read(),y=read(),s=read();
e[i]=(edge){x,y,s};
v[x].push_back(make_pair(y,s));
v[y].push_back(make_pair(x,s));
}
dfs(1,0);
int la=0;
tr.build(1,cnt,1);
while(q--)
{
int id=(read()+la)%(n-1)+1,s=(read()+la)%w;
int son=0;
if(dis[e[id].x]<dis[e[id].y])
{
son=e[id].y;
}
else
{
son=e[id].x;
}
tr.update(1,L[son],R[son],s-e[id].s);
write(la=tr.node[1].ans);
putchar('\n');
e[id].s=s;
}
return 0;
}
C. 排列
这玩意儿不是一眼双指针吗?
显然一个排列中同一个数不可能同时出现两次,所以我们可以在枚举右端点的同时标记一个左端点 \(L\)。现在我们需要考虑的是这个区间里所有的排列。
显然我们并不需要考虑那么多,因为有些区间的右端点触及不到当前枚举的右端点,这些显然可以去掉,那留下来的区间是什么呢?显然只有这个区间的最大值比右端点的值大时这个区间才会触及到当前的右端点。
所以我们保存当前这个区间里所有可以触及到右端点的区间最大值,然后你会发现这个东西是一个单调递减的序列(因为如果后面某个数比前面大,那么前面的那个数的右端点必然触及不到当前的右端点),所以我们可以那单调队列来维护这个东西。
那那个左端点有什么用呢?显然如果你的头已经超出了当前合法区间的左端点,那么你的右端点是直接因为合法性被强制断开了,此时直接计算答案。
时间复杂度:\(O(n)\)。
代码:
const int N=1e6+6;
int n,l,r,ans,a[N],q[N],la[N];
void check(int l,int r,int mx)
{
if(mx>r)
{
return;
}
if(r-l+1==a[mx])
{
ans=max(ans,a[mx]);
}
}
signed main()
{
n=read();
read(a+1,a+n+1);
int L=0;
l=r=1;
for(int i=1;i<=n;i++)
{
L=max(L,la[a[i]]+1);
while(l<r&&q[l]<=la[a[i]])
{
check(q[l]+1,i-1,q[l+1]);
l++;
}
while(l<r&&a[q[r-1]]<=a[i])
{
check(max(q[r-2]+1,L),i-1,q[r-1]);
r--;
}
q[r++]=i;
check(L,i,q[l]);
la[a[i]]=i;
}
while(l+1<r)
{
check(q[l]+1,n,q[l+1]);
l++;
}
write(ans);
return 0;
}
D. 下 Niz
这个跟上面那道题显然是相同的原理。
稍微改一下就行了:
const int N=1e6+6;
int n,l,r,ans,a[N],q[N],la[N];
map<pair<int,int>,bool>mp;
void check(int l,int r,int mx)
{
if(mx>r)
{
return;
}
if(r-l+1==a[mx])
{
if(l<=r&&!mp[make_pair(l,r)])
{
mp[make_pair(l,r)]=1;
ans++;
}
}
}
signed main()
{
n=read();
read(a+1,a+n+1);
int L=0;
l=r=1;
for(int i=1;i<=n;i++)
{
L=max(L,la[a[i]]+1);
while(l<r&&q[l]<=la[a[i]])
{
check(q[l]+1,i-1,q[l+1]);
l++;
}
while(l<r&&a[q[r-1]]<=a[i])
{
check(max(q[r-2]+1,L),i-1,q[r-1]);
r--;
}
q[r++]=i;
check(L,i,q[l]);
la[a[i]]=i;
}
while(l+1<r)
{
check(q[l]+1,n,q[l+1]);
l++;
}
write(ans);
return 0;
}
E. 动态图连通性
转化题。
首先显然无法在线做,不然就真的成动态图连通性了。
我们假设第 \(i\) 条边被删除的时间是 \(d_i\)(如果没被删那就设为 \(\infty\)),那么题目实际上就是让我们找到一条从 \(1\) 到 \(n\) 的路径,使得:
-
路径上最小的 \(d\) 尽可能大。
-
在满足条件一的情况下,路径上第二小的 \(d\) 尽可能大。
-
在满足条件一、二的情况下,路径上第三小的 \(d\) 尽可能大。
依次类推。
其实就是把路径上的 \(d\) 从小到大排序,然后取字典序最大的那个,别的路都可以删。
现在的问题就是如何找到这条路。
在这之前我们先考虑一下别的情况:
如果同一条边被删除了多次,那么显然我们以第一次删除的时间为准,后面的删除操作的答案显然都是 \(0\),直接标记就行了。
回到找路径这里。因为每个 \(d\) 只会出现一次,所以我们可以考虑把所有的 \(d\) 刻画到某个东西上面然后用最短路(或最长路)做。
显然我可以把 \(d_i\) 视作 \(10^{d_i}\),此时我如果经过了这条边,那么我就对距离做 \(10^{d_i}\) 的贡献,但是这样如果我们要比较两条路径的长度,就需要从最低位开始比,因此我们可以反过来用 \(10^{q-d_i}\)。
此时上面的条件就被我们转化为最短路问题。我们考虑怎么做这个最短路问题。
显然你如果加一个 \(10^{q-d_i}\),就相当于把 \(q-d_i\) 这个位置从 \(0\) 变成 \(1\),因此我们可以用可持久化线段树来维护每一位,比较就用字典序比较就行。显然这是可以实现的。
但是可持久化线段树实现稍微有点复杂,所以我们来观察性质。
我们考虑两个点 \(st_1\) 和 \(st_2\),如果它们都能转移给点 \(ed\),那么会出现什么性质。
-
假设 \(dis_{st_1}<dis_{st_2}\),显然我们可以将条件反过来,结果是一样的。
-
那么根据 Dijkstra 的特性,因为 \(st_2\) 在 \(ed\) 之前被取出来,所以有 \(dis_{st_1}+10^{q-d_1}>dis_{st_2}\)。
-
又因为每个 \(d\) 只出现过一次,所以 \(dis_{st_1}\) 和 \(dis_{st_2}\) 在 \(q-d_1\) 这一位的数字都是 \(0\)。
-
所以我们可以知道 \(dis_{st_1}\) 和 \(dis_{st_2}\) 应该长这样:在 \(q-d_1\) 位及之前都一样且第 \(q-d_1\) 位为 \(0\),在 \(q-d_1\) 为后面的随意但是保证 \(dis_{st_2}>dis_{st_1}\)。
现在我们考虑 \(q-d_2\):如果 \(q-d_2\ge q-d_1\),此时 \(d_1\ge d_2\),那么显然 \(dis_{st_2}+10^{q-d_2}>dis_{st_1}+10^{q-d_1}\),否则 \(dis_{st_2}+10^{q-d_2}<dis_{st_1}+10^{q-d_1}\)。进一步观察我们会发现:这实际上就是取所有能转移的当中 \(d\) 最大的来转移。
因此我们直接将第一维改为 \(d\),并按照 \(d\) 从大到小排序。
代码:
const int N=2e5+6;
struct edge{
int x,y;
}e[N];
int n,m,q,d[N],la[N];
bool ans[N],vis[N];
vector<int>v[N];
signed main()
{
n=read(),m=read(),q=read();
for(int i=1,x,y;i<=m;i++)
{
x=read(),y=read();
e[i]=(edge){x,y};
v[x].push_back(i);
d[i]=q+1;
}
for(int i=1;i<=q;i++)
{
int x=read();
if(d[x]>q)
{
d[x]=i;
ans[i]=true;
}
else
{
ans[i]=false;
}
}
priority_queue<pair<int,int>>pq;
pq.push(make_pair(0,m+1));
e[m+1].y=1;
while(!pq.empty())
{
pair<int,int>p=pq.top();
pq.pop();
int x=e[p.second].y;
if(vis[x])
{
continue;
}
vis[x]=1;
la[x]=p.second;
for(auto i:v[x])
{
pq.push(make_pair(d[i],i));
}
}
for(int i=n;i;i=e[la[i]].x)
{
ans[d[la[i]]]=false;
}
for(int i=1;i<=q;i++)
{
write(ans[i]?1:0);
putchar('\n');
}
return 0;
}

浙公网安备 33010602011771号