练习曲

这是一个做题记录。

洛谷P1725 琪露诺

2023.8.5

题目链接

标签:动态规划、单调队列。

一道动态规划题,先考虑暴力一点的做法:

\(dp[i]\) 表示跳到第 \(i\) 个位置时所能获得的最大冰冻指数。那么 \(i\) 位置的状态可以从区间 \([i-L,i-R]\) 转移过来。

转移方程:$dp[i] = max(dp[j]) + a[i] $ ,其中 \(j\in [i-L,i-R]\)

时间复杂度为 \(O(n^2)\)

暴力DP在本题的预计得分为60分,然而在开了氧气优化的情况下能直接过掉本题。

考虑一个时间复杂度更优的做法:

每次转移需要求一个区间的最大值,而每个区间都是从上一个区间右移一个单位得到的,因此可以使用单调队列优化。

题目存在部分细节需要注意:

\(a[i]\) 可能为负数。当 \(i+r>=n\) 时才能跳到对岸,答案应在 \([n-r,n]\) 范围里选取。

暴力代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,dp[10211021],vis[10231311],L,R,ans=-0x7ffffff,w=0;
signed main(){
    cin>>n>>L>>R;
    memset(dp,-0x3f,sizeof(dp));
	for (int i=0;i<=n;++i) {
    	cin>>vis[i];
	}
	dp[0]=0;
	for (int i=L;i<=n+R-1;++i) {
		for (int j=max(w,i-R);j<=i-L;++j) {
			dp[i]=max(dp[i],dp[j]+vis[i]);
		} 
		//cout<<dp[i]<<endl;
		if (i>=n) ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

单调队列优化代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+2023;
int n,m,l,r,a[N],dp[N],tail,q[N],p[N],ans=-1e9;
signed main(){
	cin>>n>>l>>r;
	for (int i=0;i<=n;++i) {
		cin>>a[i];
	}
	memset(dp,~0x3f,sizeof(dp));
	int head=tail=0,len=r-l+1;
	dp[0]=0;
	for (int i=0;i<=n-l;++i) {
		while(head<=tail&&q[tail]<dp[i]) --tail;
	    q[++tail]=dp[i]; p[tail]=i;
	    while(head<=tail && p[head]<i-len+1) ++head;
	    dp[i+l]=q[head]+a[i+l];
	    if (i+r>=n) ans=max(ans,dp[i+l]);
		//ans=max(ans,dp[i+l]);
	}
	cout<<ans;
	return 0;  
}

CF383C Propagating tree

2023.8.5

题目链接

标签:树链剖分

题目需要我们对一个节点加上一个值,然后修改这个点的子树内各个点的值,从这个点到根节点每层 \(+val\)\(-val\) 交替进行。以及查询某点的子树权值和。

需要支持操作:子树修改,查询子树权值和。

子树修改和查询子树权值和不难想到树剖,但怎么在线段树上修改区间就成了问题。

不难发现一个节点的子树内每一层改变权值的符号相同(同一层一定都是加上一个数或者同一层都时减去一个数)。因此我们可以通过每个节点的深度的奇偶性判断这个数是加上这个值还是减去这个值。

即在 \(x\) 子树内,深度与\(x\)深度的奇偶性相同的点加上 \(val\),奇偶性不同的点减去 \(val\)

然而这样懒标记不好合并,如果选择舍弃懒标记直接单点修改的话,时间复杂度就会退化成 \(O(n^2 logn)\)

一个比较神奇的思路是:

修改时只考虑子树根节点深度的奇偶性,子树根节点深度为奇数就对整颗子树 \(+val\),子树根节点深度为偶数时就对整颗子树 \(-val\)

查询时根据每个节点的奇偶性,加上或减去对应的值。(可以放在 push_down 里实现)。

这个题目不需要查询区间,因此树状数组更优秀一些,不过我还是更喜欢写线段树。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
	char ch=getchar(); int x=0,f=0;
	for (;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for (;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if (x<0) { putchar('-'); x=-x; }
    if (x>9) print(x/10);
	putchar(x%10+48);
}
const int N=2e5+2023;
int n,m,a[N],head[N<<1],cnt;
int tot,dfn[N],pre[N],top[N],fa[N],son[N],dep[N],size[N];
struct node{
	int next,to,w; 
}e[N<<1];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1
	struct node{
		int sum,len,lazy,dep;
	}tree[N<<2];
	void build(int pos,int l,int r) {
		tree[pos].len=r-l+1;
		if (l==r) {
			tree[pos].sum=a[pre[l]];
			tree[pos].dep=dep[pre[l]];
			return ;
		}
		int mid=l+r>>1;
		build(lson,l,mid); build(rson,mid+1,r);
	}
    void push_down(int pos){
    	if (!tree[pos].lazy) return ;
    	if (tree[lson].dep!=0) {
    		if (tree[lson].dep%2==1) tree[lson].sum+=tree[pos].lazy;
    		else tree[lson].sum-=tree[pos].lazy;
		}
		if (tree[rson].dep!=0) {
			if (tree[rson].dep%2==1) tree[rson].sum+=tree[pos].lazy;
		    else tree[rson].sum-=tree[pos].lazy;
		}
	    tree[lson].lazy+=tree[pos].lazy;
	    tree[rson].lazy+=tree[pos].lazy;
	    tree[pos].lazy=0;
	}
	void change(int pos,int l,int r,int L,int R,int k){
		if (l>=L && r<=R) {
			tree[pos].lazy+=k;
			if (tree[pos].dep!=0) {
				if (tree[pos].dep%2) tree[pos].sum+=k;
				else tree[pos].sum-=k;
			}
			return ;
		}
	    int mid=l+r>>1; push_down(pos);
	    if(L<=mid) change(lson,l,mid,L,R,k);
		if(R>mid) change(rson,mid+1,r,L,R,k); 
	}
	int query(int pos,int l,int r,int k) {
		if (l==r) return tree[pos].sum;
		int mid=l+r>>1; push_down(pos);
		if (k<=mid) return query(lson,l,mid,k);
		else return query(rson,mid+1,r,k);
	}
} 
namespace sp{
	void dfs1(int now,int Fa) {
		size[now]=1; fa[now]=Fa; dep[now]=dep[Fa]+1;
		for (int i=head[now];i;i=e[i].next)  {
	        if (e[i].to==Fa) continue;
			dfs1(e[i].to,now);
			size[now]+=size[e[i].to];
			if (size[son[now]]<size[e[i].to]) son[now]=e[i].to; 
		} 
	}
	void dfs2(int now,int Top) {
	    dfn[now]=++tot; pre[tot]=now; top[now]=Top;
	    if(son[now]) dfs2(son[now],Top);
	    for (int i=head[now];i;i=e[i].next) {
	    	if (e[i].to==son[now]||e[i].to==fa[now]) continue;
	    	dfs2(e[i].to,e[i].to);
		}
	}
}
signed main(){
    n=read(); m=read();
    for (int i=1;i<=n;++i) {
    	a[i]=read();
	}
	for (int i=1;i<n;++i) {
		int x=read(),y=read();
		add(x,y); add(y,x);
	}
    sp::dfs1(1,0); sp::dfs2(1,1); ss::build(1,1,n);
    int op,x,y;
	for (int i=1;i<=m;++i) {
    	op=read();
    	if (op==1) {
    		x=read(),y=read();
    		if (dep[x]%2) ss::change(1,1,n,dfn[x],dfn[x]+size[x]-1,y); 
		    else ss::change(1,1,n,dfn[x],dfn[x]+size[x]-1,-y);
 	        //cout<<"  "<<ss::tree[1].sum<<endl;
	 	}
 		if (op==2) {
 			x=read();
 			print(ss::query(1,1,n,dfn[x]));
 			putchar('\n');
		 }
	} 
	return 0;
} 

洛谷P2344 [USACO11FEB] Generic Cow Protests G

2023.8.5

题目链接

标签:动态规划,前缀和,树状数组。离散化。

动态规划题,划分区间,需要求方案数,考虑设计状态:

\(dp[i]\) 表示前 \(i\) 头奶牛可以划分的方案数。从前 \(i-1\) 个位置中枚举点 \(j\) 作为区间端点,\([1,j-1]\) 里是已经被划分好的若干个合法区间,转移时只需考虑 \([j,i]\) 区间是否合法,因此需要满足 \([j,i]\) 区间和大于等于0。

处理区间和可以使用前缀和解决,设 \(sum[i]\) 表示前 \(i\) 个位置的前缀和。那么转移时需要满足 \(sum[i]-sum[j]>=0\)

初始化,dp[0]=1。

转移方程:

\[dp[i] = \sum_{j=0}^{i-1}dp[j] (sum[i]>=sum[j]) \]

时间复杂度为:\(O(n^2)\),面对1e5的数据还是慢了点,开O2也会TLE一个点。考虑优化。

枚举 \(i\) 这一维不好优化,想一下如何优化掉$ j $ 这一维。

考虑新建一个序列 \(q\) , 令 \(q[sum[i]]=dp[i]\) ,从左到右扫描 \(q\) ,这样就天然满足了 $ j<=i $ 这一条件。

那么转移就变成了:

\[ dp[i]=\sum_{j=min(sum)}^{sum[i]} q[j] \]

其中 \(\text{min{sum}}\)\(sum\) 中的最小值

每次转移后令 \(q[sum[i]] = dp[i]\)

于是题目转化成了求区间和,单点修改的问题,可以用树状数组实现(或者说权值树状数组)。不过根据数据范围会发现 $ -1e9<=sum[i]<=1e9 $,因此需要离散化一下。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+2023;
const int Mod=1e9+9;
int tot,sum[N],a[N],n,m,tree[N],dp[N],lsh[N],b[N];
int lowbit(int x) {
	return (x&-x);
}
void update(int x,int y) {
    while(x<=n) {
    	tree[x]+=y;
    	tree[x]%=Mod;
    	x+=lowbit(x);
	}
} 
int Sum(int x) {
	int res=0;
	while(x) {
		res+=tree[x];
		res%=Mod;
		x-=lowbit(x);
	}
	return res;
}
signed main(){
	cin>>n;
	for (int i=1;i<=n;++i) {
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
		//lsh[i]=sum[i];
	}
	for (int i=0;i<=n;++i) {
		lsh[++tot]=sum[i];
	}
	sort(lsh+1,lsh+tot+1);
	
	tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	for (int i=0;i<=n;++i) {
		b[i]=lower_bound(lsh+1,lsh+tot+1,sum[i])-lsh;
	}
	dp[0]=1;
	update(b[0],dp[0]);
	for (int i=1;i<=n;++i) {
		dp[i]=Sum(b[i]);
		update(b[i],dp[i]);
	}
	cout<<dp[n];
	return 0;
}

CF402E Strictly Positive Matrix

2023.8.6

题目链接

标签:图论、矩阵、强联通分量。

图论建模题。将矩阵中 \(a_{i,j}>0\) 的点,看作 \(i\)\(j\) 连一条有向边(为防止爆long long,权值大于零的统一看成 \(1\) )。

根据矩阵乘法的定义,对于矩阵 \(A\)\(k\) 次方,\(a_{i,j}^{k}\) 表示从 \(i\)\(j\) 经过 \(k\) 条边的路径长度。如果存在 \(k\) 使得这个路径长度大于 \(0\) ,则 \(i\) 可以到达 \(j\) 。反之,如果存在 \(k\) ,使得 \(a_{i,j}^{k}>0\) ,则在 \(A^{k}\) 中,
\(a_{i,j}^{k} > 0\)

因此,只要存在 \(k\) ,使得所有元素都能互相到达,那么就能使 \(A^k\) 中所有元素全为正数。

那么如果所有元素都能互相到达,整张图就是张强联通图,所有点都在同一个强联通分量里。 跑一个 \(tarjan\) 即可。

(经过上述推导后,可以发现实际做法跟 \(k\) 没有关系。)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;

inline int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) print(x/10);
	putchar(x%10+48);
}
const int N=1e6+2023;
int ans,n,m,a[2021][2021],tot,sta[N],sc,dfn[N],low[N];
bool vis[2021];
void Tarjan(int now) {
	dfn[now]=low[now]=++tot;
	vis[now]=1;
	sta[++sc]=now;
	for (int i=1;i<=n;++i) {
		if (!a[now][i]) continue;
		if (!dfn[i]) {
			Tarjan(i);
			low[now]=min(low[now],low[i]);
		}
		else if(vis[i]) {
			low[now]=min(low[now],dfn[i]);
		}
	}
	if (dfn[now]==low[now]) {
	    ++ans;
	    int t;
	    t=sta[sc--]; vis[t]=0;
	    while(now!=t) {
	    	t=sta[sc--];
	    	vis[t]=0;
		}
	}
}
signed main(){
	n=read();
	for (int i=1;i<=n;++i) {
		for (int j=1;j<=n;++j) {
			a[i][j]=read();
			if (a[i][j]>0) a[i][j]=1;
			else a[i][j]=0; 
		}
    }   
    for (int i=1;i<=n;++i) {
    	if(!dfn[i]) {
    		Tarjan(i);
		} 
	}
	if (ans==1) {
		cout<<"YES";
	}
	else cout<<"NO";
	return 0;  
}

