2024.3 总结

图论

【Luogu P6961】 Journey from Petersburg to Moscow

题目描述

给定一个 \(n\)\(m\) 边的有向图,边有边权,找一条从 \(1\)\(n\) 的最短路径,一条路径的代价为这条路径上前 \(k\) 大的边权和,\(1 \le n,m \le 3000\)

解题思路

首先我们可以想到枚举第 \(k\) 大的边权 \(x\),考虑如何求解每条路径的代价。
直接将小于 \(x\) 的边权设为 \(0\) 是不可以的,因为不能保证在某条路径上该边为第 \(k\) 大。
我们可以考虑将某些方案变得更劣,但不改变最优值以方便计算,在这题里,就是希望找出一种计算方法,使得 \(x\) 在某条路径中的排名不管大于 \(k\) 还是小于 \(k\) 都会不优于该条路径正确的代价。
我们可以将某条路径中所以边权大于等于 \(x\) 的边的边权计算时都减去 \(x\) ,最后再加上 \(k \times x\),这样的话,我们能保证 \(x\) 为该路径中第 \(k\) 大时,代价不变,而 \(x\) 的排名大于或小于 \(k\) 的话,都会比正确的代价劣。
套最短路时间复杂度到 \(O(n^2logn)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,v;
}q;
long long n,m,k,f[3005],b[3005];
bool v[3005];
vector<long long> a[3005],t[3005];
bool operator <(const datay &q,const datay &w)
{
	return q.v>w.v;
}
priority_queue<datay> l;
void dijah(long long p)
{
	memset(f,1,sizeof(f));
	memset(v,false,sizeof(v));
	q.x=1,q.v=0,l.push(q),f[1]=0;	
	long long x,z;
	while(l.size())
	{
		while(l.size()&&v[l.top().x])l.pop();
		if(!l.size())break;
		x=l.top().x,v[x]=true;
		for(int i=0;i<a[x].size();i++)
		{
			z=0,z=max(z,t[x][i]-p);
			if(f[a[x][i]]>f[x]+z)
			{
				f[a[x][i]]=f[x]+z;
				if(!v[a[x][i]])q.x=a[x][i],q.v=f[a[x][i]],l.push(q);
			}
		}
	}
	return;
}
int main()
{
	long long x,y,z,s=1e13+5;
	scanf("%lld%lld%lld",&n,&m,&k);
	for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&x,&y,&z),a[x].push_back(y),a[y].push_back(x),t[x].push_back(z),t[y].push_back(z),b[i]=z;
	for(int i=1;i<=m;i++)dijah(b[i]),s=min(s,f[n]+b[i]*k);
	dijah(0),cout<<min(s,f[n]);

  return 0;
}

【Luogu P6822】 Tax

题目描述

给出一个 \(n\) 个点 \(m\) 条边的无向图,经过一个点的代价是进入和离开这个点的两条边的边权的较大值,求从 \(1\)\(n\) 的最小代价,起点的代价是离开起点的边的边权,终点的代价是进入终点的边的边权,\(1 \le n \le 10^5,1 \le m \le 2 \times 10^5\)

解题思路

因为代价为两条边权的最大值,所以我们考虑拆点。
我们这样考虑:经过一条边来到一个点,要么下一条边权值比自己小,要么比自己大,这样我们就可以在每一个点出每一条边都向每一条边建一条有向边,代价为两条边权的最大值减去进来的边。
但这样做会被菊花图卡掉,我们考虑每一条边只向最大的边权小于它的边连一条边权为 \(0\) 的有向边,向最小的大于它的边连一条等于两边边权之差的边,对应着最大值升高。
直接跑最短路,时间复杂度 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y;
};
struct data
{
	long long x,y;
};
long long n,m,start,endd,s,f[400005];
vector<datay> l[100005];
vector<long long> a[400005],t[400005]; 
priority_queue<data> l1;
bool v[400005];
bool operator <(const data &q,const data &w)
{
	return q.y>w.y;
} 
void insert(long long x,long long y,long long z)
{
	datay q;
	q.x=y,q.y=z,l[x].push_back(q);
}
bool cmp(datay q,datay w)
{
	return q.y<w.y;
}
void dijah()
{
	memset(v,false,sizeof(v));
	memset(f,1,sizeof(f));
	data q,x;q.x=start,q.y=0;
	f[start]=0,l1.push(q);
	while(l1.size()!=0)
	{
		while(l1.size()!=0&&v[l1.top().x])l1.pop();
		if(l1.size()==0)break;
		x=l1.top(),l1.pop(),v[x.x]=true;
		for(int i=0;i<a[x.x].size();i++)if(f[a[x.x][i]]>f[x.x]+t[x.x][i])f[a[x.x][i]]=f[x.x]+t[x.x][i],q.x=a[x.x][i],q.y=f[a[x.x][i]],l1.push(q);
	}
	return;
}
int main()
{
	long long x,y,z;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&x,&y,&z),a[2*i-1].push_back(2*i),a[2*i].push_back(2*i-1),t[2*i-1].push_back(z),t[2*i].push_back(z),insert(x,2*i-1,z),insert(y,2*i,z);
	for(int i=1;i<=n;i++)
	{
		if(l[i].size()==0)continue;
		sort(l[i].begin(),l[i].end(),cmp);
		if(i==1)start=l[i][0].x,s=l[i][0].y;
		if(i==n)endd=l[i][0].x;
		for(int j=0;j<l[i].size()-1;j++)a[l[i][j].x].push_back(l[i][j+1].x),t[l[i][j].x].push_back(l[i][j+1].y-l[i][j].y),a[l[i][j+1].x].push_back(l[i][j].x),t[l[i][j+1].x].push_back(0);
	}
	dijah();
	printf("%lld",s+f[endd]);
  return 0;
}

