日常の练习

水博客太快乐了

同步于洛谷博客。
并不知道为什么要同步,也许某一天就因为懒不同步了。。。

在教练的要求下开始有规划的刷一些题,感觉还是应该记录一下的。。。

图论

飞行路线

分层图最短路

貌似没啥可说的,就是裸的分层图最短路。。。
放下代码吧。。。。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
#define t first
#define w second
const int N=1e5+10, K=20;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m,k;
int s,t;
vector<pair<int, int> > l[N];
struct node{
	int u,k,dis;
	friend bool operator < (const node &x, const node &y) { return x.dis > y.dis; }
};
priority_queue<node> q;
bool vis[N][K];
int dis[N][K];
void dij(){
	memset(dis,0x3f,sizeof dis);
	q.push((node) { s, 0, dis[s][0]=0 });
	while(!q.empty()){
		node now=q.top(); q.pop();
		if(vis[now.u][now.k]) continue;
		vis[now.u][now.k]=1;
		for(pair<int,int> v : l[now.u]){
			if(dis[v.t][now.k+1]>dis[now.u][now.k]&&now.k<k){
				dis[v.t][now.k+1]=dis[now.u][now.k];
				if(!vis[v.t][now.k+1]) q.push((node){ v.t, now.k+1, dis[v.t][now.k+1] });
			}
			if(dis[v.t][now.k]>dis[now.u][now.k]+v.w){
				dis[v.t][now.k]=dis[now.u][now.k]+v.w;
				if(!vis[v.t][now.k]) q.push((node){ v.t, now.k, dis[v.t][now.k] });
			}
		}
	}
}
int main(void){
	n=read(), m=read(), k=read();
	s=read(), t=read();
	int x,y,z;
	for(int i=1;i<=m;i++){
		x=read(), y=read(), z=read();
		l[x].push_back(make_pair(y,z));
		l[y].push_back(make_pair(x,z));
	}
	dij();
	int ans=0x3f3f3f3f;
	for(int i=0;i<=k;i++) ans=min(ans,dis[t][i]);
	printf("%d\n",ans);
	return 0;
}

CodeForces 1163F Indecisive Taxi Fee

最短路+线段树维护最短路径

乍一看可做,实际完全不可做。
实不相瞒,第一次见这种思路。
果断看题解

要注意每次询问是独立的,不会对之后产生影响。。。
首先分类讨论很好想:
先从 \(1\) 号点, \(n\) 号点开始分别跑一遍最短路,每个点的距离记为 \(dis, dis1\)
设该次修改的是第 \(i\) 条边, \(( u, v, w)\) , 修改后边权为 \(x\)
若修改的边不位于最短路上,且边权变大,那么答案显然还是原最短路不变;
若修改的边不位于最短路上,且边权变小,那么答案即为 \(min( dis_{u}+dis1_{v}+w, dis_{v}+dis1{u}+w )\) (这里注意有两种情况) 与最短路的最小值;
若修改的边为位于最短路上,且边权变小,那么答案即为 \(dis_{n}-w+x\)

接下来就是最麻烦的情况。。。
若修改的边位于最短路上,且边权变大,此时答案可能仍是原最短路, 也可能完全与原最短路无关, 思考如何处理这种情况。
若我们已知一条严格最短路, 它可能是此时的答案吗? 不一定, 因为修改的边有可能在最短路与次短路的公共部分上。
我们可以预处理出从 \(1\) 号点到 \(n\) 号点的所有非严格最短路, 然而这些非严格最短路也与最短路有公共路径, 那么这些公共路径分别在哪里?
我们观察没条非严格次短路(实际上是 \(k\) 短路)是如何求出的, 对于每条边 \(( u, v, w)\)\(dis_{u}+dis1_{v}+w\) 即为一条非严格最短路。。。
显然 \(1\)\(u\) 的最短路径 \(1\)\(n\) 的最短路径会有一个交点 \(l\) , 而这之前的路径x显然都是重合的, 同理, \(n\)\(v\) 的最短路也会有一个交点 \(r\) , 这之前的路径也是重合的。 因此, 在最短路径上 \(l\)\(r\) 其中一条边被改变时,这条非严格次短路有可能成为答案。
\(l\)\(r\) 均可以在最短路中求得。
搞一棵可以区间修改, 单点查找的线段树。
对每条非严格次短路进行这样的处理, 然后将路径长度塞到线段树里, 每次询问的时候在线段树中查找即可。。。。

这么毒瘤的题码的时候居然没有看题解。。。
超多细节要注意。。。。