洛谷P5025 [SNOI2017] 炸弹

2023.8.6

题目链接

标签:图论,优化建图。

把每个炸弹看作点,每个炸弹向它能炸到的炸弹连一条有向边,然后统计一下每个点能到达多少个点即可,时间复杂度 \(O(n^2)\)

这种做法很慢,需要连的边数很多,考虑优化。

考虑到每个炸弹所能炸到的一定是一个区间范围内的所有炸弹,那么可以考虑区间连边,用线段树优化。(qbxt老师没讲这个做法,回头再补一个这个做法吧。) 连边的时间复杂度为 \(O(n log n)\) ,统计答案时需要跑一次 \(Tarjan\)

考虑一个时间复杂度为线性的做法。

我们设 \(r_i\) 表示第 \(i\) 个炸弹左边第一个能炸到它的炸弹, \(r_i\) 表示第 \(i\) 个炸弹右边第一个能炸到它的炸弹。

连边时 \(l_i ->i,r_i->i\) 连边,可以发现原图中能到达的点在这个简化过的图中依然能到达,最后在这个图上统计答案即可。

每个炸弹最终引爆的炸弹一定是一段区间,统计答案时记录每个点能到达编号的最大最小值即可(类似单调栈?)时间复杂度大致为 \(O(n)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+2023;
int n,m,a[N],x[N],r[N];
int L[N],R[N],ans;
const int Mod=1e9+7;
signed main(){
	cin>>n;
	for (int i=1;i<=n;++i) {
		cin>>x[i]>>r[i];
		L[i]=R[i]=i; 
	}
	for (int i=2;i<=n;++i) {
		while (L[i]>1&&x[i]-x[L[i]-1]<=r[i]) {
			r[i]=max(r[i],r[L[i]-1]-(x[i]-x[L[i]-1]));
			L[i]=L[L[i]-1];
		}
	}
	for (int i=n-1;i>=1;--i) {
		while(R[i]<n&&x[R[i]+1]-x[i]<=r[i]) {
			L[i]=min(L[i],L[R[i]+1]);
			R[i]=R[R[i]+1];
		}
	}
	for (int i=1;i<=n;++i) {
		ans+=(i*(R[i]-L[i]+1));
		ans%=Mod;
	}
	cout<<ans;
	return 0;
}

洛谷P4211 [LNOI2014] LCA

2023.8.10

标签:LCA、树剖、线段树。

暴力求出所有LCA的深度肯定是不行的,而且这个题目只需要知道LCA的深度,并不需要确切知道LCA是谁。考虑一种特别的 LCA 的深度的求法。

\(u\)\(v\) 的 LCA 的深度时,初始时各个点的点权看成0,然后让 \(u\) 到根路径上各个点的点权+1,之后求出 \(v\) 到根节点路径的权值和,该权值和即为 \(u\)\(v\) 的 LCA 的深度。

题目要求我们求出给定区间里各个点对的 LCA 的深度和,那么每次对区间里的各个点执行一次上述操作,然后统计答案即可。

实现可以用树剖实现。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) print(x/10);
	putchar(x%10+48);
}
const int N=1e6+2023;
const int Mod=201314;
int n,m,a[N],fa[N],head[N<<1],cnt;
int dep[N],dfn[N],pre[N],top[N],size[N],son[N],tot;
int root=1;
struct node{
	int next,to,w;
}e[N<<1];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1
	struct node{
		int sum,len,lazy;
	}tree[N<<2];
	void build(int pos,int l,int r) {
		tree[pos].len=r-l+1;
		if(l==r) return ;
		int mid=l+r>>1;
		build(lson,l,mid); build(rson,mid+1,r);
	}
	void push_down(int pos) {
		if (!tree[pos].lazy) return ;
		tree[lson].lazy+=tree[pos].lazy;
		tree[rson].lazy+=tree[pos].lazy;
		tree[lson].sum+=tree[pos].lazy*tree[lson].len;
		tree[rson].sum+=tree[pos].lazy*tree[rson].len;
		tree[pos].lazy=0;
	}
	void change(int pos,int l,int r,int L,int R,int k) {
		if (l>=L && r<=R) {
			tree[pos].sum+=tree[pos].len*k;
			tree[pos].sum%=Mod;
		//	cout<<" "<<tree[pos].sum<<" "<<tree[pos].len<<endl;
			tree[pos].lazy+=k; tree[pos].lazy%=Mod;
			return ; 
		}
		int mid=l+r>>1; push_down(pos);
		if (L<=mid) change(lson,l,mid,L,R,k);
		if (R>mid) change(rson,mid+1,r,L,R,k);
		tree[pos].sum=tree[lson].sum+tree[rson].sum;
	} 
	int query(int pos,int l,int r,int L,int R) {
		//cout<<" "<<pos<<"   "<<L<<"  "<<R<<endl;
		if (l>=L && r<=R) return tree[pos].sum;
		int mid=l+r>>1,res=0; push_down(pos);
		if (L<=mid) res=query(lson,l,mid,L,R),res%=Mod;
		if (R>mid) res+=query(rson,mid+1,r,L,R),res%=Mod;
		return res;
	}
}
namespace sp{
    void dfs1(int now,int Fa) {
    	fa[now]=Fa; dep[now]=dep[Fa]+1; size[now]=1;
    	for (int i=head[now];i;i=e[i].next) {
    		if (e[i].to==Fa) continue;
    		dfs1(e[i].to,now);
    		size[now]+=size[e[i].to];
    		if (size[son[now]]<size[e[i].to]) son[now]=e[i].to;
		}
	}
	void dfs2(int now,int Top) {
		dfn[now]=++tot; pre[tot]=now; top[now]=Top;
		//cout<<"  "<<now<<"  "<<dfn[now]<<"\n";
		if(son[now]) dfs2(son[now],Top);
		for (int i=head[now];i;i=e[i].next) {
			if (e[i].to==fa[now]||e[i].to==son[now]) continue;
			dfs2(e[i].to,e[i].to);
		}
	}
	void change(int x,int y) {
		while(top[x]^top[y]) {
			if (dep[top[x]]<dep[top[y]]) swap(x,y);
		    ss::change(1,1,n,dfn[top[x]],dfn[x],1);
		    //cout<<" "<<ss::query(1,1,n,dfn[top[x]],dfn[x])<<endl; 
			x=fa[top[x]];
		}
		if (dep[x]>dep[y]) swap(x,y);
		ss::change(1,1,n,dfn[x],dfn[y],1);
	}
	int query(int x,int y) {
		int res=0;
		while(top[x]^top[y]) {
			if (dep[top[x]]<dep[top[y]]) swap(x,y);
			res+=ss::query(1,1,n,dfn[top[x]],dfn[x]); 
			res%=Mod;
			x=fa[top[x]];
		}
		if (dep[x]>dep[y]) swap(x,y);
		res+=ss::query(1,1,n,dfn[x],dfn[y]);
		return res%Mod;
	}
}
int tt,ans[N];
struct nd{
	int flag,x,y,i;
}q[N<<1];
bool cmp(nd x,nd y) {
	return x.x<y.x;
}
signed main(){
	n=read(); m=read();
	for (int i=2;i<=n;++i) {
		int x=read()+1;
		add(x,i); add(i,x);
	}
	ss::build(1,1,n);
	sp::dfs1(1,0); sp::dfs2(1,1);
	for (int i=1;i<=m;++i) {
		int l=read(),r=read()+1,z=read()+1;
        q[++tt].flag=0; q[tt].i=i; q[tt].x=l; q[tt].y=z;
        q[++tt].flag=1; q[tt].i=i; q[tt].x=r; q[tt].y=z;
	}
	sort(q+1,q+tt+1,cmp);
	for (int i=1;i<=tt;++i) {
		for (int j=q[i-1].x+1;j<=q[i].x;++j) {
			sp::change(1,j);
		}
		if (!q[i].flag) ans[q[i].i]=-sp::query(1,q[i].y);
		else {
			ans[q[i].i]+=sp::query(1,q[i].y);
			ans[q[i].i]=(ans[q[i].i]+Mod)%Mod;
			//ans[q[i].i]%=Mod;
		}
	}
	for (int i=1;i<=m;++i) {
		cout<<ans[i]<<"\n";
	}
	return 0;
}