【CF196E】 Opening Portals

题目描述

给出一个有\(n\)个结点,\(m\)条带权边的无向连通图,边权为经过这条边所需的时间。有\(k\)个点设有传送门,初始时,传送门都关闭。你从\(1\)号点出发,每当你到达一个已开启传送门的点,那个传送门就会永久开启,你可以从一个开启的传送门花费\(0\)时间到达另一个开启的传送门。求开启所有传送门所需的最小时间,\(1 \le n \le 10^5\)

解题思路

首先,这道题其实可以转化为求 \(k\) 个点的最小生成树。
暴力计算 \(k\) 个点两两之间的最短路会超时,考虑优化。
首先,我们可以求出每条边两端到 \(k\) 个点中的某一点的最短路,这个只需要设一个超级源点,往 \(k\) 个点各连一条边权为 \(0\) 的边,跑最短路即可。
答案中的生成树的连接关键点的边上必包含了一边,该边两端最近的关键点即为所属生成树边的两端。
所以我们有 \(m\) 条边,每条边的代价为 \(dis(i,u)+dis(u,v)+dis(y,j)\) ,跑最小生成树即可。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y,z;
}b[100005];
long long n,m,k,d[100005],start,f[100005],qwe=0,fa[100005],r[100005];
bool v1[100005],v[100005];
queue<long long> l;
vector<long long> a[100005],t[100005];
bool cmp(datay q,datay w)
{
	return q.z<w.z;
}
long long search(long long x)
{
	if(fa[x]!=x)fa[x]=search(fa[x]);
	return fa[x];
}
void dijah()
{
	memset(v,false,sizeof(v)),memset(f,10,sizeof(f)),memset(r,0,sizeof(r));
	l.push(start),f[start]=0,r[start]=0,v[start]=true;
	long long x;
	while(l.size())
	{
		x=l.front(),l.pop(),v[x]=false;
		for(int i=0;i<a[x].size();i++)
		{
			if(f[a[x][i]]>f[x]+t[x][i])
			{
				f[a[x][i]]=f[x]+t[x][i];
				if(r[x])r[a[x][i]]=r[x];
				else r[a[x][i]]=a[x][i];
				if(!v[a[x][i]])v[a[x][i]]=true,l.push(a[x][i]);
			}
		}
	}
	return;
}
int main()
{
	long long x,y,z,s=0;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&x,&y,&z),a[x].push_back(y),a[y].push_back(x),t[x].push_back(z),t[y].push_back(z),b[i].x=x,b[i].y=y,b[i].z=z;
	scanf("%lld",&k);
	for(int i=1;i<=k;i++)scanf("%lld",&d[i]),v1[d[i]]=true,a[0].push_back(d[i]),t[0].push_back(0);
	start=1,dijah(),f[0]=1e16+5;
	for(int i=1;i<=k;i++)if(f[qwe]>f[d[i]])qwe=d[i],s=f[d[i]];
	start=0,dijah();
	for(int i=1;i<=m;i++)b[i].z+=f[b[i].x]+f[b[i].y];
	sort(b+1,b+m+1,cmp);
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		x=search(r[b[i].x]),y=search(r[b[i].y]);
		if(x!=y)fa[y]=x,s+=b[i].z;
	}
	printf("%lld",s);

  return 0;
}

【AT_zone2021_f】 出会いと別れ

题目描述

给出 \(2^n\) 个点,编号从 \(0\)\(2^n\),每个点能向其他点连边的条件为两点编号的异或和不包含于集合 \(S\) 中,找出原图的一个生成树,或输出不存在,\(1 \le n \le 20\)

解题思路