code
// By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+10, M=2e5+10, MAXN=0X3f3f3f3f3f3f3f3f;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m,Q,ans;
int head[N], cnt=1;
struct EDGE{
	int f, t, nxt;
	int w;
}l[M<<1];
int dis[N], dis1[N], pre[N];
int lft[N], rit[N], num[M];
bool vis[N], ch[M], ul[N];
struct TRE{
	int l,r;
	int minn;
}t[M<<2];
void add(int a,int b,int c){
	l[++cnt].nxt=head[a];
	l[cnt].f=a, l[cnt].t=b; l[cnt].w=c;
	head[a]=cnt;
}
void built(int l, int r, int p){
	t[p].l=l, t[p].r=r; t[p].minn=MAXN; 
	if(l==r) return;
	int mid=(l+r)>>1;
	built(l, mid, p<<1); built(mid+1, r, p<<1|1);
}
void add(int x, int l, int r, int p){
	if(l>r) return;
	if(l<=t[p].l&&r>=t[p].r) { t[p].minn=min(t[p].minn, x); return; }
	int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) add(x, l, r, p<<1);
	if(r>mid) add(x, l, r, p<<1|1);
}
int che(int x, int p){
	if(t[p].l==x&&t[p].r==x) return t[p].minn;
	int mid=(t[p].l+t[p].r)>>1;
	if(x<=mid) return min(t[p].minn, che(x, p<<1));
	else return min(t[p].minn, che(x, p<<1|1));
}
struct node{
	int id, dis;
	friend bool operator < (const node &x, const node &y) { return x.dis > y.dis; }
};
priority_queue<node> q;
void dij(int *chn, int *dis, int s){
	memset(dis,0x3f3f,sizeof(int)*(n+1));
	memset(vis,0,sizeof vis);
	dis[s]=0, q.push((node){s, 0});
	while(!q.empty()){
		int u=q.top().id; q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=head[u];i;i=l[i].nxt){
			int v=l[i].t;
			if(dis[v]>dis[u]+l[i].w){
				dis[v]=dis[u]+l[i].w; pre[v]=i;
				if(!ul[v]) chn[v]=chn[u];
				if(!vis[v]) q.push((node){v, dis[v]});
			}
		}
	}
}
int chain(){
	int len=0;
	for(int p=n; p; p=l[pre[p]^1].t, ++len) ch[pre[p]>>1]=1, ul[p]=1;
	for(int p=n, ul=len-1; p; p=l[pre[p]^1].t, --ul) lft[p]=ul, rit[p]=ul+1, num[pre[p]>>1]=ul;
	return len;
}
signed main(void){
	n=read(), m=read(), Q=read();
	int x, y, z, l1, r1;
	for(int i=1;i<=m;i++){
		x=read(), y=read(), z=read();
		add(x, y, z); add(y, x, z);
	}
	dij(lft, dis, 1); int len=chain();
	dij(rit, dis1, n); dij(lft, dis, 1);
	built(1, len, 1);
	for(int i=2;i<=cnt;i+=2){
		if(ch[i>>1]) continue;
		x=dis[l[i].f]+dis1[l[i].t]+l[i].w; y=dis[l[i].t]+dis1[l[i].f]+l[i].w;
		l1=lft[l[i].f], r1=rit[l[i].t];
		add(x, l1+1, r1-1, 1);
		l1=lft[l[i].t], r1=rit[l[i].f];
		add(y, l1+1, r1-1, 1);
	}
	while(Q--){
		x=read(), y=read();
		if(ch[x]){
			x<<=1;
			if(y<=l[x].w) ans=min(dis[l[x].f]+dis1[l[x].t]+y,dis[l[x].t]+dis1[l[x].f]+y);
			else ans=min(che(num[x>>1], 1), dis[n]-l[x].w+y);
		}else{
			x<<=1;
			if(y>=l[x].w) ans=dis[n];
			else ans=min(dis[n],min(dis[l[x].f]+dis1[l[x].t]+y,dis[l[x].t]+dis1[l[x].f]+y));
		}
		printf("%lld\n",ans);
	}
	return 0;
}

就这样吧,不管它了。

逛公园

最短路+记忆化搜索( \(DAGdp\) ) || 拓扑排序+ \(DAGdp\)

乍一看没什么思路,感觉完全不可做。
后来想到可以先在反图中跑一遍最短路,再记忆化搜索。
但是因为数据太大,如果直接将路径长度当做数组下表显然会直接爆炸,此时我们发现 \(k\) 小的可怜,因此要想办法把 \(k\) 搞到状态里。
\(f[u][i]\) 表示跑到编号为 \(u\) 的节点,且目前的路径比到达 \(u\) 节点的最短路多出 \(i\) 时,有多少种合法的到达 \(n\) 号节点的方法,然后直接转移即可。

此时我们发现出题人不讲武德,居然还有零环。。。
所以要判零环,而且并不是图中只要出现零环就有无数条路径,可能经过该零环没有符合长度小于 \(d+k\) 的路径。。。
所以在 \(dfs\) 时,若经过某点没有合法路径,直接返回0即可,入栈时将该点的编号与路径长度一同标记,若下次访问到该点时发现该店在此长度下已经被标记,则显然出现零环,返回\(-1\),出栈时取消标记。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
#define t first
#define w second
const int N=1e5+10, K=60;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int T;
int n,m,k,p;
vector<pair<int,int> > l[N], l1[N];
bool ul[N][K];
int f[N][K];
int dis[N], dis1[N];
bool vis[N];
struct node{
	int id, dis;
	friend bool operator < (const node &x, const node &y) { return x.dis > y.dis; }
};
priority_queue<node> q;
inline void init(){
	memset(vis,0,sizeof vis);
	memset(f,0,sizeof f);
	memset(ul,0,sizeof ul);
	for(int i=1;i<=n;i++) l[i].clear(), l1[i].clear();
}
void dij(){
	memset(dis, 0x3f, sizeof dis);
	dis[1]=0, q.push((node){1, 0});
	while(!q.empty()){
		int u=q.top().id; q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(pair<int, int> v : l[u]){
			if(dis[v.t]>dis[u]+v.w){
				dis[v.t]=dis[u]+v.w;
				if(!vis[v.t]) q.push((node){v.t, dis[v.t]});
			}
		}
	}
}
void dij1(){
	memset(vis,0,sizeof vis);
	memset(dis1,0x3f,sizeof dis1);
	dis1[n]=0, q.push((node){n, 0});
	while(!q.empty()){
		int u=q.top().id; q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(pair<int,int> v : l1[u]){
			if(dis1[v.t]>dis1[u]+v.w){
				dis1[v.t]=dis1[u]+v.w;
				if(!vis[v.t]) q.push((node){v.t, dis1[v.t]});
			}
		}
	}
}
int dfs(int u,int len){
	if(len+dis1[u]>dis[n]+k) return 0;
	if(len-dis[u]>k) return 0;
	if(ul[u][len-dis[u]]) return -1;
	if(f[u][len-dis[u]]) return f[u][len-dis[u]];
	ul[u][len-dis[u]]=1;
	if(u==n) f[u][len-dis[u]]++;
	for(pair<int,int> v : l[u]){
		int tmp=dfs(v.t,len+v.w);
		if(tmp==-1) return -1;
		f[u][len-dis[u]]+=tmp;
		f[u][len-dis[u]]%=p;
	}
	ul[u][len-dis[u]]=0;
	return f[u][len-dis[u]];
}
int main(void){
	T=read();
	while(T--){
		init();
		n=read(), m=read(), k=read(), p=read();
		int x,y,z;
		for(int i=1;i<=m;i++){
			x=read(), y=read(), z=read();
			l[x].push_back(make_pair(y,z));
			l1[y].push_back(make_pair(x,z));
		}
		dij(); dij1();
		printf("%d\n",dfs(1,0));
	}
	return 0;
}