洛谷P2312 [NOIP2014 提高组] 解方程

2023.8.14

标签:数学。

一道比较老的题目,很符合早期CCF出题风格。

题目给的数据范围很有意思, \(n<=100\) , \(m<=1e6\) ,这使得这个题可以在 \(O(nm)\) 的时间内跑过去。而 \(a\) 的范围就很毒了, \(a<=10^{10000}\) ,用高精会TLE。

\(x\) 的数据范围在 \([1,m]\) ,也就是 \([1,1e6]\) 里,因此可以枚举 \(x\) 的值,代入到式子里,判断一下是否合法。枚举自然没什么问题,但正常代入 \(x\) 到式子里运算会很慢。

对于一个普通的一元n次多项式来说,它的求值需要经过 $ \dfrac{(n+1)*n}{2} $ 次乘法和 \(n\) 次加法。而有一种算法只需要经过 \(n\) 次乘法和 \(n\) 次加法就能实现求值,这种算法名叫秦九韶算法。

计算过程

这是一个一元n次多项式:

\[ f(x)=a_0+a_1x+a_2x^2+a_3x^3+......+a_{n-1}x^{n-1}+a_nx^n \]

我们把它转成如下形式:

\[ f(x)=a_0+a_1x+a_2x^2+a_3x^3+......+a_{n-1}x^{n-1}+a_nx^n \]

\[ =a_0+x(a_1+a_2x+a_3x^2+......+a_{n-1}x^{n-2}+a_nx^{n-1}) \]

\[ =a_0+x(a_1+x(a_2+a_3x+......+a_{n-1}x^{n-3}+a_nx^{n-2})) \]

\[ =a_0+x(a_1+x(a_2+x(a_3+x(......+x(a_{n-1}+(xa_n)...)) \]

我们从最里面的那个括号开始运算,一层一层向外扩展,就能在只经过 \(n\) 次乘法和 \(n\) 次加法的情况下求出答案了。

对于这道题来讲,使用这种算法进行判断可以在 \(O(nm)\) 时间复杂度内求出答案。

一点小细节: \(a\) 的范围实在太大,高精会TLE,可以选择用大质数取模。建议用快读,因为题目有点卡常,而且a的范围过大,从快读里面就要开始取模。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int Mod=1e9+7;
inline int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-'),f%=Mod;
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48),x%=Mod;
	return f?-x:x;
}
void print(int x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) print(x/10);
	putchar(x%10+48);
}
const int N=1e6+2023;
int n,m,a[N],cnt,ans[N],sum;
bool check(int x) {
	sum=0;
	for (int i=n;i>=1;--i) {
		sum=(a[i]+sum)*x%Mod;
	}
    sum=(sum+a[0])%Mod;
    if (sum) return 0;
	else return 1; 
}
signed main(){
	n=read(); m=read();
    for (int i=0;i<=n;++i) {
    	a[i]=read();
	}
	for (int i=1;i<=m;++i) {
		if (check(i)) { 
		    ans[++cnt]=i;
		}
	}
    cout<<cnt<<"\n";
    for (int i=1;i<=cnt;++i) {
    	cout<<ans[i]<<"\n";
	}
	return 0;
}