有异或,考虑线性基。
首先,我们对 \(S\) 的补集建一个线性基,表示一个点能异或的值。
若线性基从 \(1\)\(n\) 项中有一项为 \(0\) ,说明会存在两个区域因无法异或该项而不连通,所以不存在。
否则,我们可以尽量向上扩展,对于每个数,设其最高位的 \(0\) 在第 \(x\) 位,直接将该数异或上线性基中第 \(x\) 个即可。

Code

#include<bits/stdc++.h>
using namespace std;
long long n,m,p[1000005],d[1000005]; 
bool v[1000005];
void dijah(long long x)
{
	long long q=x;
	for(int i=20;i>=0;i--)
	{
		if(!(x&(1<<i)))continue;
		if(!p[i])p[i]=x,d[i]=q;
		x^=p[i];
	}
	return;
}
long long gaia(long long x)
{
	for(int i=20;i>=0;i--)if(d[i]&&(!(x&(1<<i))))return d[i];
	return 0;
}
int main()
{
	long long x;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)scanf("%lld",&x),v[x]=true;
	for(int i=0;i<n;i++)if(!v[i])dijah(i);
	for(int i=0;(1<<i)<n;i++)
	{
		if(!p[i])
		{
			printf("-1");
			return 0;
		}
	}
	for(int i=0;i<n-1;i++)printf("%d %d\n",i,i^gaia(i));
  return 0;
}

【AT_abc341_g】Highest Ratio

题目描述

给出 \(n\) 个数 \(a_1...a_n\) ,对于每一位 \(i\) ,求 \(j\) 使得 \(\frac{\sum_{x=i}^j a_x}{n}\) 最大。

解题思路

\(v_i=\sum_{x=1}^i a_x\)
就是找到一个 \(j\) 使得 \(\frac{v_j-v_{i-1}}{j-(i-1)} 最大\)
每个数都看成一个点 \((i,v_i)\) ,就是找一个点 \(j\) 使得 \(i,j\) 连线斜率最大。
直接维护上凸包即可。

Code

#include<bits/stdc++.h>
using namespace std;
long long n,a[200005],d[200005],l;
double f[200005];
double T(long long x,long long y)
{
	return double(a[y]-a[x])/double(y-x);
}
int main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),a[i]+=a[i-1];
	d[++l]=n;
	for(int i=n-1;i>=0;i--)
	{
		while(l>1&&T(i,d[l])<T(d[l],d[l-1]))l--;
		f[i+1]=T(i,d[l]),d[++l]=i;
	}
	for(int i=1;i<=n;i++)printf("%.8lf\n",f[i]);
  return 0;
}

【Luogu P4251】 小凸玩矩阵

题目描述

给出一个 \(n \times m\) 的矩阵,每个点有权值,选出 \(n\) 个行列都不重复的点,使得这些点中第 \(k\) 大的点最小,\(1 \le n \le m \le 250\)

解题思路

看到第 \(k\) 大最小,考虑二分第 \(k\) 大的值,每次只考虑大于二分值 \(x\) 的数,从这些数中能否选出满足题目的 \(k\) 个数,剩下的数乱选。
看到这种数据范围,考虑 \(dp\) 或网络流。
我们建一个二分图,左边代表行,右边代表列,若一个点权值大于等于 \(x\) 就将其所在行与列联通,最后跑最大流检验能否达到 \(k\) 即可。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int to,nex,val;
}a[100005];
int n,m,k,head[505],num=-1,start,endd,now[505],d[505],b[505][505];
queue<int> l;
void add(int x,int y,int z)
{
	a[++num].to=y,a[num].val=z,a[num].nex=head[x],head[x]=num;
	return;
}
void edge(int x,int y,int z)
{
	add(x,y,z),add(y,x,0);
	return;
}
bool bfs()
{
	while(l.size())l.pop();
	memset(now,-1,sizeof(now)),memset(d,-1,sizeof(d));
	now[start]=head[start],d[start]=0,l.push(start);
	int x,u,v;
	while(l.size())
	{
		x=l.front(),l.pop();
		for(int i=head[x];i!=-1;i=a[i].nex)
		{
			u=a[i].to,v=a[i].val;
			if(v>0&&d[u]==-1)d[u]=d[x]+1,now[u]=head[u],l.push(u);
		}
	}
	if(d[endd]==-1)return false;
	return true;
}
int dfs(int x,int y)
{
	if(x==endd)return y;
	int now_y=y,u,v,fl;
	for(;now[x]!=-1;now[x]=a[now[x]].nex)
	{
		if(now_y==0)break;
		u=a[now[x]].to,v=a[now[x]].val;
		if(v>0&&d[u]==d[x]+1)fl=dfs(u,min(now_y,v)),now_y-=fl,a[now[x]].val-=fl,a[now[x]^1].val+=fl;
	}
	return y-now_y;
}
int dinic()
{
	int s=0;
	while(bfs())s+=dfs(start,1e9+5);
	return s;
}
bool check(int x)
{
	memset(head,-1,sizeof(head)),num=-1,start=n+m+1,endd=start+1;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)if(b[i][j]<=x)edge(i,n+j,1);
	} 
	for(int i=1;i<=n;i++)edge(start,i,1);
	for(int i=1;i<=m;i++)edge(n+i,endd,1);
	int p=dinic();
	if(p>=n-k+1)return true;
	return false;
} 
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)scanf("%d",&b[i][j]);
	}
	int l=1,r=1e9+5,mid,s=1e9+5;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(check(mid))r=mid-1,s=min(s,mid);
		else l=mid+1;
	}
	cout<<s;

  return 0;
}