(实际上只需要跑一次最短路,但本人一开始感觉正图反图都得跑一次,后来也没有改。。。导致代码冗长。。。)
(倒着跑 $ dfs $ 会更快, 但是写的时候并没有意识到。。。我还是太菜了。。。)

[NOI2018] 归程

最短路+ \(kruskal\) 重构树 || 最短路+可持久化并查集

第一思路显然是从 \(1\) 号节点跑最短路,对于每次询问都可以较快速得求出询问的点在当前深度下与那些点在同一个连通块内,即不需步行就可到达。

\(Sol1\)

首先思考离线做法,之后显然我们可以将询问按降水量大到小排序,再将海拔比当前询问大的边建立并查集,每个集合内即为不需步行即可走到的点,询的答案即为点所在集合内的点到 \(1\) 号点的最短距离。

然而询问强制在线,因此我们将并查集可持久化即可。

代码还没来得及写,写了补上。

\(Sol2\)

将边的海拔从大到小排序,跑 \(kruskal\) 重构树,得到一个小根堆,满足一下性质:对于每一个节点,它的所有儿子节点的点权都比它大,因此若无法淹没某一个节点,也必然无法淹没它之下的所有节点。
所以在每次询问时,只需找到该节点的一个祖先满足其海拔大于 \(p\) 而它的父亲海拔不大于 \(p\) , 用倍增求解即可。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
#define t first
#define w second
const int N=2e5+10, M=4e5+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m,Q,k,s,t;
int dis[N<<1], f[N<<1][20], val[N<<1], fa[N<<1], son[N<<1][2];
bool vis[N];
int find(int x) { return x==fa[x] ? x : fa[x]=find(fa[x]); }
struct edge{
	int f, t, w;
}l1[M];
bool cmp(const edge &x, const edge &y) { return x.w > y.w; }
vector<pair<int,int > > l[N];
struct node{
	int id, dis;
	friend bool operator < (const node &x, const node &y) { return x.dis > y.dis; }
};
priority_queue<node> q;
inline void init(){
	for(int i=1; i<=n; ++i) l[i].clear();
	for(int i=1; i<=(n<<1); ++i) fa[i]=i;
	memset(dis, 0x3f, sizeof(int)*(n+1));
	memset(vis, 0, sizeof(bool)*(n+1));
}
inline void dij(int s=1){
	dis[s]=0, q.push((node){s, 0});
	while(!q.empty()){
		int u=q.top().id; q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(pair<int,int > v : l[u]){
			if(dis[v.t]>dis[u]+v.w){
				dis[v.t]=dis[u]+v.w;
				if(!vis[v.t]) q.push((node){v.t, dis[v.t]});
			}
		}
	}
}
void dfs(int u){
	for(int i=1; i<=19; i++) f[u][i]=f[f[u][i-1]][i-1];
	if(u>n) dfs(son[u][1]), dfs(son[u][0]);
}
int kruskal(){
	int cnt=n;
	sort(l1+1, l1+1+m, cmp);
	for(int i=1; i<=m; i++){
		int f1=find(l1[i].f), f2=find(l1[i].t);
		if(f1!=f2){
			fa[f1]=fa[f2]=f[f1][0]=f[f2][0]=++cnt;
			val[cnt]=l1[i].w;
			son[cnt][0]=f1, son[cnt][1]=f2;
			dis[cnt]=min(dis[f1], dis[f2]);
		}
		if(cnt==(n<<1)-1) break;
	}
	return cnt;
}
int que(int u, int p){
	for(int i=19; ~i; i--) if(f[u][i]&&val[f[u][i]]>p) u=f[u][i];
	return dis[u];
}
int main(void){
	t=read();
	while(t--){
		n=read(), m=read();
		init();
		int x, y, z, a, ans=0;
		for(int i=1; i<=m; ++i){
			x=read(), y=read(), z=read(), a=read();
			l[x].push_back(make_pair(y,z));
			l[y].push_back(make_pair(x,z));
			l1[i]=(edge){x, y, a};
		}
		dij();
		dfs(kruskal());
		Q=read(), k=read(), s=read();
		int u, p;
		while(Q--){
			u=(read()+k*ans-1)%n+1; p=(read()+k*ans)%(s+1);
			printf("%d\n",ans=que(u, p));
		}
	}
	return 0;
}