CF1153D Serval and Rooted Tree

2023.8.16

题目链接

标签:树形DP,记忆化搜索。

一个min-max类型的DP题。

对于min节点,我们希望它的所有子节点的最小值尽可能大。

对于max节点,我们希望它的所有子节点中最大值尽可能大。

换而言之,对于min节点,它的每个子节点都可能影响其值,而对于max节点,能影响它的值的只有最大的那个子节点。

有一个贪心的思想是,对于每个对根节点有贡献的叶子节点,我们尽可能把大的数放在上面。对答案没有贡献的点就放剩下的数。

对于一个点 \(x\) ,我们可以用DP的思想去求其子树内对它的值有影响的叶子节点的量。

设会影响根节点大小的叶子结点的数量为 \(k\)

那么最终答案就是 \(n-k+1\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if (x<0) putchar('-'),x=-x;
	if (x>9) print(x/10);
	putchar(x%10+48); 
}
const int N=1e6+2023; 
int n,m,a[N],head[N],cnt;
struct node{
	int next,to,w;
}e[N];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
int dfs(int now) {
	int res=0;
	if (!head[now]) { //如果当前节点为叶子节点 
		res=1; // 需要在这个点上填一个数。 
		++m;//叶子节点数量+1。 
	}
    else if (a[now]) { //如果当前点取max 
    	res=0x7ffffff;
		for (int i=head[now];i;i=e[i].next) {
    		res=min(res,dfs(e[i].to));  
		}
	}
	else { // 如果当前点取min 
	    res=0;
	    for (int i=head[now];i;i=e[i].next) {
	    	res+=dfs(e[i].to);  
		}
	}
	return res;
}
signed main(){
	n=read();
	for (int i=1;i<=n;++i) {
	    a[i]=read();
	}
	for (int i=2;i<=n;++i) {
		int x=read();
		add(x,i);
	}
	int k=dfs(1);
	cout<<m-k+1; //根节点最大值 = 叶子节点数量 - 需要填的数的数量 + 1 
	return 0;
} 

洛谷P1742 最小圆覆盖

2023.8.22

题目链接

标签:计算几何,随机化。

一道看上去很简单的题目。求n个点的最小圆覆盖。

根据数学知识,可以得知三点可以确定一个圆。(前提是三点不共线。)

根据三点坐标计算圆心和半径的公式这里就不展开了。

一个定理是:如果一个点不在所有点的最小覆盖圆内,那么这个点一定在所有点的最小覆盖圆上。

那么就可以想到一个 \(O(n^3)\) 的做法:

初始时选一个点为圆心,此时这个圆里只有这一个点,半径为 \(0\)

然后枚举其他的点,枚举到的点无非三种情况:

1.在当前所求的圆里面。

2.在当前所求圆的边境上。

3.在当前所求圆外。

前两种情况无需更新答案。当出现第三种情况的时候,我们用新枚举到的这个点和前面枚举过的两个点(这两个点需要再枚举一下)确定一个圆,同时判断一下是否合法(不合法的话继续枚举两个之前枚举过的点与这个新枚举到的点确定圆,直到合法为止。)。

最终枚举过所有点后所求出的圆心和半径即为答案。

然而这样做的时间复杂度是难以接受的,同时毒瘤出题人还可以搞出前三个点共线这种数据卡人。

因此可以使用一种算法:随机增量法。

由于三个点可以确定一个圆。因此对于一个随机的数列,在 \(i\) 个点中每个点有 \(\dfrac{3}{i}\) 的概率成为更新这些点最小圆覆盖的点。(并不是 \(\dfrac{3}{i}\) 的概率在圆上 ) 。

而影响我们时间复杂度的是更新最小圆覆盖的次数。

因此进入第一重循环的if的概率(第一个点检测到合法的概率)为 \(\dfrac{3}{i}\)

进入第二重循环的if的概率为 \(\dfrac{3}{j}\) ,因此在第三重循环里操作次数的期望为 \(\dfrac{3}{j} \times j =3\) 。属于常数级的时间复杂度。不考虑常数的情况下,在第二重循环的操作次数的期望为 \(i\)

而算上开启第二重循环的概率,可以得出在第二重循环所操作次数的期望为 \(\dfrac{3}{i} \times i =1\)

因此,三重循环下来的实际时间复杂度是 \(O(n)\) 的。当然这需要在随机数据的情况下,需要用随机化大法处理一下。

代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+2023;
double eps=1e-9;
int n,m;
double X,Y,R;
struct node{
	double x,y;
}e[N];
double dis(node b) {
	return sqrt((X-b.x)*(X-b.x)+(Y-b.y)*(Y-b.y));
}
void Get_ans(node A,node B,node C){
	double a=2*(A.x-B.x),b=2*(A.y-B.y),c=2*(B.x-C.x),d=2*(B.y-C.y);
	double f=(A.x*A.x)-(B.x*B.x)+(A.y*A.y)-(B.y*B.y);
	double g=(B.x*B.x)-(C.x*C.x)+(B.y*B.y)-(C.y*C.y);
	X=(d*f-b*g)/(d*a-b*c); Y=(c*f-a*g)/(c*b-a*d);
	R=sqrt((A.x-X)*(A.x-X)+(A.y-Y)*(A.y-Y));
}  
void ss(){
	X=e[1].x,Y=e[1].y; R=0;
	for (int i=2;i<=n;++i) {
		if (dis(e[i])>R+eps){
			X=e[i].x,Y=e[i].y,R=0;
			for (int j=1;j<=i-1;++j) {
				if (dis(e[j])>R+eps) {
					X=(e[i].x+e[j].x)/2; Y=(e[i].y+e[j].y)/2;
					R=dis(e[j]);
					for (int k=1;k<=j-1;++k) {
						if (dis(e[k])>R+eps) {
							Get_ans(e[i],e[j],e[k]);
						}
					}
				}
			}
		}
	} 
}
signed main(){
	cin>>n;
	for (int i=1;i<=n;++i) {
		cin>>e[i].x>>e[i].y;
	}
	random_shuffle(e+1,e+n+1); //随机打乱。
	ss();
	printf("%.10lf\n%.10lf %.10lf",R,X,Y);
	return 0;
}

CF916E Jamie and Tree

2023.8.23

题目链接

标签:树链剖分、LCA。

如果没有换根操作,这题就是道树剖裸题。加入换根操作后,我们肯定不能每换一次根就从头再遍历一次树。

先以 \(1\) 号点为根,预处理出一些信息。之后我们考虑树上各点的位置关系:

设根节点为 \(R\) ,询问子树的根节点为 \(x\) 。现在我们需要对 \(x\) 的子树进行操作,无非如下几种情况:

  1. \(R\) 位于 \(x\) ,那么使用原本树剖修改、查询 \(x\) 的子树的做法即可。

  2. \(R\) 位于 \(x\) 的子树外,则对做法依旧无影响,使用原本树剖做法即可。

  3. \(R\) 位于 \(x\) 的子树内,那么需要执行操作的点为:

\(x\)\(R\) 路径上所经过第一个点的子树内的点以外的所有点。

先对整颗树进行操作,再找到 \(x\)\(R\) 路径上所经过的第一个点 ,再对这个点的子树进行一次反操作即可。

不过我们在执行操作的时候还会遇到一个问题: \(LCA\) 不好求了。

原本求 \(LCA\) 的做法在加入换根后需要发生改变,分类讨论一下:

1.如果 \(x\)\(y\)\(R\) 的子树内,那么它们的 \(LCA\) 依旧是 \(LCA(x,y)\)

2.如果 \(x\)\(y\) 只有一个在 \(R\) 的子树内,那么 \(LCA\) 肯定是 \(R\)

3.如果 \(x\)\(y\) 都不在 \(R\) 的子树内,我们先找到 \(A=LCA(x,R)\) , \(B=LCA(y,R)\) , \(C=LCA(x,y)\)

\(A\)\(B\) 相同时, \(x\)\(y\)\(LCA\) 为 C。

\(A\)\(B\) 不同时,\(x\)\(y\)\(LCA\)\(A\)\(B\) 中深度较深的那个。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) print(x/10);
	putchar(x%10+48);
}
const int N=2e5+2023;
int n,m,a[N],cnt,head[N];
int root=1,tot;
int pre[N],dfn[N],son[N],fa[N],size[N],top[N],dep[N];
struct node{
	int next,to,w;
}e[N<<1];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1
	struct node{
		int sum,len,lazy;
	}tree[N<<2];
	void build(int pos,int l,int r) {
		tree[pos].len=r-l+1;
		if (l==r) {
			tree[pos].sum=a[pre[l]];
			return ; 
		} 
		int mid=l+r>>1; 
		build(lson,l,mid); build(rson,mid+1,r);
		tree[pos].sum=tree[lson].sum+tree[rson].sum;
	}
	void push_down(int pos) {
	    if (!tree[pos].lazy) return ;
	    tree[lson].lazy+=tree[pos].lazy;
	    tree[rson].lazy+=tree[pos].lazy;
	    tree[lson].sum+=tree[pos].lazy*tree[lson].len;
	    tree[rson].sum+=tree[pos].lazy*tree[rson].len;
	    tree[pos].lazy=0;
    } 
    void change(int pos,int l,int r,int L,int R,int k) {
    	if (l>=L && r<=R) {
    		tree[pos].sum+=k*tree[pos].len;
    		tree[pos].lazy+=k;
    		return ;
		}
		int mid=l+r>>1; push_down(pos);
		if (L<=mid) change(lson,l,mid,L,R,k);
		if (R>mid) change(rson,mid+1,r,L,R,k);
		tree[pos].sum=tree[lson].sum+tree[rson].sum;
	}
	
	int query(int pos,int l,int r,int L,int R) {
		if (l>=L && r<=R) return tree[pos].sum;
		int mid=l+r>>1,res=0; push_down(pos);
		if (L<=mid) res+=query(lson,l,mid,L,R);
		if (R>mid) res+=query(rson,mid+1,r,L,R);
		return res;
	}
}
namespace sp{
	
	void dfs1(int now,int Fa) {
		fa[now]=Fa; dep[now]=dep[Fa]+1; size[now]=1;
		for (int i=head[now];i;i=e[i].next) {
		    if (e[i].to==Fa) continue;
		    dfs1(e[i].to,now);
		    size[now]+=size[e[i].to];
		    if (size[e[i].to]>size[son[now]]) son[now]=e[i].to;
		}
	}
	
	void dfs2(int now,int Top) {
		dfn[now]=++tot; pre[tot]=now; top[now]=Top;
	    if (son[now]) dfs2(son[now],Top);
	    for (int i=head[now];i;i=e[i].next) {
	    	if (e[i].to==fa[now]||e[i].to==son[now]) continue;
			dfs2(e[i].to,e[i].to); 
		}
	}
	
	int lca(int x,int y) {
	    while(top[x]^top[y]) {
	    	if (dep[top[x]]<dep[top[y]]) swap(x,y);
	    	x=fa[top[x]];
		}
		if (dep[x]>dep[y]) swap(x,y);
		return x;
	}
    int LCA(int x,int y) {
        if (dep[x]>dep[y]) swap(x,y);
        int A=lca(x,root),B=lca(y,root),C=lca(x,y);
        //cout<<" "<<A<<" "<<B<<" "<<C<<endl;
        if (C==x) {
            if (A==x) {
            	if (B==y) return y;
            	else return B;
			}
            return x;
		}
		if (A==x) return x;
		if (B==y) return y;
		if ((A==root&&B==C)||(B==root&&A==C)) return root;
		if (A==B) return C;
		if (C!=A) return A;
		else return B;
    }
    int find(int x,int y) {
    	while(top[x]!=top[y]) {
    		if (dep[top[x]]<dep[top[y]]) swap(x,y);
    		if (fa[top[x]]==y) return top[x];
    		x=fa[top[x]];
		}
		if (dep[x]>dep[y]) swap(x,y);
		return son[x];
	}
    void update(int x,int k) {
    	if (x==root) {
		    ss::change(1,1,n,1,n,k);
		    return ;
		}
		if (dfn[root]>=dfn[x]&&dfn[root]<=dfn[x]+size[x]-1) {
		//	ss::change(1,1,n,dfn[x],dfn[x]+size[x]-1,k);
		    ss::change(1,1,n,1,n,k);
			int y=find(x,root);
			ss::change(1,1,n,dfn[y],dfn[y]+size[y]-1,-k);
	    }	
	    else ss::change(1,1,n,dfn[x],dfn[x]+size[x]-1,k);
    }
    int query(int x) {
    	if (x==root){
    		return ss::query(1,1,n,1,n);
		} 
    	if (dfn[root]>=dfn[x]&&dfn[root]<=dfn[x]+size[x]-1) { //root在x子树内 
    	    int res=0;
			//res+=ss::query(1,1,n,dfn[x],dfn[x]+size[x]-1);
			res+=ss::query(1,1,n,1,n);
			int y=find(x,root);
    	    res-=ss::query(1,1,n,dfn[y],dfn[y]+size[y]-1);
    	    return res;
	    }
		return ss::query(1,1,n,dfn[x],dfn[x]+size[x]-1);
	}
}
signed main(){
    n=read(); m=read();
	for (int i=1;i<=n;++i) {
		a[i]=read();
	} 
	for (int i=1;i<n;++i) {
		int x=read(),y=read();
		add(x,y); add(y,x);
	}
    root=1;
    sp::dfs1(root,0); sp::dfs2(root,root); ss::build(1,1,n);
    //for (int i=1;i<=n;++i) {
    //	cout<<"  "<<dfn[i]<<"\n";
	//
    for (int i=1;i<=m;++i) {
    	int op=read();
    	if (op==1) root=read();
    	else if (op==2) {
    		int x=read(),y=read(),z=read();
    		sp::update(sp::LCA(x,y),z);
		}
		else if (op==3) {
			int x=read();
			print(sp::query(x));
			putchar('\n');
		}
	}
	return 0;
} 

ABC318D General Weighted Max Matching

2023.9.3

题目链接

标签:状压DP

看到 \(n<=16\) 考虑状压 。

\(S\) 表示 状压后已选边的两端点的集合(状压后 \(S\) 二进制下每一位表示该点有没有被选到。)

\(dp[S]\) 表示在状态 \(S\) 下所能取得的最大收益。

转移方程:

\[ dp[s]= max_{j,k\in S } ( \text{ }{dp[S-{j,k}]+w_{j->k}} ) \]

注: $ (j<k) $

枚举状态时间复杂度为 \(2^n\) ,枚举 \((j,k)\) 时间复杂度为 \(O(n^2)\) , 总时间复杂度为 \(O(2^n n^2)\),因为 \(n<=16\) ,因此能通过。

需要开 long long

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e7+2023;
int n,m,a[102][102];
int dp[N],ans; 
// 设 S 表示已选的集合。dp[s]表示选择集合为 S 时的最大收益  
signed main(){
	cin>>n;
	for (int i=0;i<n;++i) {
		for (int j=i+1;j<n;++j) {
			cin>>a[i][j];
		}
	}
    for (int i=0;i<(1<<n)-1;++i) {
    	int w=-1;
    	for (int j=0;j<n;++j) {
    		if (!(i>>j&1)) {
    			w=j; 
				break;
			}
		}
		for (int j=0;j<n;++j) {
			if (!(i>>j&1)) {
				int s=i|(1<<w)|(1<<j);
				dp[s]=max(dp[s],dp[i]+a[w][j]);
			    ans=max(ans,dp[s]);
			}
		}
	}
	cout<<ans;
	return 0;
}