数据结构

【CF1439C】 Greedy Shopping

题目描述

给出一个长度为 \(n\) 的单调不递增序列 \(a\) ,有两种操作:

  • \(1\)\(x\) 小于 \(y\) 的赋值为 \(y\)
  • 从下标 \(x\) 向后访问 \(a\),若 \(a_i\le y\) ,则 \(y--,ans++\),最后输出 \(ans\)

解题思路

首先对于第二个操作,我们可以让 \(y\) 加上 \(\sum_{i=1}^{x-1} a_x\) ,这样就可以视为从 \(1\) 开始。
由于第一个操作与初始序列,\(a\) 保证单调不增,那么每次减连续的一段后 \(y\) 的值必定会变成原来的一半。
那我们就可以考虑线段树二分,循环 \(O(logy)\) 次找连续一段。
或者是在遍历线段树时就求答案,每次查左右子树存不存在小于 \(y\) 的值,并先左后右统计答案。
时间复杂度 \(O(nlog^2n)\)

Code

#include<bits/stdc++.h>
using namespace std;
long long n,m,f[2500005],d[2500005],minn[2500005],s=0;
void galaxy(long long x,long long l,long long r,long long v)
{
	f[x]=(r-l+1)*v,d[x]=v,minn[x]=v;
	return;
}
void pushdown(long long x,long long l,long long r)
{
	if(d[x]==0)return;
	long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	galaxy(lc,l,mid,d[x]),galaxy(rc,mid+1,r,d[x]),d[x]=0;
	return;
}
void dijah(long long x,long long l,long long r,long long ql,long long qr,long long v)
{
	if(ql<=l&&r<=qr)
	{
		galaxy(x,l,r,v);
		return;
	}
	pushdown(x,l,r);
	long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(ql<=mid)dijah(lc,l,mid,ql,qr,v);
	if(qr>mid)dijah(rc,mid+1,r,ql,qr,v);
	f[x]=f[lc]+f[rc],minn[x]=minn[rc];
	return;
}
long long gaia(long long x,long long l,long long r,long long ql,long long qr)
{
	if(ql<=l&&r<=qr)return f[x];
	pushdown(x,l,r);
	long long lc=(x<<1),rc=(x<<1)|1,h=0,mid=(l+r)>>1;
	if(ql<=mid)h=gaia(lc,l,mid,ql,qr);
	if(qr>mid)h+=gaia(rc,mid+1,r,ql,qr);
	return h;
}
long long gaia1(long long x,long long l,long long r,long long ql,long long v)
{
	if(l==r)return (f[x]<=v)?l:-1;
	pushdown(x,l,r);
	long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(ql>mid)return gaia1(rc,mid+1,r,ql,v);
	if(minn[lc]>v)return gaia1(rc,mid+1,r,ql,v);
	return gaia1(lc,l,mid,ql,v);
}
long long gaia2(long long x,long long l,long long r,long long v)
{
	if(f[x]<=v)
	{
		s+=(r-l+1);
		return f[x];
	}
	pushdown(x,l,r);
	long long lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1,v1=v;
	if(minn[lc]<=v)v1-=gaia2(lc,l,mid,v1);
	if(minn[rc]<=v)v1-=gaia2(rc,mid+1,r,v1);
	return v-v1;
}
int main()
{
	long long p,x,y,z,q;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&x),dijah(1,1,n,i,i,x);
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&p,&x,&y);
		if(p==1)
		{
			p=gaia1(1,1,n,1,y);
			if(p<=x&&p!=-1)dijah(1,1,n,p,x,y);
		}
		else
		{
			if(x!=1)z=gaia(1,1,n,1,x-1);
			else z=0;
			s=0,gaia2(1,1,n,y+z);
			printf("%lld\n",s-x+1);
		}
	}
  return 0;
}

【CF703D】Mishka and Interesting sum

题目描述

给定一个长度为 \(n\) 的序列 \(a\) ,给出 \(m\) 个询问操作,每次求区间 \([l,r]\) 内出现次数为偶数第异或和,\(1 \le n,m \le 10^6\)

解题思路