peaks

\(kruskal\)重构树+主席树

智能推荐了这道题,乍一看这不是和归程一摸一样吗???
妙啊!!!

仔细一看发现并不完全一样,归程要求的是一个联通块内到 \(1\) 号点的最小值,而这个题要求的是一个联通块内第 \(k\) 大的值。
没有修改,也就是静态区间第 \(k\) 大,可以想到用主席树来维护欧拉序,求第 \(k\) 大。
当我们用 \(kruskal\) 重构树的到一棵树,观察显然可以发现,只有树中的叶子节点的高度是有用的,所以我们在初始化主席树的时候并不需要求出每个点的欧拉序(貌似好多题解都是这么写的),再按欧拉序把它塞到树里,只需将叶子节点编号塞到树中,再在每个节点上记录下它包含的叶子节点的区间(显然是连续的,因此可以用区间表示)。
之后对于每次询问同归程一样在树上倍增,再在主席树中查找即可。

貌似还可以用线段树合并做,但是本蒟蒻并不会。。。
切记 \(kruskal\) 重构树数组要开两倍。。。。

注意题中并没有保证图是联通的,然而 洛谷 上数据太水了,建议去 DarkBZOJ 上提交,还有 强制在线版 ,双倍经验,妙啊。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10, M=5e5+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m,q;
struct EDGE{
	int f, t, v;
}l[M];
bool cmp(const EDGE &x, const EDGE &y) { return x.v < y.v; }
int rt[N], top, ind;
int fa[N<<1][20], f[N<<1], son[N<<1][2], lft[N<<1], rit[N<<1], a[N], val[N<<1], h[N];
int find(int x) { return x==f[x] ? x : f[x]=find(f[x]); }
struct TRE{
	int ls, rs;
	int num;
}t[6000010];
void built(int l, int r, int &p){
	p=++top;
	if(l==r) return;
	int mid=(l+r)>>1;
	built(l, mid, t[p].ls);
	built(mid+1, r, t[p].rs);
}
void add(int lst, int &p, int x, int a, int b){
	p=++top; t[p]=t[lst]; t[p].num++;
	if(a==b) return;
	int mid=(a+b)>>1;
	if(x<=mid) add(t[lst].ls, t[p].ls, x, a, mid);
	else add(t[lst].rs, t[p].rs, x, mid+1, b);
}
int query(int l, int r, int k, int a, int b){
	if(a==b) return a;
	int mid=(a+b)>>1;
	if(k>t[r].num-t[l].num) return 0;
	else if(k<=t[t[r].rs].num-t[t[l].rs].num) return query(t[l].rs, t[r].rs, k, mid+1, b);
	else return query(t[l].ls, t[r].ls, k-t[t[r].rs].num+t[t[l].rs].num, a, mid);
}
int rmq(int u, int p, int k){
	for(int i=19; ~i; --i) if(fa[u][i]&&val[fa[u][i]]<=p) u=fa[u][i];
	return query(rt[lft[u]-1], rt[rit[u]], k, 1, n);
}
void dfs(int u){
	for(int i=1; i<=19; ++i) fa[u][i]=fa[fa[u][i-1]][i-1];
	if(!son[u][1]){
		add(rt[ind], rt[ind+1], h[u], 1, n);
		lft[u]=rit[u]=++ind; return;
	}
	dfs(son[u][0]), dfs(son[u][1]);
	lft[u]=lft[son[u][0]], rit[u]=rit[son[u][1]];
}
void kruskal(){
	int cnt=n, f1, f2;
	sort(l+1, l+1+m, cmp);
	for(int i=1; i<=(n<<1); i++) f[i]=i;
	for(int i=1; i<=m; ++i){
		f1=find(l[i].t), f2=find(l[i].f);
		if(f1==f2) continue;
		son[++cnt][1]=f1, son[cnt][0]=f2;
		f[f1]=f[f2]=fa[f1][0]=fa[f2][0]=cnt; val[cnt]=l[i].v;
		if(cnt==(n<<1)-1) break;
	}
	for(int i=cnt; i>n; --i) if(f[i]==i) dfs(i);
}
int main(void){
	n=read(), m=read(), q=read();
	for(int i=1; i<=n; ++i) a[i]=h[i]=read();
	sort(a+1, a+1+n);
	int len=unique(a+1, a+1+n)-a-1;
	for(int i=1; i<=n; ++i) h[i]=lower_bound(a+1, a+1+len, h[i])-a;
	built(1, len, rt[0]);
	int u, v, x, k;
	for(int i=1;i<=m;i++){
		u=read(), v=read(), x=read();
		l[i]=(EDGE){u, v, x};
	}
	kruskal();
	a[0]=-1;
	while(q--){
		u=read(), x=read(), k=read();
		printf("%d\n",a[rmq(u, x, k)]);
	}
	return 0;
}

[AHOI2005] 航线规划