洛谷P3469 [POI2008] BLO-Blockade

2023.9.9

题目链接

标签:tarjan,割点.

总点数设为 \(n\)

题目要我们求封锁一个点后的不连通点对(有顺序性,\((x,y)\)\((y,x)\) 视为两组不同点对 )的数量,注意是"封锁"而非"删除"。

也就是说,即使点 \(x\) 是割点,封锁之后也会有 \((n-1)\) 个点 无法到达 \(x\) ,因此会出现 \(2*(n-1)\) 个不连通点对。

设当前点为 \(now\) ,封锁该点后的答案为 \(ans[now]\) ,删去以 \(now\) 为端点的边后出现 \(k\) 个不同的连通块,其大小记作 $ S_1,S_2,...S_k $。

则每个连通块对于 \(ans[now]\) 的贡献为 \(S_i(n-S_i)\)

注意:\(S_i\) 没有被实际删去,它本身也是一个大小为 \(1\) 的连通块。

所以答案为:

\[ ans[now]=\sum_{i=1}^{k}S_i(n-S_i) 。 \]

非割点的情况答案为 \(2*(n-1)\) 。接下来考虑割点的情况:

假设点 \(u\) 为割点,那么在tarjan的过程中,对于任意能判定 \(u\) 为割点的点 \(v\)\(v\)一定在 \(u\) 的dfs树上的子树内,在封锁 \(u\) 之后, \(v\) 及其在dfs树上的子树一定会构成一个独立的连通块,对答案造成 \(size[v]*(n-size[v])\) 的贡献(其中 size[v]表示 \(v\) 在dfs树上的子树大小),因此对于一个割点 \(u\),我们统计所有能判定 \(u\) 为割点的 \(v\) 所做的贡献,除去这些节点后,还有一个大小为 \(n-1-\sum{size[v_i]}\) 的连通块(可能为空,但不影响答案)。

时间复杂度为 \(O(n)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {                   
	int x=0,f=0;char ch=getchar();                   
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');             
    for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);   
	return f?-x:x;        
} 
const int N=1e6+2023;
int n,m,head[N<<1],cnt,tot;
int low[N],dfn[N],size[N],ans[N];
// size[now] 表示 在dfs树上,now 的子树大小 
// ans[i] 表示封锁第i个点后的答案。 
struct node{
	int next,to,w;
}e[N<<1];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
} 
void Tarjan(int now) {
	dfn[now]=low[now]=++tot;
	int t=n-1; 
	//t表示除去now及其子树的连通块大小,初始视为n-1 。 
	size[now]=1; ans[now]=t;
	for (int i=head[now];i;i=e[i].next) {
	    if (!dfn[e[i].to]) {
	    	Tarjan(e[i].to);
	    	size[now]+=size[e[i].to];
	    	low[now]=min(low[now],low[e[i].to]);
	    	if (low[e[i].to]>=dfn[now]) { 
			// now为割点,统计其子树e[i].to对答案的贡献。 
	    		ans[now]+=size[e[i].to]*(n-size[e[i].to]);
	    		t-=size[e[i].to];
			}
		}
		else {
			low[now]=min(low[now],dfn[e[i].to]);
		}
 	}
 	ans[now]+=t*(n-t);  
	//除去now及其子树外,还有一个大小为t的连通块。 
}
signed main(){
	n=read(); m=read();
	for (int i=1;i<=m;++i) {
		int x=read(),y=read();
		add(x,y); add(y,x);
	} 
	Tarjan(1);
	for (int i=1;i<=n;++i) 
	    cout<<ans[i]<<"\n";
	return 0;
} 

洛谷P5058 [ZJOI2004] 嗅探器

2023.9.13

题目链接

标签:tarjan,双连通分量。

先考虑无解的情况,点 \(a\) 和点 \(b\) 形成成点双连通,即从点 \(a\)\(b\) 存在至少两条完全不相交(无重合点)的路径,那么无论选择哪个点安装嗅探器,从 \(a\)\(b\) 都有一条绕过这个点的路径。

考虑有解的情况,不难发现,我们安装嗅探器的点一定是个割点,因此我们可以使用 \(tarjan\) 来找到所有割点。

接下来,我们考虑这些割点是否适合安装嗅探器。可以想到,不在 \(a\)\(b\) 路径上的割点一定不适合安装嗅探器,而如果在一个位于 \(a\)\(b\) 路径上的割点上安装嗅探器,则一定可以使得 \(a\) 无法到达 \(b\) 。因此答案为 \(a\)\(b\) 路径上编号最小的割点。

如何判断一个割点是否在 \(a\)\(b\) 的路径上呢?可以发现一个性质性质:

在从点 \(a\) 为起点开始 \(Tarjan\) 所形成的 \(dfs\) 树中,如果 \(b\) 在割点 \(x\) 的子树内时,则 \(x\) 位于 \(a\)\(b\) 的必经之路上。

代码实现时,可以在tarjan过程中,找到一个点 \(now\) 为割点时 ( \(low[e[i].to]>dfn[now]\) ) , 判断 \(b\) 是否在 \(now\) 的子树内( \(dfn[e[i].to]<=dfn[b]\) )即可。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+2023;
inline int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
} 
bool vis[N];
int n,m,cnt,head[N<<1],tot,dfn[N],low[N],a,b;
struct node{
	int next,to;
}e[N<<1];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
void Tarjan(int now) {
	dfn[now]=low[now]=++tot;
	for (int i=head[now];i;i=e[i].next) {
		if (!dfn[e[i].to]) {
			Tarjan(e[i].to);
			low[now]=min(low[now],low[e[i].to]);
		    if (low[e[i].to]>=dfn[now]&&dfn[e[i].to]<=dfn[b]&&now!=a) {
		    	vis[now]=1;
			}
		}
		else {
			low[now]=min(low[now],dfn[e[i].to]);
		}
	}
}
signed main(){
	n=read();
	while(1) {
		int x=read(),y=read();
		if (x==0&&y==0) break;
		add(x,y); add(y,x);
	}
	a=read(); b=read();
	Tarjan(a);
	for (int i=1;i<=n;++i) {
		if (vis[i]) {
			cout<<i;
			return 0;
		}
	}
	cout<<"No solution";
	return 0;
}

洛谷P2824 [HEOI2016/TJOI2016] 排序

2023.10.7

题目链接

标签:线段树。

正常对一个序列进行排序需要 $ O(nlogn) $ 的时间复杂度,但如果对一个01序列进行排序呢?

我们可以用线段树维护序列的值,以及区间里1的个数。

\([l,r]\) 区间里1的个数为 \(cnt1\) ,若对 \([l,r]\) 排序,则可用线段树进行区间赋值来实现。

升序排序: \([r-cnt1+1,r]\) 赋值为1,\([l,r-cnt1]\) 赋值为0 。

降序排序: \([l,r-cnt1]\) 赋值为1, \([r-cnt1+1,r]\) 赋值为0 。

特别注意:当 \(cnt1=0\) 的时候,我们可以发现需要修改的区间是 \(L>R\) 的,需要在线段树里特判一下。(或者单独对 \(cnt1=0\) 特判) 。

这样我们就可以实现 $ O(logn) $ 的复杂度对01序列进行排序了。

现在考虑如何将原序列转化为01序列。

由于原题只需要执行一次询问,因此考虑一个离线做法。

我们二分位置 \(p\) 的值,设 \(mid\) 为我们当前枚举到的值。由于序列一定是一个排列,因此二分边界就确定了: \(l=1,r=n\)

每次我们把原序列里所有大于等于 \(mid\) 的数赋值为1,所有小于 \(mid\) 的数赋值为0 。