一个区间内的数分为出现次数为偶数或奇数,根据异或的性质,答案即为区间出现过的数的异或和异或上奇数次数异或和。
出现了奇数次数的异或和可用前缀和解决,而区间出现过的数的异或和类似数颜色。
我们只需要参考HH的项链,用树状数组,离线询问按右端点排序,每次在每个值最后的位置上异或上该数,查询只需要查询 \(l\) 后的数的异或和即可。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y,v,p;
}b[1000005];
long long n,m,a[1000005],d[1000005],f[1000005],v[1000005],v1[1000005],last[1000005];
set<long long> l;
map<long long,long long> p;
bool cmp(datay q,datay w)
{
	return q.y<w.y;
}
bool cmp1(datay q,datay w)
{
	return q.p<w.p;
}
long long lowbit(long long x)
{
	return x&(-x);
}
void dijah(long long x,long long y)
{
	if(x==0)return;
	for(int i=x;i<=n+1;i+=lowbit(i))f[i]^=y;
	return;
}
long long gaia(long long x)
{
	long long h=0;
	while(x)h^=f[x],x-=lowbit(x);
	return h;
}
int main()
{
	long long x=1,g=0;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),d[i]=d[i-1]^a[i],l.insert(a[i]);
	scanf("%lld",&m);
	for(int i=1;i<=m;i++)scanf("%lld%lld",&b[i].x,&b[i].y),b[i].p=i,b[i].v=d[b[i].y]^d[b[i].x-1];
	set<long long>::iterator q=l.begin();
	for(;q!=l.end();q++)v[++g]=*q,p[*q]=g;
	for(int i=1;i<=n;i++)a[i]=p[a[i]],last[i]=v1[a[i]],v1[a[i]]=i;
	sort(b+1,b+m+1,cmp);
	for(int i=1;i<=n;i++)
	{
		dijah(n-last[i]+1,v[a[i]]),dijah(n-i+1,v[a[i]]);
		while(x<=m&&b[x].y<=i)b[x].v^=gaia(n-b[x].x+1),x++;
	}
	sort(b+1,b+m+1,cmp1);
	for(int i=1;i<=m;i++)printf("%lld\n",b[i].v);

  return 0;
}

【CF671C】Ultimate Weirdness of an Array

题目描述

给出一个长度为 \(n\) 的序列 \(a\) ,保证 \(a\) 中数两两不相同,\(f(i,j)\) 表示区间 \([1,l-1]\) 与区间 \([r+1,n]\) 中的数取出两个的 \(gcd\) 最大值,求 \(\sum_{i=1}^n \sum_{j=i}^n f(i,j)\)\(1 \le n \le 2 \times 10^5,1 \le a_i \le 2 \times 10^5\)

解题思路

直接做不好做,观察到 \(a_i\) 不大,我们考虑枚举答案并统计每个答案的区间个数。
直接统计也不好做,观察一下,发现对于一个固定的 \(l\) ,随着 \(r\) 的增大,答案也会单调不增。
我们可以这样想:枚举答案时随着枚举答案的增大,对于同一个 \(l\) ,满足条件的区间的右端点 \(r\) 相对于上一次也会逐渐往左扩展,我们只需要求出对于每一个 \(l\) 它的右端点 \(r\) 变化了多少位即可。
于是,我们可以这么做:设数组 \(t\) 的每一位 \(i\) 表示能满足 \(f(i,r) \le v\) 的最小 \(r\)\(v\) 表示当前枚举到的答案,从大往小枚举答案,每次改变 \(t\) 数组,设改变前的 \(t\) 数组的和为 \(x\) ,改变后 \(t\) 数组的和为 \(y\) ,则满足 \(f(l,r)=v\)的 区间个数 \(y-x\) ,累加即可。
考虑 \(t\) 数组的变化规律,由于从大到小枚举答案,\(t\) 数组每一项 \(i\) 初始化为 \(i\) ,并且我们可以发现 \(t\) 数组是单调不减的。
设当前枚举到的答案为 \(v\) ,预处理出序列 \(a\) 中所有为 \(v\) 倍数的数的位置,那么一个满足条件的区间必须满足所选区间的区间中至多只有一个数为 \(v\) 的倍数,我们设为 \(v\) 倍数的数分别在 \(l_1,l_2,...,l_m\) ,对于每一项 \(x\) ,做以下分类讨论。

  • \(l_2 < x\)
    由于此时已经有两个数为 \(v\) 的倍数,直接把 \(t_x\) 设为 \(n+1\) 不可行即可。
  • \(l_1<x\le l_2\)
    左边只有一个数为 \(v\) 的倍数,右边就不能有,令 \(t_x=max(t_x,x_m)\)
  • \(x \le l_1\)
    左边可以有一个,令 \(t_x=max(t_x,x_{m-1})\)