树剖线段树(可以用 \(tarjan\)

刚开始没思路,后来在巨佬指导下去看了 这道题 ,发现可以和本题使用相同的套路。。。
实际上这道题就是套路题。。。

可以先将询问离线,然后倒序处理,这样摧毁就变成了修建,显然在这种情境下修建比摧毁是好处理的。
首先先将操作中所有摧毁的边在原图中删去,这样我们就得到一张末状态的图,此时我的想法是跑 \(tarjan\) 缩点,这样我们便得到一棵树,然后使用树剖+线段树维护的套路即可。。。
但是巨佬 \(YCX\) 显然有更好的方法,若我们在原图中随便生成一棵树,会用掉 \(n-1\) 条边,那么剩下所有的边只要加入到图中显然就会形成一个环,还是用树剖线段树处理即可。。。

记得要边权转点权。。。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
const int N=3e4+10, Q=4e4+10, M=2e5+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m,q;
int head[N], cnt=1;
struct EDGE{
	int f, t, nxt;
}l[M];
void add(int a,int b){
	l[++cnt].nxt=head[a];
	l[cnt].t=b, l[cnt].f=a;
	head[a]=cnt;
}
bitset<N> g[N];
struct QUE{
	int op;
	int u,v;
	int ans;
}que[Q];
int dep[N], siz[N], son[N], top[N], num[N], fa[N], ind;
void dfs1(int u){
	siz[u]=1;
	for(int i=head[u]; i; i=l[i].nxt){
		int v=l[i].t;
		if(g[u][v]||siz[v]) continue;
		dep[v]=dep[u]+1; fa[v]=u;
		dfs1(v); siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u, int tp){
	num[u]=++ind;
	top[u]=tp;
	if(son[u]) dfs2(son[u], tp);
	for(int i=head[u]; i; i=l[i].nxt){
		int v=l[i].t;
		if(v==son[u]||fa[v]!=u) continue;
		dfs2(v, v);
	}
}
struct TRE{
	int l, r;
	int v;
	bool lz;
}t[N<<2];
void built(int l, int r, int p){
	t[p].l=l, t[p].r=r, t[p].v=r-l+1;
	if(l==r) return;
	int mid=(l+r)>>1;
	built(l, mid, p<<1);
	built(mid+1, r, p<<1|1);
}
void push(int p){
	if(!t[p<<1].l||!t[p].lz) return;
	t[p<<1].v=t[p<<1|1].v=0;
	t[p<<1].lz=t[p<<1|1].lz=1;
	t[p].lz=0;
}
void change(int l, int r, int p){
	if(l<=t[p].l&&r>=t[p].r) { t[p].v=0, t[p].lz=1; return; }
	push(p);
	int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) change(l, r, p<<1);
	if(r>mid) change(l, r, p<<1|1);
	t[p].v=t[p<<1].v+t[p<<1|1].v;
}
int cha(int l, int r, int p){
	if(l<=t[p].l&&r>=t[p].r) return t[p].v;
	push(p);
	if(!t[p].v) return 0;
	int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid&&r>mid) return cha(l, r, p<<1)+cha(l, r, p<<1|1);
	if(l<=mid) return cha(l, r, p<<1);
	if(r>mid) return cha(l, r, p<<1|1);
}
int query(int x, int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		ans+=cha(num[top[x]], num[x], 1);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	if(dep[x]!=dep[y]) ans+=cha(num[y]+1, num[x], 1);
	return ans;
}
void cover(int x, int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		change(num[top[x]], num[x], 1);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	if(dep[x]!=dep[y]) change(num[y]+1, num[x], 1);
}
int main(void){
	n=read(), m=read();
	int x,y,op;
	built(1,n,1);
	for(int i=1;i<=m;i++){
		x=read(), y=read();
		add(x,y), add(y,x);
	}
	op=read();
	while(op!=-1){
		x=read(), y=read();
		que[++q]=(QUE){op, x, y};
		if(!op) g[x][y]=g[y][x]=1;
		op=read();
	}
	dep[1]=1;
	dfs1(1); dfs2(1,1);
	for(int i=2;i<=cnt;i+=2){
		if(g[l[i].f][l[i].t]||fa[l[i].f]==l[i].t||fa[l[i].t]==l[i].f) continue;
		else cover(l[i].f, l[i].t);
	}
	for(int i=q; i; --i){
		if(que[i].op) que[i].ans=query(que[i].u, que[i].v);
		else cover(que[i].u, que[i].v);
	}
	for(int i=1; i<=q; i++) if(que[i].op) printf("%d\n",que[i].ans);
	return 0;
}

大概就这样。。。

[NOIP2015 提高组] 运输计划

二分+LCA+树上差分

乍一看还是没什么思路,想了树剖、树上差分等一系列算法。。。
就是没想二分答案。。。

看到题中有最大值最小,首先就应该想到要二分答案。。。。
果然做题还是少,居然没有注意到。。。
意识还是没跟上啊。。。应该多刷刷二分。。。

接下来想 \(check\) 函数怎么写。。。
显然我们可以注意到,删去的边肯定是在最长路径上的,若最长路径与次长路径有公共边,才有可能在次长路径上。。。
所以将所有路径长度大于 \(mid\) 的路径在树上用差分标识出来,看这些路径有没有公共边,若删掉公共边后最长的路径长度能否小于 \(mid\)