然后执行排序操作,若所有排序操作结束后,位置 \(q\) 的数为1,那么说明答案大于等于 \(mid\) ,我们让 $ l=mid+1 $ , 然后继续二分;如果位置 \(q\) 的数为0,那么说明答案小于 \(mid\) 我们让 \(r=mid-1\) 再继续二分下去。

最后得到的合法 \(mid\)(最小的,能让位置 \(q\) 等于1的 \(mid\) ) 就是答案。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if (x<0) putchar('-'),x=-x;
	if (x>9) print(x/10);
	putchar(x%10+48);
}
const int N=1e6+2023;
int n,m,a[N],op[N],L[N],R[N],p;
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1
	struct node{
		int cnt1,lazy,len;
	}tree[N<<4]; 
	void build(int pos,int l,int r,int x) {
		tree[pos].len=r-l+1; tree[pos].lazy=-1;
	    tree[pos].cnt1=0;
		if (l==r) {
			tree[pos].cnt1=(a[l]>=x);
			tree[pos].lazy=-1;
			return ;
		}
		int mid=l+r>>1;
		build(lson,l,mid,x); build(rson,mid+1,r,x);
		tree[pos].cnt1=tree[lson].cnt1+tree[rson].cnt1;
	}
	void push_down(int pos) {
		if (tree[pos].lazy==-1) return ;
		tree[lson].lazy=tree[pos].lazy;
		tree[rson].lazy=tree[pos].lazy;
	    if (tree[pos].lazy==1) {
	    	tree[lson].cnt1=tree[lson].len;
	    	tree[rson].cnt1=tree[rson].len;
		}
		else tree[lson].cnt1=tree[rson].cnt1=0;
		//tree[lson].cnt1=tree[lson].len*tree[pos].lazy;
	    //tree[rson].cnt1=tree[rson].len*tree[pos].lazy;
	    
		tree[pos].lazy=-1; return ;
	}
	void change(int pos,int l,int r,int L,int R,int k) {
	    if (l>=L && r<=R) {
	    	tree[pos].cnt1=k*tree[pos].len;
	    	tree[pos].lazy=k; return ;
		}
		if (L>r||R<l) return ;
		int mid=l+r>>1; push_down(pos);
		if (L<=mid) change(lson,l,mid,L,R,k);
		if (R>mid) change(rson,mid+1,r,L,R,k);
		tree[pos].cnt1=tree[lson].cnt1+tree[rson].cnt1;
	}
	int query(int pos,int l,int r,int L,int R) {
		if (l>=L && r<=R) return tree[pos].cnt1;
		if (L>r||R<l) return 0; 
		int mid=l+r>>1; push_down(pos);
		return query(lson,l,mid,L,R)+query(rson,mid+1,r,L,R); 
	}
	int x_query(int pos,int l,int r,int x) {
		if (l==r) return tree[pos].cnt1;
		int mid=l+r>>1; push_down(pos);
		if (x<=mid) return x_query(lson,l,mid,x);
		else return x_query(rson,mid+1,r,x);
	}
}
bool check(int x) {
	ss::build(1,1,n,x);
	for (int i=1;i<=m;++i) {
		int cnt1=ss::query(1,1,n,L[i],R[i]);
		if (op[i]==0) {
			ss::change(1,1,n,R[i]-cnt1+1,R[i],1);
			ss::change(1,1,n,L[i],R[i]-cnt1,0);
		}
		else {
			ss::change(1,1,n,L[i],L[i]+cnt1-1,1);
			ss::change(1,1,n,L[i]+cnt1,R[i],0);
		}
	}
	return ss::x_query(1,1,n,p);
}
signed main(){
    n=read(); m=read();
    for (int i=1;i<=n;++i) {
    	a[i]=read();
	}
    for (int i=1;i<=m;++i) {
    	op[i]=read(); L[i]=read(); R[i]=read();
	} 
    p=read();
    int l=1,r=n,mid,ans;
    while(l<=r) {
    	mid=l+r>>1;
    	if (check(mid)) {
    		ans=mid;
    		l=mid+1;
		}
		else r=mid-1;
	}
    cout<<ans;
	return 0;
}

CF1083C Max Mex

2023.10.8

题目链接

标签:线段树,LCA。

我们设 \([l,r]\) 区间表示一段刚好经过 \([l,r]\) 区间里所有数的路径。

我们用线段树对 \([1,n]\) 及其子区间进行维护,设 \(x\) 表示路径起点, \(y\) 表示路径终点。

对于线段树上的一个区间 \([l,r]\) ,我们维护它的 \(x\)\(y\) ,可以想到, \(x\) 点的值为 \(l\) , \(y\) 点的值为 \(r\)

如果不存在一个 \(x\)\(y\) 的路径,使得其刚好经过 \([l,r]\) 里的所有数,那么 \([l,r]\) 区间的 \(x\)\(y\) 都设成 \(-1\)

\(num[l]\) 表示 值为 \(l\) 的点的序号。

线段树上所有叶子节点的 \(x\)\(y\) 均等于 \(num[l]\)

再考虑区间合并。

设两个相邻区间分别为 \([l1,r1]\)\([l2,r2]\) ,其 \(x\)\(y\) 的值分别为 \(x1\) , \(y1\)\(x2\) , \(y2\)

\(x1\)\(y1\)\(x2\)\(y2\) 都不是 \(-1\) 并且 \(x1\) , \(y1\) , \(x2\) 三点位于同一条简单路径上(存在 \(x2\)\(x1\)\(y1\) 的路径上或 \(x1\)\(x2\)\(y1\) 的路径上或 \(y1\)\(x1\)\(x2\) 路径上)时,两区间才能合并。

判断三点位于同一条路径上的方法:

设三点间的距离为 \(A\) , \(B\) , \(C\) ,如果存在 \(A+B=C\)\(A+C=B\)\(B+C=A\) 时,说明三点位于同一路径上。

树上快速求两点间距离的方法:

设两点分别为 \(a\)\(b\) , \(dep[x]\) 表示 \(x\) 点的深度,则

\[dis(a,b) = dep[a] +dep[b] -2*(dep[lca(a,b)]) \]

注意: 倍增求LCA在本题可能会被卡常,建议使用树剖求LCA。