操作看起来涉及到了区间取 \(max\) ,要用吉司机线段树,但根据 \(t\) 单调不减的性质,用线段树二分即可维护。
所以,我们只需要先预处理出对于每一个 \(v\)\(x_1,x_2,x_{m-1},x_m\) ,然后从大到小枚举答案,维护 \(t\) 数组,在线段树上做区间修改,线段树二分和区间求和即可。
时间复杂度 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n,a[200005];
long long f[800005];
int minn[800005],d[800005];
vector<int> t[200005];
void galaxy(int x,int l,int r,long long v)
{
	f[x]=(r-l+1)*v,minn[x]=v,d[x]=v;
	return;
}
void up(int x)
{
	f[x]=f[x<<1]+f[(x<<1)|1],minn[x]=min(minn[x<<1],minn[(x<<1)|1]);
	return;
}
void pushdown(int x,int l,int r)
{
	if(d[x]==-1)return;
	int mid=(l+r)>>1,lc=(x<<1),rc=(x<<1)|1;
	galaxy(lc,l,mid,d[x]),galaxy(rc,mid+1,r,d[x]),d[x]=-1;
	return;
}
void dijah(int x,int l,int r,int ql,int qr,int v)
{
	if(ql<=l&&r<=qr)
	{
		galaxy(x,l,r,v);
		return;
	}
	pushdown(x,l,r);
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(ql<=mid)dijah(lc,l,mid,ql,qr,v);
	if(qr>mid)dijah(rc,mid+1,r,ql,qr,v);
	up(x);
	return;
}
int gaia(int x,int l,int r,int v)
{
	if(l==r)return l;
	pushdown(x,l,r);
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(minn[rc]>=v)return gaia(lc,l,mid,v);
	return gaia(rc,mid+1,r,v);
}
void add(int x,int y)
{
	for(int i=1;i<=sqrt(x);i++)
	{
		if(x%i==0)
		{
			t[i].push_back(y);
			if(i*i!=x)t[x/i].push_back(y);
		}
	}
	return;
}
int main()
{
	long long s=0;
	memset(d,-1,sizeof(d));
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),add(a[i],i);
	for(int i=1;i<=n;i++)dijah(1,1,n,i,i,i);
	for(int i=200000;i>=1;i--)
	{
		if(t[i].size()<=1)continue;
		long long p=f[1];
		int x;
		if(t[i][1]!=n)dijah(1,1,n,t[i][1]+1,n,n+1);
		x=gaia(1,1,n,t[i][t[i].size()-1]);
		if(x>t[i][0])dijah(1,1,n,t[i][0]+1,min(x,t[i][1]),t[i][t[i].size()-1]);
		x=gaia(1,1,n,t[i][t[i].size()-2]);
		if(minn[1]<t[i][t[i].size()-2])dijah(1,1,n,1,min(x,t[i][0]),t[i][t[i].size()-2]);
		s+=(long long)(f[1]-p)*i;
	}
	cout<<s;

  return 0;
}

【Luogu P7521】 取模

题目描述

给出 \(n\) 个数,求最大的 \((a_i+a_j)\%a_k\) ,其中 \(i \ne j ,j \ne k,i \ne k\) ,$ 3\le n \le 2\times 10^5$

解题思路

枚举 \(k\) ,将所有 \(a_i\) 都模上 \(a_k\) ,有两种情况。

  • \(a_i+a_j \ge a_k\)
    这种情况相当于取模后最大的两个值。
  • \(a_i+a_j <a_k\)
    这个可以用双指针做,时间复杂度 \(O(n)\)

这样的话时间复杂度就压到了 \(O(n^2logn)\) ,但还是不够,考虑优化。
首先我们可以跳过重复的模数,但看起来用处不大。
一个很重要的优化,当当前 \(a_k \le ans-1\) 时,就可以退出了。
正确性显然,而它能让时间复杂度骤降到 \(O(nlog^2n)\)
这样就能过了。

Code

#include<bits/stdc++.h>
using namespace std;
long long n,a[200005],b[200005],s;
int main()
{
	long long q=1;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	sort(a+1,a+n+1);
	for(int i=n;i>=1;i--)
	{
		for(int j=1;j<=n;j++)b[j]=a[j]%a[i];
		sort(b+1,b+n+1),s=max(s,(b[n-1]+b[n])%a[i]),q=n;
		for(int j=2;j<=n;j++)
		{
			while(b[q]+b[j]>=a[i]&&q>2)q--;
			if(q>1)s=max(s,(b[q]+b[j])%a[i]);
		}
		if(s>=a[i]-1)break;
	}
	cout<<s;
  return 0;
}

【CF1093G】Multidimensional Queries

题目描述

\(n\)\(k\) 维的点 \(a_{1...n}\) ,有两种操作:修改某个点的位置,或查询一段区间 \([l,r]\) 内的点最大的曼哈顿的距离,\(1 \le n \le 2 \times 10^5,1 \le k \le 5\)