没想到这题代码异常的好写。。。。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
#define t first
#define w second
const int N=3e5+10, M=3e5+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x; 
}
int n, m;
vector<pair<int, int > > l[N];
int fa[N], top[N], dep[N], siz[N], son[N], num[N], dis[N];
struct TRA{
	int u, v;
	int dis;
	friend bool operator < (const TRA &x, const TRA &y) { return x.dis > y.dis; }
}q[M];
void dfs1(int u, int f){
	dep[u]=dep[f]+1; fa[u]=f;
	siz[u]=1;
	for(pair<int, int > v : l[u]){
		if(v.t==f) continue;
		dis[v.t]=dis[u]+v.w;
		dfs1(v.t, u); siz[u]+=siz[v.t];
		if(siz[v.t]>siz[son[u]]) son[u]=v.t;
	}
}
void dfs2(int u, int tp){
	top[u]=tp;
	if(son[u]) dfs2(son[u], tp);
	for(pair<int, int > v : l[u]){
		if(v.t==fa[u]||v.t==son[u]) continue;
		dfs2(v.t, v.t);
	}
}
int lca(int u, int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(v, u);
		u=fa[top[u]];
	}
	return dep[u]<dep[v] ? u : v;
}
void dfs3(int u){
	for(pair<int, int > v : l[u]){
		if(v.t==fa[u]) continue;
		dfs3(v.t);
		num[u]+=num[v.t];
	}
}
void diff(int i){
	int u=q[i].u, v=q[i].v, Lca=lca(u, v);
	num[u]++, num[v]++;
	num[Lca]--, num[fa[Lca]]--;
}
bool check(int x){
	int cnt=0;
	memset(num, 0, sizeof num);
	for(int i=1; i<=m&&q[i].dis>x; ++i) diff(i), cnt++;
	dfs3(1);
	for(int i=1; i<=n; ++i){
		if(num[i]!=cnt||num[fa[i]]!=cnt) continue;
		if(q[1].dis-dis[i]+dis[fa[i]]<=x) return 1;
	}
	return 0;
}
int solve(){
	sort(q+1, q+1+m);
	int l=0, r=q[1].dis;
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	return l;
}
int main(void){
	n=read(), m=read();
	int x, y, z;
	for(int i=1; i<n; ++i){
		x=read(), y=read(), z=read();
		l[x].push_back(make_pair(y, z));
		l[y].push_back(make_pair(x, z));
	}
	dfs1(1, 0), dfs2(1, 1);
	for(int i=1; i<=m; ++i){
		x=read(), y=read();
		q[i]=(TRA) {x, y, dis[x]+dis[y]-(dis[lca(x, y)]<<1)};
	}
	printf("%d\n",solve());
	return 0;
}

一些数据结构

遥远的国度

树剖+线段树

乍一看像是一道简单的树剖线段树套路题,但是本题有一个不是很常见的操作,换根。
若每换一次根重新树剖一遍显然会惨 \(T\) ,所以要尝试找规律。。
随便话一棵树,会发现对于每一个点来说,若删除这个点,会将原树分成若干个小树,而根在哪棵小树中,则该节点的的子树为除去该树的其他所有树。

切记要特判,若当前根就是询问的节点,则所有点都是它的子树。。。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10, MAXN=2147483647;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n, m, rt;
vector<int> l[N];
int val[N], siz[N], top[N], fa[N], dep[N], son[N], num[N], num1[N], ind;
struct TRE{
	int l, r, val;
	bool lz;
}t[N<<2];
void built(int l, int r, int p){
	t[p].l=l, t[p].r=r;
	if(l==r) { t[p].val=val[num1[l]]; return; }
	int mid=(l+r)>>1;
	built(l, mid, p<<1); built(mid+1, r, p<<1|1);
	t[p].val=min(t[p<<1].val, t[p<<1|1].val);
}
void pushdown(int p){
	if(!t[p].lz) return;
	t[p<<1].lz=t[p<<1|1].lz=1;
	t[p<<1].val=t[p<<1|1].val=t[p].val;
	t[p].lz=0;
}
void cover(int l, int r, int p, int x){
	if(l<=t[p].l&&r>=t[p].r) { t[p].val=x; t[p].lz=1; return; }
	pushdown(p); int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) cover(l, r, p<<1, x);
	if(r>mid) cover(l, r, p<<1|1, x);
	t[p].val=min(t[p<<1].val, t[p<<1|1].val);
}
int que(int l, int r, int p){
	if(l>r) return MAXN;
	if(l<=t[p].l&&r>=t[p].r) return t[p].val;
	pushdown(p); int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid&&r>mid) return min(que(l, r, p<<1), que(l, r, p<<1|1));
	if(l<=mid) return que(l, r, p<<1);
	else return que(l, r, p<<1|1);
}
void dfs1(int u, int f){
	fa[u]=f, dep[u]=dep[f]+1, siz[u]=1;
	for(int v : l[u]){
		if(v==f) continue;
		dfs1(v, u); siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u, int tp){
	top[u]=tp, num[u]=++ind, num1[ind]=u;
	if(son[u]) dfs2(son[u], tp);
	for(int v : l[u]) if(v!=fa[u]&&v!=son[u]) dfs2(v, v);
}
int lca(int u, int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) u^=v, v^=u, u^=v;
		u=fa[top[u]];
	}
	return dep[u]<dep[v] ? u : v;
}
void change(int u, int v, int x){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) u^=v, v^=u, u^=v;
		cover(num[top[u]], num[u], 1, x); u=fa[top[u]];
	}
	if(dep[u]<dep[v]) u^=v, v^=u, u^=v;
	cover(num[v], num[u], 1, x);
}
int query(int x){
	int Lca=lca(rt ,x);
	if(rt==x) return t[1].val;
	else if(Lca!=x) return que(num[x], num[x]+siz[x]-1, 1);
	int ul=rt, ul1; while(top[ul]!=top[x]) ul1=ul, ul=fa[top[ul]];
	if(ul==x) ul=ul1;
	for(int v : l[x]) if(top[v]==top[ul]) ul=v;
	return min(que(1, num[ul]-1, 1), que(num[ul]+siz[ul], n, 1));
}
int main(void){
	n=read(), m=read();
	int op, x, y, z;
	for(int i=1; i<n; ++i){
		x=read(), y=read();
		l[x].push_back(y);
		l[y].push_back(x);
	}
	dfs1(1, 0); dfs2(1, 1);
	for(int i=1; i<=n; ++i) val[i]=read();
	built(1, n, 1);
	rt=read();
	while(m--){
		op=read();
		switch(op){
			case 1: rt=read(); break;
			case 2:
				x=read(), y=read(), z=read();
				change(x, y, z); break;
			default : printf("%d\n", query(read()));
		}
	}
	return 0;
}