询问答案时可以采用二分,但如果直接用二分+线段树的话时带两个 \(log\) 的,可以直接在线段树上二分使其优化成一个 \(log\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if (x<0) putchar('-'),x=-x;
	if (x>9) print(x/10);
	putchar(x%10+48);
}
const int N=1e6+2023;
int n,m,a[N],head[N],cnt,dep[N],fa[N],son[N],size[N],top[N],tot;
int num[N];
struct nd{
	int next,to;
}e[N];
void add(int u,int v) {
	e[++cnt].next=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
namespace sp{
	void dfs1(int now,int Fa) {
		size[now]=1; fa[now]=Fa; dep[now]=dep[Fa]+1;
		for (int i=head[now];i;i=e[i].next) {
	        if (e[i].to==Fa) continue;
	        dfs1(e[i].to,now);
	        size[now]+=size[e[i].to];
	        if (size[e[i].to]>size[son[now]]) son[now]=e[i].to;
		} 
	}
	void dfs2(int now,int Top) {
	    top[now]=Top;
	    if (son[now]) dfs2(son[now],Top);
	    for (int i=head[now];i;i=e[i].next) {
	    	if (e[i].to==fa[now]||e[i].to==son[now]) continue;
	    	dfs2(e[i].to,e[i].to);
		}
	}
	int LCA(int x,int y) {
		while(top[x]^top[y]) {
			if (dep[top[x]]<dep[top[y]]) swap(x,y);
			x=fa[top[x]];
		}
		if (dep[x]>dep[y]) swap(x,y);
		return x;
	}
}
struct node{
	int x,y;
}tree[N<<2];
int JL(int x,int y) {
	int lca=sp::LCA(x,y);
	//cout<<"   "<<x<<"   "<<y<<"   "<<lca<<endl;
	return dep[x]+dep[y]-2*dep[lca];
}
node hb(node a,int C) {
	if (a.x<0||C<0) return (node){-1,-1};
	int A=a.x,B=a.y;
	int d1=JL(A,C); int d2=JL(B,C); int d3=JL(A,B);
	if (d1+d2==d3) return (node){A,B};
	if (d1+d3==d2) return (node){C,B};
	if (d2+d3==d1) return (node){A,C};
	return (node){-1,-1};
}
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1 
	node Merge(node a,node b) {
		if (a.x<0||b.x<0) return (node){-1,-1};
		node s=hb(a,b.x);
		if (s.x<0) return (node){-1,-1};
		else return hb(s,b.y);
 	}
	void push_up(int pos) {
		tree[pos]=Merge(tree[lson],tree[rson]);
	}
	void build(int pos,int l,int r) {
		if (l==r) {
			tree[pos].x=num[l];
			tree[pos].y=num[l];
			return ;
		}
		int mid=l+r>>1;
		build(lson,l,mid); build(rson,mid+1,r);
		push_up(pos);
	}
	void change(int pos,int l,int r,int x) {
		if (l==r) {
			tree[pos].x=num[l];
			tree[pos].y=num[l];
			return ;
		}
		int mid=l+r>>1;
		if (x<=mid) change(lson,l,mid,x);
		else change(rson,mid+1,r,x);
		push_up(pos);
	}
	int query(node &now,int pos,int l,int r) {
		if (tree[pos].x>=0) {
			if (now.x<0) {
				now=tree[pos];
				return r+1;
			}
			node s=Merge(now,tree[pos]);
	     	if (s.x!=-1) {
			    now=s; 
			    return r+1;
		    }			
		}		
		if (l==r) return l;
		int mid=l+r>>1,res;
		res=query(now,lson,l,mid);
		if (res<=mid) return res;
		else return query(now,rson,mid+1,r); 
	}
}
signed main(){
    n=read();
    for (int i=1;i<=n;++i) {
    	a[i]=read()+1;
    	num[a[i]]=i;
	} 
    for (int i=2;i<=n;++i) {
    	int x=read();
    	add(i,x); add(x,i);
	}
	sp::dfs1(1,0); 
	sp::dfs2(1,1); 
	ss::build(1,1,n);
	m=read();
	while(m--) {
		int op=read();
		if (op==1) {
			int l=read(),r=read();
			swap(num[a[l]],num[a[r]]);
			swap(a[l],a[r]);
			ss::change(1,1,n,a[l]);
			ss::change(1,1,n,a[r]);
		}
		else {
			node s=(node){-1,-1};
			int ans=ss::query(s,1,1,n);
			cout<<ans-1<<"\n";
		}	
	}
	return 0;
}

CF689D Friends and Subsequences

2023.10.8

题目链接

标签:线段树,二分。

注:由于本题是不带修改的静态查询,因此用ST表代替线段树可以优化成一个 \(log\) ,不过由于我不擅长用 \(ST\) 表,所以用的线段树。

不难想到使用线段树维护区间最大值和最小值,但 \(O(n^2)\) 枚举每个区间的话无法满足 \(n <= 2e5\) 的要求,考虑优化。

可以发现,如果我们固定住 \(l\) ,再从小到大枚举 \(r\) ,那么:

$\mathop{\max}_{i=l}^{r} a[i] $ 的值是单调不降的,

$\mathop{\min}_{i=l}^{r} b[i] $ 的值是单调不减的。

因此可以得出结论:对于一个确定的 \(l\) ,其合法的 \(r\) 的范围是连续的。

我们可以二分求出每个 \(l\) 所对应 \(r\) 的最大值和最小值。

可以采用在线段树上二分的方法,对于每个区间采用:

递归左子树-> 返回答案 -> 递归右子树的方法查询区间 \([L,R]\) 中最右或最左满足条件的点,复杂度为 \(O(logn)\)

具体实现:

固定 \(l\) 端点,二分 \(r\) 端点。

\(query1(pos,L,R)\) 表示区间 \([L,R]\) 中满足 $ max(a_l,a_{l+1},......,a_k) >min(b_l,b_{l+1},......,b_k) $ 的最小的点,其中 \(L<=k<=R\)

同理设 \(query2(pos,L,R)\) 表示区间 \([L,R]\) 中满足 $ max(a_l,a_{l+1},......,a_k) < min(b_l,b_{l+1},......,b_k) $ 的最小的点,其中 \(L<=k<=R\)

用全局变量 \(maxx\)\(minn\) 记录遍历过的最大值和最小值,每次归左子树,返回答案,再递归右子树。

\(query1\) 求出 \(r\) 合法区间的左端点,再用 \(query2\) 求出 \(r\) 合法区间的右端点,两者做差就是以 \(l\) 为左端点的合法区间的数量。

再从 \([1,n]\) 枚举 \(l\) ,统计答案。

最终时间复杂度约为 \(O(nlogn)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
	for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}
void print(int x) {
	if (x<0) putchar('-'),x=-x;
	if (x>9) print(x/10);
	putchar(x%10+48);
}
const int INF=0x7fffffff;
const int N=1e6+2023;
int n,m,a[N],b[N],maxx,minn;
namespace ss{
	#define lson pos<<1
	#define rson pos<<1|1
	struct node{
		int maxx,minn;
	}tree[N<<2];
	void push_up(int pos) {
		tree[pos].maxx=max(tree[lson].maxx,tree[rson].maxx);
		tree[pos].minn=min(tree[lson].minn,tree[rson].minn);
	}
	void build(int pos,int l,int r) {
		if (l==r) {
			tree[pos].maxx=a[l];
			tree[pos].minn=b[l];
			return ;
		}
		int mid=l+r>>1;
		build(lson,l,mid); build(rson,mid+1,r);
	    push_up(pos);
	}
	int query1(int pos,int l,int r,int L,int R) {
		int mid=l+r>>1;
		if (l>=L && r<=R) {
			int tmax=max(maxx,tree[pos].maxx);
			int tmin=min(minn,tree[pos].minn);
			if (tmax<=tmin) {
				maxx=tmax;
				minn=tmin;
				return r+1;
			} 
			if (l==r) return l;
		    int now=query1(lson,l,mid,L,R);
		    if (now==mid+1) now=query1(rson,mid+1,r,L,R);
		    return now;
		}
		else if (mid>=L && mid<R){
			int now=query1(lson,l,mid,L,R);
			if (now==mid+1) now=query1(rson,mid+1,r,L,R);
			return now;
		}
		else if (mid>=R) return query1(lson,l,mid,L,R);
		else return  query1(rson,mid+1,r,L,R);
	}
	int query2(int pos,int l,int r,int L,int R) {
		int mid=l+r>>1;
		if (l>=L && r<=R) {
			int tmax=max(maxx,tree[pos].maxx);
			int tmin=min(minn,tree[pos].minn);
			if (tmax<tmin) {
				maxx=tmax;
				minn=tmin;
				return r;
			}
			if (l==r) return l-1;
			int now=query2(lson,l,mid,L,R);
			if (now==mid) now=query2(rson,mid+1,r,L,R);
			return now;
		} 
		else if (mid>=L && mid<R) {
			int now=query2(lson,l,mid,L,R);
			if (now==mid) now=query2(rson,mid+1,r,L,R);
			return now;
		}
		else if (R<=mid) return query2(lson,l,mid,L,R);
		else return query2(rson,mid+1,r,L,R);
	}
}
signed main(){
	n=read();
	for (int i=1;i<=n;++i) {
		a[i]=read();
	}
	for (int i=1;i<=n;++i) {
		b[i]=read();
	}
	ss::build(1,1,n);
	int ans=0;
	for (int i=1;i<=n;++i) {
		maxx=-INF,minn=INF;
	    int R=ss::query1(1,1,n,i,n);
	    maxx=-INF,minn=INF;
		int L=ss::query2(1,1,n,i,n);
	    ans+=R-L-1;
	}
	cout<<ans;
	return 0;
}
posted @ 2023-08-05 20:41  int_Hello_world  阅读(36)  评论(1编辑  收藏  举报