解题思路

出现了绝对值,考虑将绝对值拆开。
我们知道一个东西,\(|x-y|=ma(x-y,y-x)\) ,据此,我们可以有 \(|x_1-x_2|+|y_1-y_2|=max(x_1+y_1-x_2-y_2,x_1+y_2-x_2-y_1,x_2+y_1-x_1-y_2,x_2+y_2-x_1-y_1)\)
扩展到 \(k\) 维就是 \(2^k\)\(max\)
据此我们可以建 \(2^k\) 棵线段树,第 \(0\) 棵线段树表示 \(-a_1-...-a_k\) ,第一棵线段树表示 \(-a_1-...+a_k ...\)
每次查询只需从 \(0\) 枚举到 \(2^k-1\) ,查询每种情况对应线段树的最大值累加求最大即可。
时间复杂度 \(nlogn2^k\)

Code

#include<bits/stdc++.h>
using namespace std;
int f[32][800005],n,m,k,a[800005][6];
void dijah(int p,int x,int l,int r,int k,int v)
{
	if(l==r)
	{
		f[p][x]=v;
		return;
	}
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(k<=mid)dijah(p,lc,l,mid,k,v);
	else dijah(p,rc,mid+1,r,k,v);
	f[p][x]=max(f[p][lc],f[p][rc]);
	return;
}
int gaia(int p,int x,int l,int r,int ql,int qr)
{
	if(ql<=l&&r<=qr)return f[p][x];
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1,h=-1e9-5;
	if(ql<=mid)h=max(h,gaia(p,lc,l,mid,ql,qr));
	if(qr>mid)h=max(h,gaia(p,rc,mid+1,r,ql,qr));
	return h;
}
int main()
{
	memset(f,-2,sizeof(f));
	scanf("%d%d",&n,&m);
	int p=(1<<m),x=0,y,op,s=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
		for(int j=0;j<p;j++)
		{
			x=0;
			for(int u=1;u<=m;u++)x+=((j&(1<<(u-1)))?1:(-1))*a[i][u]; 
			dijah(j,1,1,n,i,x);
		}
	}
	scanf("%d",&k);
	for(int i=1;i<=k;i++)
	{
		scanf("%d",&op);
		if(op==2)
		{
			s=-1e9;
			scanf("%d%d",&x,&y);
			for(int j=0;j<p;j++)s=max(s,gaia(j,1,1,n,x,y)+gaia(p-j-1,1,1,n,x,y));
			printf("%d\n",s);
		}
		else
		{
			scanf("%d",&x);
			for(int j=1;j<=m;j++)scanf("%d",&a[x][j]);
			for(int j=0;j<p;j++)
			{
				s=0;
				for(int u=1;u<=m;u++)s+=((j&(1<<(u-1)))?1:(-1))*a[x][u];
				dijah(j,1,1,n,x,s);
			}
		}
	}

  return 0;
}

动态规划

【CF348D】 Turtles

题目描述

给一个 \(n \times m\) 的网格,有些格子有障碍物,现在有两只乌龟从 \((1,1)\) 走到 \((n,m)\) ,要求出起点与终点外两只乌龟不能撞在一起,每只乌龟只能向下或向右走,求有多少种方案,\(1 \le n,m \le 3000\)

解题思路

考虑容斥,求出 $(1,2) 到 \((n-1,m)\)\((2,1)\)\((n,m-1)\) 的路径条数乘积,考虑减去两只乌龟撞在一起的方案。
手推一下,发现若将撞在一起后两只乌龟调换一下,就变成了一个从 \((1,2)\)\((n,m-1)\) ,另一个从 \((2,1)\)\((n-1,m)\) ,求出他们的路径条数乘积就是不合法的方案数。
求解路径条数用 \(dp\) ,时间复杂度 \(O(n^2)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,m,a[3005][3005];
int f[3005][3005];
char c;
int read()
{
	c=getchar();
	while(c!='.'&&c!='#')c=getchar();
	return (c=='.')?0:1;
}
long long dijah(long long stx,long long sty,long long ex,long long ey)
{
	memset(f,0,sizeof(f));
	if(!a[stx][sty])f[stx][sty]=1;
	for(int i=stx;i<=ex;i++)
	{
		for(int j=sty;j<=ey;j++)if((!a[i][j])&&(i!=stx||j!=sty))f[i][j]=(f[i-1][j]+f[i][j-1])%mod;
	}
	return f[ex][ey];
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)a[i][j]=read();
	}
	printf("%lld",((dijah(2,1,n,m-1)*dijah(1,2,n-1,m)-dijah(2,1,n-1,m)*dijah(1,2,n,m-1))%mod+mod)%mod);

  return 0;
}