[NOI2016] 区间

线段树

一道比较有思维难度的题。。。

乍一看感觉可以分治,发现不可行。。。

看数据范围显然要离散化。思考用一些数据结构。。。
看题目要求,显然应该将所有区间按长度排序,这样我们寻找的最小答案所选的区间应该离得越近越好,但是如果直接选相距 \(m\) 的两个区间的话,并不能这其中间的 \(m\) 个区间都覆盖了同一个点。那怎么判断呢?考虑用线段树维护,区间加以及区间最大值,按顺序将区间加入树中,每个被覆盖的点加一,若所有点最大值已经大于 \(m\) ,则代表出现可行的情况,再按顺寻将区间删除(从最早进来的开始删),直到最大值小于 \(m\) ,那么刚刚删去的区间与最新加入的区间可以构成一个可行答案。。。
显然在这种做法下,一些可能的情况会被忽略,但是被忽略的情况显然不是最优的,不用管。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10, MAXN=0x7fffffff;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int a[N<<1], n, m;
int ans=MAXN;
struct QUE{
	int l, r;
	int len;
	friend bool operator < (const QUE &x, const QUE &y) { return x.len < y.len; }
}q[N];
struct TRE{
	int l, r, num, lz;
}t[N<<4];
void built(int l, int r, int p){
	t[p].l=l, t[p].r=r;
	if(l==r) return;
	int mid=(l+r)>>1;
	built(l, mid, p<<1);
	built(mid+1, r, p<<1|1);
}
void pushdown(int p){
	if(!t[p].lz) return;
	t[p<<1].num+=t[p].lz, t[p<<1|1].num+=t[p].lz;
	t[p<<1].lz+=t[p].lz, t[p<<1|1].lz+=t[p].lz;
	t[p].lz=0;
}
void add(int l, int r, int p, int x){
	if(l<=t[p].l&&r>=t[p].r){
		t[p].lz+=x, t[p].num+=x;
		return;
	}
	pushdown(p);
	int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) add(l, r, p<<1, x);
	if(r>mid) add(l, r, p<<1|1, x);
	t[p].num=max(t[p<<1].num, t[p<<1|1].num);
}
int main(void){
	n=read(); m=read();
	for(int i=1; i<=n; ++i)
		a[i]=q[i].l=read()+1, a[i+n]=q[i].r=read()+1, q[i].len=q[i].r-q[i].l;
	sort(a+1, a+1+n+n); int len=unique(a+1, a+1+n+n)-a-1;
	for(int i=1; i<=n; ++i)
		q[i].l=lower_bound(a+1, a+1+len, q[i].l)-a, q[i].r=lower_bound(a+1, a+1+len, q[i].r)-a;
	built(1, len, 1); sort(q+1, q+1+n);
	for(int i=1, l=0; i<=n; ++i){
		add(q[i].l, q[i].r, 1, 1);
		while(t[1].num>=m) { ++l; add(q[l].l, q[l].r, 1, -1); }
		if(t[1].num==m-1&&l) ans=min(ans, q[i].len-q[l].len);
	}
	if(ans==MAXN) printf("-1\n");
	else printf("%d\n", ans);
	return 0;
}

状压

软件补丁问题

状压+最短路

很有趣的一道题,不知道为啥被放到网络流 \(24\) 题里,貌似和网络流没啥关系。。。。
一开始被网络流误导了,想了半天也没建出图来。。。。
后来看了标签才知道是状压。
不能 \(dp\) 。。。
将每个状态作为节点,每个补丁所用的时间为边权,跑最短路。
如果把图建出来会 \(MLE\) ,所以不建图直接跑最短路即可。