【CF958C】 Encryption

题目描述

有一个包含 \(n\) 个正整数的数列,将这个数列分成 \(k\) 个非空段,每段的价值为这段的所有数总和 \(mod\) \(p\),你需要使 \(k\) 段价值总和最小,请求出这个最小值,\(1 \le n \le 5 \times 10^5, 2\le k \le 100,2 \le p \le 100\)

解题思路

首先,无论答案 \(ans\) 是什么,\(ans \% p=(\sum_{i=1}^n a_i) \% p\) ,对于每一位也同理。
我们设 \(f_{i,j}\) 表示做到 \(i\) ,割了 \(j\) 段得到的最小值,对于任意两个决策点 \(x,y\),若 \(x\)\(y\) 优,那么 \(f_{x,j-1}+(\sum_{u=x+1}^i a_u) \% p+p \le f_{y,j-1}+(\sum_{u=y+1}^i a_u) \% p\)
后面无论怎么加 ,\(y\) 都会不比 \(x\) 优。
那么我们每次计算直接从前一位的最优决策点转移而来,与前一位比较哪个更优,作为当前这位的决策点即可。
时间复杂度 \(O(nm)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,p,a[500005],f[500005][105],minn[105];
int main()
{
	int x;
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1;i<=n;i++)scanf("%d",&x),a[i]=(a[i-1]+x)%p;
	memset(f,1,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)f[i][j]=f[minn[j-1]][j-1]+(a[i]-a[minn[j-1]]+p)%p;
		for(int j=1;j<=m;j++)minn[j]=(f[i][j]<f[minn[j]][j])?i:minn[j];
	}
	printf("%d",f[n][m]);
  return 0;
}

最大环

题目描述

给出平面上的 \(n\) 个点,每个点 \(i\) 的坐标为 \((x_i,y_i)\) ,两个点之间的距离为两点之间的曼哈顿距离,求一个含 \(k\) 个点的最大环,\(1 \le n \le 1000,1 \le k \le 6\)

解题思路

我们先设选的 \(k\) 个点编号是单调递增的,考虑如何选。
利用上面的技巧,我们可以用 \(dp\) 来做,对于是个环的问题,我们只需要细化状态,存储一下从最后一个点到第一个点是第一个点所贡献的类型即可。
但事实上编号可能不单增,我们可以这样做:不断重排序列,每次都做一次 \(dp\)
大概重排 \(1000\) 次就大概率能过了。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int x,y;
}a[1005];
int n,k;
int f[1005][7][4][4],d[1005][4];
int dijah()
{
	for(int i=1;i<=n;i++)d[i][0]=-a[i].x-a[i].y,d[i][1]=-a[i].x+a[i].y,d[i][2]=a[i].x-a[i].y,d[i][3]=a[i].x+a[i].y;
	int s=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<4;j++)
		{
			for(int u=0;u<4;u++)f[i][1][j][u]=d[i][j]+d[i][u];
		}
		for(int j=2;j<=k;j++)
		{
			for(int u=0;u<4;u++)
			{
				for(int p=0;p<4;p++)
				{
					f[i][j][u][p]=-2e9-5;
					for(int q=0;q<4;q++)f[i][j][u][p]=max(f[i][j][u][p],f[i-1][j-1][q][p]+d[i][3-q]+d[i][u]);
					if(j==k)s=max(s,f[i][j][u][p]-d[i][u]+d[i][3-p]);
					f[i][j][u][p]=max(f[i-1][j][u][p],f[i][j][u][p]);
				}
			}
		}
	}
	return s;
}
void change()
{
	int x,y,p=n+n/2;
	for(int i=1;i<=p;i++)
	{
		x=rand()%n+1,y=x;
		while(y==x)y=rand()%n+1;
		swap(a[x],a[y]);
	}
	return;
}
void poi()
{
	int s=0;
	scanf("%d%d",&n,&k);
	int p=1000;
	p=min((long long)(p),(long long)(n)*n);
	if(n<=200)p*=2;
	if(n<=80)p*=2;
	if(n<=60)p*=3;
	for(int i=1;i<=n;i++)scanf("%d",&a[i].x);
	for(int i=1;i<=n;i++)scanf("%d",&a[i].y);
	for(int i=1;i<=p;i++)s=max(s,dijah()),change();
	printf("%d\n",s);
	return;
}
int main()
{
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=6;j++)
		{
			for(int u=0;u<4;u++)
			{
				for(int q=0;q<4;q++)f[i][j][u][q]=-2e9-5;
			}
		}
	}
	srand(time(0));
	int qwe;
	scanf("%d",&qwe);
	for(int i=1;i<=qwe;i++)poi();

  return 0;
}
posted @ 2024-04-11 22:12  dijah  阅读(13)  评论(0编辑  收藏  举报