code
//By Cyber_Tree
#include<bits/stdc++.h>
using namespace std;
const int N=30, M=110;
#define int unsigned int
#define t first
#define w second
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int n,m;
int b1[M], b2[M], f1[M], f2[M], t[M];
char ul[N];
bool vis[1<<20];
int dis[1<<20];
struct node{
	int id, dis;
	friend bool operator < (const node &x, const node &y) { return x.dis > y.dis; }
};
priority_queue<node> q;
void dij(){
	memset(dis,0x3f,sizeof dis);
	q.push((node){(1<<n)-1, dis[(1<<n)-1]=0});
	while(!q.empty()){
		int u=q.top().id; q.pop();
		if(vis[u]) continue; vis[u]=1;
		for(int j=1;j<=m;j++){
			if((u&b1[j])!=b1[j]||(u&b2[j])!=0) continue;
			int ul=(~((~u)|f1[j]))|f2[j];
			if(dis[ul]>dis[u]+t[j]){
				dis[ul]=dis[u]+t[j];
				if(!vis[ul]) q.push((node){ ul, dis[ul] });
			}
		}
	}
}
signed main(void){
	n=read(), m=read();
	for(int i=1;i<=m;i++){
		t[i]=read();
		scanf("%s",ul);
		for(int j=0;j<n;j++){
			if(ul[j]=='+') b1[i]|=(1<<j);
			else if(ul[j]=='-') b2[i]|=(1<<j);
		}
		scanf("%s",ul);
		for(int j=0;j<n;j++){
			if(ul[j]=='+') f2[i]|=(1<<j);
			else if(ul[j]=='-') f1[i]|=(1<<j);
		}
	}
	dij();
	if(dis[0]!=0x3f3f3f3f) printf("%d\n",dis[0]);
	else printf("0\n");
	return 0;
}

[HEOI2016/TJOI2016]排序

二分答案+线段树

这题思路着实奇葩。。。
谁能想到居然要用二分答案。。。

首先显然,给一个普通的数列排序相比给一个 \(01\) 序列排序会慢很多,给01序列排序只需要查询 \(1\) 的数量再赋值即可。。。显然可以用线段树辅助。
如我们能将原序列的排序问题转化成 \(01\) 序列,就可以很轻松水过这道题。

所以我们二分答案,二分出一个 \(mid\) ,将所有大于等于 \(mid\) 的数变成 \(1\) ,小于 \(mid\) 的数变成 \(0\) ,然后处理所有操作,再查看位置 \(q\) 上是否是 \(1\)
如何证明单调性?
假设当前的 \(mid\) 小于结束时位置 \(q\) 上的数,即 \(q\) 上的数大于 \(mid\) ,那么这个位置上显然是 \(1\) 反之则为 \(0\)

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
inline int read(){
	int f=1, x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
	return f*x;
}
int a[N], n, m, Q;
struct QUE{
	int op, l, r;
}q[N];
struct TRE{
	int l, r;
	int num;
	bool lz;
}t[N<<2];
void built(int l, int r, int p, int x){
	t[p].l=l, t[p].r=r, t[p].lz=0;
	if(l==r) { t[p].num=a[l]>=x; return; }
	int mid=(l+r)>>1;
	built(l, mid, p<<1, x); built(mid+1, r, p<<1|1, x);
	t[p].num=t[p<<1].num+t[p<<1|1].num;
}
void pushdown(int p){
	if(!t[p].lz) return;
	t[p<<1].lz=t[p<<1|1].lz=1;
	t[p<<1].num=(t[p<<1].r-t[p<<1].l+1)*(t[p].num!=0);
	t[p<<1|1].num=(t[p<<1|1].r-t[p<<1|1].l+1)*(t[p].num!=0);
	t[p].lz=0;
}
int query(int l, int r, int p){
	if(l<=t[p].l&&r>=t[p].r) return t[p].num;
	pushdown(p); int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid&&r>mid) return query(l, r, p<<1)+query(l, r, p<<1|1);
	if(l<=mid) return query(l, r, p<<1);
	else return query(l, r, p<<1|1);
}
void cover(int l, int r, int p, int x){
	if(l>r) return;
	if(l<=t[p].l&&r>=t[p].r) { t[p].num=(t[p].r-t[p].l+1)*x; t[p].lz=1; return; }
	pushdown(p); int mid=(t[p].l+t[p].r)>>1;
	if(l<=mid) cover(l, r, p<<1, x);
	if(r>mid) cover(l, r, p<<1|1, x);
	t[p].num=t[p<<1].num+t[p<<1|1].num;
}
bool check(int x){
	built(1, n, 1, x); int cnt;
	for(int i=1; i<=m; ++i){
		cnt=query(q[i].l, q[i].r, 1);
		if(q[i].op) cover(q[i].l, q[i].l+cnt-1, 1, 1), cover(q[i].l+cnt, q[i].r, 1, 0);
		else cover(q[i].r-cnt+1, q[i].r, 1, 1), cover(q[i].l, q[i].r-cnt, 1, 0);
	}
	return query(Q, Q, 1);
}
int solve(){
	int l=1, r=n, mid;
	while(l<r){
		mid=(l+r+1)>>1;
		if(check(mid)) l=mid;
		else r=mid-1;
	}
	return l;
}
int main(void){
	n=read(), m=read();
	for(int i=1; i<=n; ++i) a[i]=read();
	for(int i=1; i<=m; ++i) q[i]=(QUE){read(), read(), read()};
	Q=read(); printf("%d\n",solve());
	return 0;
}

肉眼可见的,收集在这里的是神题。
那些思路清奇,非人类能想出的题。

我们充分发扬人类智慧

[国家集训队]阿狸和桃子的游戏

这题思路极其清奇,代码极其简单,仅仅因其思路而成为紫题。。。。。

讲边权除二,平分给它连接的两个点,排序即可。。。

若连接一条边所连接的两个点都被同一个人选了,那么显然边权也被算进去了。而如果没被同一个人选,因为我们所要求的答案是一个差值,所以相当于对答案没有贡献。。。

代码极短,不能让我丑陋的代码亵渎神题。

posted @ 2021-06-01 14:45  Cyber_Tree  阅读(119)  评论(1编辑  收藏  举报