2026年7月短专题训练题解

图论

[国家集训队] 飞飞侠

曼哈顿距离转切比雪夫距离。线段树套线段树优化建图跑 Dijkstra。切比雪夫距离平移的时候有点细节。负奇数 \(\%2=-1\) 而不是 \(1\)。时间复杂度 \(O(N^2 \log^2N)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN=1005,MAXM=155*155*155*2;
struct Edge
{
	int x,y,Len,Next;
}e[MAXM];
int elast[MAXM],tot;
void Add(int x,int y,int Len)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Len=Len;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
int Dis[MAXM];
bool vis[MAXM];
struct Dot
{
	int u,Dis;
	Dot(){}
	Dot(int _u,int _Dis)
	{
		u=_u;
		Dis=_Dis;
	}
};
bool operator >(Dot a,Dot b)
{
	return a.Dis>b.Dis;
}
int Cnt;
void Dijkstra(int Begin)
{
	for(int i=1;i<=Cnt;i++)
	{
		vis[i]=false;
		Dis[i]=1e9;
	}	
	priority_queue<Dot,vector<Dot>,greater<Dot> >q;
	q.push(Dot(Begin,0));
	Dis[Begin]=0;
	while(!q.empty())
	{
		int u=q.top().u;
		q.pop();
		if(vis[u])
		continue;
		vis[u]=true;
		for(int i=elast[u];i;i=e[i].Next)
		{
			int v=e[i].y;
			if(Dis[v]>Dis[u]+e[i].Len)
			{
				Dis[v]=Dis[u]+e[i].Len;
				q.push(Dot(v,Dis[v]));
			}
		}
	}
}
int N,M;
int Root[MAXN<<2];
int N1,M1;
int GetId(int x,int y)
{
	y-=M1;
	int X=(x+y)/2,Y=(x-y)/2;
	return (X-1)*M1+Y;
}
bool flag[MAXN][MAXN];
bool Can(int x,int y)
{
	if(x<0||y<0)
	return false;
	return flag[x][y];
}
int ls[MAXM],rs[MAXM];
void Build2(int &u,int l,int r,int Hl,int Hr)
{
	if(u==0)
	{
		u=++Cnt;
	}
	if(l==r)
	{
		for(int i=Hl;i<=Hr;i++)
		{
			if(Can(i,l))
			{
				
				Add(u,GetId(i,l),0);		
			}
		}
		return;
	}
	int Mid=l+r>>1;
	Build2(ls[u],l,Mid,Hl,Hr);
	Build2(rs[u],Mid+1,r,Hl,Hr);
	Add(u,ls[u],0);
	Add(u,rs[u],0);
}
int Max;
void Build(int u,int l,int r)
{
	Build2(Root[u],1,M,l,r);
	if(l==r)
	{
		return;
	}
	int Mid=l+r>>1;
	Build(u<<1,l,Mid);
	Build(u<<1|1,Mid+1,r);
}
void Modify2(int u,int l,int r,int x,int y,int U,int Val)
{
	if(x<=l&&r<=y)
	{
		Add(U,u,Val);
		return;
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Modify2(ls[u],l,Mid,x,y,U,Val);
	if(y>=Mid+1)
	Modify2(rs[u],Mid+1,r,x,y,U,Val);
}
void Modify(int u,int l,int r,int x,int y,int X,int Y,int U,int Val)
{
	if(x<=l&&r<=y)
	{
		Modify2(Root[u],1,M,X,Y,U,Val);
		return;
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Modify(u<<1,l,Mid,x,y,X,Y,U,Val);
	if(y>=Mid+1)
	Modify(u<<1|1,Mid+1,r,x,y,X,Y,U,Val);
}
int B[MAXN][MAXN],A[MAXN][MAXN];
ll a[MAXN][MAXN],b[MAXN][MAXN],c[MAXN][MAXN];
const int INF=1e9;
int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M;
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<=M;j++)
		{
			flag[i+j][i-j+M]=true;
		}
	}
	N1=N,M1=M;
	N=N1+M1;
	M=N1+M1-1;
	Cnt=N1*M1;
	Build(1,1,N);
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			cin>>B[i][j];	
		}
	}
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			cin>>A[i][j];
		}
	}
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			int x=i+j,y=i-j+M1;
			Modify(1,1,N,max(1,x-B[i][j]),min(N,x+B[i][j]),max(1,y-B[i][j]),min(M,y+B[i][j]),GetId(x,y),A[i][j]);
		}
	}
	int X1,Y1,X2,Y2,X3,Y3;
	cin>>X1>>Y1;
	Dijkstra((X1-1)*M1+Y1);
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			a[i][j]=Dis[(i-1)*M1+j];
		}
	}
	cin>>X2>>Y2;
	Dijkstra((X2-1)*M1+Y2);
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			b[i][j]=Dis[(i-1)*M1+j];
		}
	}
	cin>>X3>>Y3;
	Dijkstra((X3-1)*M1+Y3);
	for(int i=1;i<=N1;i++)
	{
		for(int j=1;j<=M1;j++)
		{
			c[i][j]=Dis[(i-1)*M1+j];
		}
	}
	long long Min=min(a[X1][Y1]+b[X1][Y1]+c[X1][Y1],min(a[X2][Y2]+b[X2][Y2]+c[X2][Y2],a[X3][Y3]+b[X3][Y3]+c[X3][Y3]));
	if(Min>=INF)
	{
		cout<<"NO";
		return 0;
	}
	if(a[X1][Y1]+b[X1][Y1]+c[X1][Y1]==Min)
	{
		cout<<"X"<<'\n'<<Min;
		return 0;
	}
	if(a[X2][Y2]+b[X2][Y2]+c[X2][Y2]==Min)
	{
		cout<<"Y"<<'\n'<<Min;
		return 0;
	}
	if(a[X3][Y3]+b[X3][Y3]+c[X3][Y3]==Min)
	{
		cout<<"Z"<<'\n'<<Min;
		return 0;
	}
}

[BalticOI 2005] Bus Trip (Day2)

题意描述有点不清。其实想表达的是一辆车占用的时间段是 \([a_i,d_i]\),但是坐在车上的时间是 \([b_i,c_i]\)。因为最后要求的其实是理论最大的总等待时间的最小值。

把每条线路视作一个点,如果两条线路满足 \(d_i<a_j\)\(t_i=s_j\)\(i\)\(j\) 连边,边权为 \(c_i-b_i\),然后找最长路,这个图是 DAG,所以可以直接跑拓扑排序找最长路。但是这样做复杂度太高了,考虑前缀和优化建图,对于每个城镇,记录有哪些线路的终点在这里,哪些线路的起点在这里,把起点按照起始时间从小到大排序,对于起点进行前缀和优化建图,每次连边就二分找到第一个可以连边的位置,然后一段后缀都是可行的,只需要连一条边即可,然后拓扑排序上 dp。时间复杂度 \(O(n \log n)\)

值得注意的是拓扑排序总是要把所有入度为 \(0\) 的点加入队列,哪怕它可能没什么实际意义。具体的在写代码的时候就知道了。

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

const int MAXN=1e6+50;
const ll INF=1e18;
int N,M,P,T;

struct Edge
{
	int x,y,Len,Next;
}e[MAXN<<1];
int elast[MAXN],tot;
int Deg[MAXN];
void Add(int x,int y,int Len)
{
	Deg[y]++;
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Len=Len;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
int s[MAXN],t[MAXN],a[MAXN],b[MAXN],c[MAXN],d[MAXN];
vector<int>In[MAXN],Out[MAXN];
bool cmp(int x,int y)
{
	return a[x]<a[y];
}
int Cnt;
vector<int>Pos[MAXN];
ll f[MAXN];
bool Inq[MAXN];
int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M>>P>>T;
	Cnt=M;
	for(int i=1;i<=M;i++)
	{
		cin>>s[i]>>t[i]>>a[i]>>b[i]>>c[i]>>d[i];
		In[t[i]].push_back(i);
		Out[s[i]].push_back(i);
		Cnt++;
		Pos[s[i]].push_back(Cnt);
	}
	for(int i=1;i<=N;i++)
	{
		sort(Out[i].begin(),Out[i].end(),cmp);
		for(int j=0;j<Out[i].size();j++)
		{
			Add(Pos[i][j],Out[i][j],c[Out[i][j]]-b[Out[i][j]]);
			if(j+1<Out[i].size())
			Add(Pos[i][j],Pos[i][j+1],0);
		}
	}
	for(int i=1;i<=N;i++)
	{
		for(int j=0;j<In[i].size();j++)
		{
			int u=In[i][j];
			int End=d[u];
			int l=0,r=((int)Out[i].size())-1,Mid,ans=-1;
			while(l<=r)
			{
				Mid=l+r>>1;
				if(a[Out[i][Mid]]>=End)
				{
					ans=Mid;
					r=Mid-1;
				}
				else
				{
					l=Mid+1;
				}
			}
			if(ans!=-1)
			{
				Add(u,Pos[i][ans],0);
			}
		}
	}
	if(Pos[1].size()==0)
	{
		if(P==1)
		cout<<T;
		else
		cout<<-1;
		return 0;
	}
	queue<int>q;
	for(int i=1;i<=Cnt;i++)
	{
		if(Deg[i]==0)
		{
			q.push(i);
		}
		f[i]=-1;
	}
	f[Pos[1][0]]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=elast[u];i;i=e[i].Next)
		{
			int v=e[i].y;
			if(f[u]!=-1)
			f[v]=max(f[v],f[u]+e[i].Len);
			Deg[v]--;
			if(Deg[v]==0)
			{
				q.push(v);
			}
		}
	}
	ll ans=-1;
	if(P==1)
	ans=0;
	for(int i=1;i<=M;i++)
	{
		if(t[i]==P&&d[i]<=T&&f[i]!=-1)
		{
			ans=max(ans,f[i]);
		}
	}
	if(ans==-1)
	cout<<-1;
	else
	{
		cout<<T-ans;
	}
}

[POI 2007] BIU-Offices

比较明显地可以看出来是求补图联通块个数。我的做法不优,考虑线段树优化并查集。考虑补图上,给出了 \(M\) 条不存在的边,则并查集合并一定是 \(O(M)\) 次单点和一段区间的合并,用线段树优化这个合并过程,复杂度 \(O(M\log^2 N)\)。我并查集没写按秩合并所以复杂度不是反阿克曼函数,或许写了也不会出问题?

本题有更优秀的做法。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=2e6+50;
int N,M;
int father[MAXN];
int getfather(int x)
{
	if(x!=father[x])
	father[x]=getfather(father[x]);
	return father[x];
}
vector<int>e[MAXN];
int Root;
int Cnt;
int ls[MAXN<<2],rs[MAXN<<2];
void Build(int &u,int l,int r)
{
	if(u==0)
	{
		u=++Cnt;
	}
	if(l==r)
	{
		return;
	}
	int Mid=l+r>>1;
	Build(ls[u],l,Mid);
	Build(rs[u],Mid+1,r);
}
void Merge(int u,int l,int r,int x,int y,int o)
{
	if(x<=l&&r<=y)
	{
		father[getfather(o)]=getfather(u);
		return;
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Merge(ls[u],l,Mid,x,y,o);
	if(y>=Mid+1)
	Merge(rs[u],Mid+1,r,x,y,o);
}
int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M;
	Cnt=N;
	Build(Root,1,N);
	for(int i=1;i<=Cnt;i++)
	{
		father[i]=i;
	}
	for(int i=1;i<=M;i++)
	{
		int a,b;
		cin>>a>>b;
		e[a].push_back(b);
		e[b].push_back(a);
	}
	for(int i=1;i<=N;i++)
	{
		e[i].push_back(0);
		e[i].push_back(N+1);
		sort(e[i].begin(),e[i].end());
		int l,r;
		for(int j=1;j<e[i].size();j++)
		{
			l=e[i][j-1]+1;
			r=e[i][j]-1;
			if(l<=r)
			{
				Merge(Root,1,N,l,r,i);
			}
		}
	}
	map<int,int>vis;
	int ans=0;
	vector<int>Vec;
	for(int i=1;i<=N;i++)
	{
		if(vis.count(getfather(i))==false)
		{
			vis[getfather(i)]=Vec.size();
			Vec.push_back(0);
			ans++;
		}
		Vec[vis[getfather(i)]]++;
	}
	cout<<ans<<'\n';
	sort(Vec.begin(),Vec.end());
	for(int i=0;i<Vec.size();i++)
	{
		cout<<Vec[i]<<" ";
	}
}

[POI 2007] ATR-Tourist Attractions

dp 部分不难,令 \(f_{i,S}\) 表示已经在 \(S\) 集合内的点停留,当前停留在 \(i\) 的最短路,需要预处理一下停留在每个点需要预先停留的点集,然后能 \(O(N)\) 判断一个集合 \(S\) 是否合法。

然而这题卡空间,容易发现状态是冗余的,因为 \(f_{i,S}\)\(i\) 一定在 \(S\) 里,所以可以少用一位,这样空间就够了,实现起来有一定的细节。要预处理 \(1\)\(K+1\) 的单源最短路。复杂度 \(O(KM\log M+K^{2}2^{K})\)。空间复杂度 \(O(KN+K2^{K-1})\).

点击查看代码
#include <bits/stdc++.h>
using namespace std;
static char buf[1000000],*p1=buf,*p2=buf,obuf[1000000],*p3=obuf;
#define getchar() p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++
template<typename item>
inline void read(register item &x)
{
	bool flag=false;
    x=0;register char c=getchar();
    while(c<'0'||c>'9')
	{
		if(c=='-')
		flag=true;
		c=getchar();
	}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    if(flag)
    x=-x;
}
const int MAXM=2e5+50,MAXN=2e4+50,INF=2e9;
struct Edge
{
	int x,y,Len,Next;
}e[MAXM<<1];
int elast[MAXN],tot;
void Add(int x,int y,int Len)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Len=Len;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
int N,M,K,G;
int Need[25];
int f[20][(1<<19)|10];
int NowDis[MAXN];
bool vis[MAXN];
struct Dot
{
	int u,Dis;
	Dot(){}
	Dot(int _u,int _Dis)
	{
		u=_u;
		Dis=_Dis;
	}
};
bool operator >(Dot a,Dot b)
{
	return a.Dis>b.Dis;
}
void Dijkstra(int Root)
{
	for(int i=1;i<=N;i++)
	{
		vis[i]=false;
		NowDis[i]=INF;
	}
	priority_queue<Dot,vector<Dot>,greater<Dot> >q;
	q.push(Dot(Root,0));
	NowDis[Root]=0;
	while(!q.empty())
	{
		int u=q.top().u;
		q.pop();
		if(vis[u])
		continue;
		vis[u]=true;
		for(int i=elast[u];i;i=e[i].Next)
		{
			int v=e[i].y;
			if(NowDis[v]>NowDis[u]+e[i].Len)
			{
				NowDis[v]=NowDis[u]+e[i].Len;
				q.push(Dot(v,NowDis[v]));
			}
		}
	}
}
vector<int>E[25];
int Deg[25];
int Dis[23][MAXN];
int Del(int S,int x)
{
	return ((S>>x+1)<<x)|(((1<<x)-1)&S);
}
int Insert(int S,int x)
{
	return ((S>>x)<<x+1)|(((1<<x)-1)&S);
}
int main()
{
	read(N);
	read(M);
	read(K);
	for(int i=1;i<=M;i++)
	{
		int x,y,Len;
		read(x);
		read(y);
		read(Len);
		Add(x,y,Len);
		Add(y,x,Len);
	}
	for(int i=1;i<=K+1;i++)
	{
		Dijkstra(i);
		for(int j=1;j<=N;j++)
		{
			Dis[i][j]=NowDis[j];
		}
	}
	read(G);
	for(int i=1;i<=G;i++)
	{
		int x,y;
		read(x);
		read(y);
		Deg[y]++;
		E[x].push_back(y);
	}
	for(int i=0;i<K;i++)
	{
		for(int j=0;j<(1<<K-1);j++)
		{
			f[i][j]=INF;
		}
	}
	queue<int>q;
	for(int i=2;i<=K+1;i++)
	{
		if(Deg[i]==0)
		{
			q.push(i);
			Need[i]=0;
			f[i-2][0]=Dis[1][i];
		}
	}
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<E[u].size();i++)
		{
			int v=E[u][i];
			Deg[v]--;
			Need[v]|=(Need[u]|(1<<u-2));
			if(Deg[v]==0)
			{
				q.push(v);
			}
		}
	}
	for(int _S=1;_S<(1<<K-1);_S++)
	{
		for(int i=2;i<=K+1;i++)
		{
			int S=Insert(_S,i-2);
			int Now=Need[i]; 
			for(int j=0;j<K;j++)
			{
				if((S>>j)&1)
				{
					Now|=Need[j+2];
				}
			}
			if((Now|S)!=S)
			{
				continue;
			}
			for(int j=0;j<K;j++)
			{
				if((S>>j)&1)
				{
					f[i-2][_S]=min(f[i-2][_S],f[j][Del(S,j)]+Dis[j+2][i]);
				}
			}
		}
	}
	int ans=INF;
	if(K==0)
	{
		ans=Dis[1][N];
	}
	else
	{
		for(int i=0;i<K;i++)
		{
			ans=min(ans,f[i][(1<<K-1)-1]+Dis[i+2][N]);
		}	
	}
	cout<<ans;
}

[CERC2013] Subway

本题输出需要注意 line 和 lines。输入输出没有看起来那么恶心。题面描述比较模糊,事实上是先考虑次数最少,再考虑时间最长。以及同个线路掉头次数会增加(所以并不会出现掉头的情况)。

考虑按线路 bfs,得到的是最少次数,然后记录每条线路的每个站,到达该站最长时间。也就是一个站如果同时在多个线路上,每个线路上的最长时间是可能不一样的。对于时间数组,bfs 到某个线路的时候从左往右更新一遍再从右往左更新一遍即可。时间复杂度 \(O(N)\)\(N\) 为点数。(事实上如果很多条线路同时经过同一个点复杂度会被卡成 \(O(N^2)\),但是经过精细实现可以避免这一点,我没有实现。)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+50,INF=1e9;
struct Dot
{
	int Id,Pos;
	Dot(){}
	Dot(int _Id,int _Pos)
	{
		Id=_Id;
		Pos=_Pos;
	}
};
int N,M;
int Ci[MAXN];
vector<int>Vec[MAXN];
vector<int>Dis[MAXN];
int L[MAXN],R[MAXN];
void Check(int Id)
{
	L[0]=Dis[Id][0];
	for(int i=1;i<Vec[Id].size();i++)
	{
		L[i]=max(L[i-1]+1,Dis[Id][i]);
	}
	R[(int)Vec[Id].size()-1]=Dis[Id].back();
	for(int i=((int)Vec[Id].size())-2;i>=0;i--)
	{
		R[i]=max(R[i+1]+1,Dis[Id][i]);
	}
	for(int i=0;i<Vec[Id].size();i++)
	{
		Dis[Id][i]=max(L[i],R[i]);
	}
	
}
vector<Dot>Group[MAXN];
bool vis[MAXN];
map<string,int>Mp1,Mp2;
void Solve()
{
	N=M=0;
	string t;
	cin>>t;
	while(true)
	{
		cin>>t;
		if(t.back()!=',')
		{
			Mp1[t]=++N;
			break;
		}
		else
		{
			t.pop_back();
			Mp1[t]=++N;
		}
	}
	cin>>t;
	while(true)
	{
		cin>>t;
		if(t.back()!=',')
		{
			Mp2[t]=++M;
			break;
		}
		else
		{
			t.pop_back();
			Mp2[t]=++M;
		}
	}
	for(int i=1;i<=M;i++)
	{
		cin>>t;
		int Id=Mp2[t];
		cin>>t;
		int tt=0;
		while(true)
		{
			cin>>t;
			
			if(t.back()!=',')
			{
				Vec[Id].push_back(Mp1[t]);
				Group[Mp1[t]].push_back(Dot(Id,tt));
				Dis[Id].push_back(-INF);
				break;
			}
			else
			{
				t.pop_back();
				Vec[Id].push_back(Mp1[t]);
				Group[Mp1[t]].push_back(Dot(Id,tt));
				Dis[Id].push_back(-INF);
			}
			tt++;	
		}
	}
	int Begin,End;
	string S,T;
	cin>>t;
	cin>>t;
	cin>>t;
	cin>>t;
	S=t;
	Begin=Mp1[t];
	cin>>t;
	cin>>t;
	cin>>t;
	cin>>t;
	T=t;
	End=Mp1[t];
	for(int i=1;i<=M;i++)
	{
		vis[i]=false;
	}
	queue<int>q;
	for(int i=0;i<Group[Begin].size();i++)
	{
		Dot Now=Group[Begin][i];
		q.push(Now.Id);
		Ci[Now.Id]=1;
		vis[Now.Id]=true;
		Dis[Now.Id][Now.Pos]=0;	
	}
	int Min=INF;
	while(!q.empty())
	{
		int u=q.front();
		if(Ci[u]>Min)
		break;
		q.pop();
		Check(u);
		
		for(int i=0;i<Vec[u].size();i++)
		{
			int v=Vec[u][i];
			if(v==End)
			{
				Min=Ci[u];
			}
			for(int j=0;j<Group[v].size();j++)
			{
				Dot Now=Group[v][j];
				
				if(vis[Now.Id]==false)
				{
					Dis[Now.Id][Now.Pos]=Dis[u][i];
					vis[Now.Id]=true;
					Ci[Now.Id]=Ci[u]+1;
					q.push(Now.Id);
				}
				else
				{
					if(Ci[Now.Id]==Ci[u]+1)
					{
						Dis[Now.Id][Now.Pos]=max(Dis[Now.Id][Now.Pos],Dis[u][i]);
					}
				}
			}
		}
	}
	int Max=0;
	for(int i=0;i<Group[End].size();i++)
	{
		Min=min(Min,Ci[Group[End][i].Id]);
	}
	for(int i=0;i<Group[End].size();i++)
	{
		if(Min==Ci[Group[End][i].Id])
		{
			Max=max(Max,Dis[Group[End][i].Id][Group[End][i].Pos]);
		}
	}
	if(Min==1)
	{
		cout<<"optimal travel from "<<S<<" to "<<T<<": "<<Min<<" line, "<<Max<<" minutes"<<'\n';
	}
	else
	{
		cout<<"optimal travel from "<<S<<" to "<<T<<": "<<Min<<" lines, "<<Max<<" minutes"<<'\n';
	}
	for(int i=0;i<=M;i++)
	{
		Vec[i].clear();
		Ci[i]=0;
		Dis[i].clear();
	}
	for(int i=1;i<=N;i++)
	{
		Dis[i].clear();
		Group[i].clear();
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int T;
	cin>>T;
	while(T--)
	{
		Solve();
	}
}

数据结构

Preprefix sum

可以看做给定差分数组求原数组前缀和,就比较好理解。操作 \(1\) 等价于原数组区间加减,操作 \(2\) 等价于求原数组前缀和。然后用线段树维护区间加减和区间求和。时间复杂度 \(O(M\log N)\)

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

const int MAXN=1e6+50;
ll Size[MAXN<<2];
ll Sum[MAXN<<2],Lazy[MAXN<<2];
ll a[MAXN];
void Build(int u,int l,int r)
{
	Size[u]=r-l+1;
	if(l==r)
	return;
	int Mid=l+r>>1;
	Build(u<<1,l,Mid);
	Build(u<<1|1,Mid+1,r);
}
void PushDown(int u)
{
	Sum[u<<1]+=Size[u<<1]*Lazy[u];
	Sum[u<<1|1]+=Size[u<<1|1]*Lazy[u];
	Lazy[u<<1]+=Lazy[u];
	Lazy[u<<1|1]+=Lazy[u];
	Lazy[u]=0;
}
void Modify(int u,int l,int r,int x,int y,ll c)
{
	if(x<=l&&r<=y)
	{
		Sum[u]+=c*Size[u];
		Lazy[u]+=c;
		return;
	}
	if(Lazy[u])
	{
		PushDown(u);
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Modify(u<<1,l,Mid,x,y,c);
	if(y>=Mid+1)
	Modify(u<<1|1,Mid+1,r,x,y,c);
	Sum[u]=Sum[u<<1]+Sum[u<<1|1];
}
ll GetSum(int u,int l,int r,int x,int y)
{
	if(x<=l&&r<=y)
	{
		return Sum[u];
	}
	if(Lazy[u])
	{
		PushDown(u);
	}
	int Mid=l+r>>1;
	ll Now=0;
	if(x<=Mid)
	{
		Now+=GetSum(u<<1,l,Mid,x,y);
	}
	if(y>=Mid+1)
	{
		Now+=GetSum(u<<1|1,Mid+1,r,x,y);
	}
	return Now;
}


int N,M;
int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M;
	Build(1,1,N);
	for(int i=1;i<=N;i++)
	{
		cin>>a[i];
		Modify(1,1,N,i,N,a[i]);
	}
	while(M--)
	
	{
		string op;
		cin>>op;
		if(op=="Modify")
		{
			int i,x;
			cin>>i>>x;
			Modify(1,1,N,i,N,-a[i]+x);
			a[i]=x;
		}
		else
		{
			int i;
			cin>>i;
			cout<<GetSum(1,1,N,1,i)<<'\n';
		}
	}
}

[POI 2015 R2] 物流 Logistics

结论是若 \(\sum_{i=1}^{n}\min(a_i,s) \geq c\times s\) 则可以,否则不行。

有一个非常好的网络流理解方式可以解释这个结论,这里不细说了。用权值线段树可以维护前缀 Sum 和后缀 Cnt。以 \(s\) 为分界。注意空间是 \(O(N\log \omega)\) 级别的,\(\omega\) 是值域。时间复杂度也是 \(O(N\log \omega)\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+50;
static char buf[1000000],*p1=buf,*p2=buf,obuf[1000000],*p3=obuf;
#define getchar() p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++
template<typename item>
inline void read(register item &x)
{
	bool flag=false;
    x=0;register char c=getchar();
    while(c<'0'||c>'9')
	{
		if(c=='-')
		flag=true;
		c=getchar();
	}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    if(flag)
    x=-x;
}
int N,M;

ll Cnt[MAXN<<5],Sum[MAXN<<5];
int ls[MAXN<<5],rs[MAXN<<5];
int tot;

void Modify(int &u,int l,int r,ll x,ll c)
{
	if(u==0)
	{
		u=++tot;
	}
	if(l==r)
	{
		Cnt[u]+=c;
		Sum[u]+=x*c;
		return;
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Modify(ls[u],l,Mid,x,c);
	else
	Modify(rs[u],Mid+1,r,x,c);
	Sum[u]=Sum[ls[u]]+Sum[rs[u]];
	Cnt[u]=Cnt[ls[u]]+Cnt[rs[u]];
}
ll GetCnt(int u,int l,int r,int x,int y)
{
	if(u==0)
	return 0;
	if(x<=l&&r<=y)
	{
		return Cnt[u];
	}
	int Mid=l+r>>1;
	ll Now=0;
	if(x<=Mid)
	Now+=GetCnt(ls[u],l,Mid,x,y);
	if(y>=Mid+1)
	Now+=GetCnt(rs[u],Mid+1,r,x,y);
	return Now; 
}
ll GetSum(int u,int l,int r,int x,int y)
{
	if(u==0)
	return 0;
	if(x<=l&&r<=y)
	{
		return Sum[u];
	}
	int Mid=l+r>>1;
	ll Now=0;
	if(x<=Mid)
	Now+=GetSum(ls[u],l,Mid,x,y);
	if(y>=Mid+1)
	Now+=GetSum(rs[u],Mid+1,r,x,y);
	return Now;
}

const int MAX=1e9+1;
int A[MAXN];
int Root;
int main()
{
	read(N);
	read(M);
	Modify(Root,0,MAX,0,N);
	while(M--)
	{
		char op=0;
		while(op!='U'&&op!='Z')
		op=getchar();
		if(op=='U')
		{
			int k,a;
			read(k);
			read(a);
			Modify(Root,0,MAX,A[k],-1);
			A[k]=a;
			Modify(Root,0,MAX,A[k],1);
		}
		else
		{
			ll c,s;
			read(c);
			read(s);
			ll Now=GetSum(Root,0,MAX,0,s)+s*GetCnt(Root,0,MAX,s+1,MAX);
			if(Now>=c*s)
			cout<<"TAK\n";
			else
			cout<<"NIE\n";
		}
	}
}

[POI 2015 R3] 访问 Visits

这题题意不太清晰,其实步伐大小为 \(c\) 步指在 \(u\)\(v\) 的简单路径上每次走 \(c\) 步。

容易发现可以对于 \(u\to lca\)\(v \to lca\) 分别求和,互不干扰。

\(c\) 根号分治,对于 \(c\) 较大的,走的步数一定不多,每次暴力跳 \(c\) 级祖先即可。

对于 \(c\) 较小的,用树上前缀和的方式维护,然后关于减去 \(lca\) 之上多算的部分较为有细节。

均匀来讲复杂度是 \(O(n\sqrt{n\log n})\)。瓶颈在求树上 \(k\) 级祖先,可以用长链剖分优化到 \(O(n\sqrt{n})\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50,S=200;
int N;
ll a[MAXN];
int b[MAXN],c[MAXN];

struct Edge
{
	int x,y,Next;	
}e[MAXN<<1];
int elast[MAXN],tot;
void Add(int x,int y)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
ll Sum[MAXN][S+2];
int depth[MAXN];
int f[MAXN][16];
void dfs(int u,int fa)
{
	f[u][0]=fa;
	for(int i=1;f[f[u][i-1]][i-1];i++)
	{
		f[u][i]=f[f[u][i-1]][i-1];
	}
	depth[u]=depth[fa]+1;
	int Now=f[u][0];
	for(int i=1;i<=S;i++)
	{
		Sum[u][i]=a[u];
	}
	for(int i=1;i<=S&&Now;i++)
	{
		Sum[u][i]+=Sum[Now][i];
		Now=f[Now][0];
	}
	
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		dfs(v,u);
	}
}
int GetLca(int x,int y)
{
	if(depth[x]<depth[y])
	swap(x,y);
	for(int i=15;i>=0;i--)
	{
		if(f[x][i]&&depth[f[x][i]]>=depth[y])
		{
			x=f[x][i];
		}
	}
	if(x==y)
	return x;
	for(int i=15;i>=0;i--)
	{
		if(f[x][i]&&f[y][i]&&f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
int GetKth(int x,int K)
{
	if(K==0)
	return x;
	for(int i=15;i>=0;i--)
	{
		if(K>=(1<<i))
		{
			K-=(1<<i);
			x=f[x][i];
		}
	}
	return x;
}
int main()
{
	cin>>N;
	for(int i=1;i<=N;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<N;i++)
	{
		int x,y;
		cin>>x>>y;
		Add(x,y);
		Add(y,x);
	}
	dfs(1,0);
	for(int i=1;i<=N;i++)
	{
		cin>>b[i];
	}
	for(int i=1;i<N;i++)
	{
		cin>>c[i];
	}
	for(int i=1;i<N;i++)
	{
		if(c[i]>S)
		{
			int Lca=GetLca(b[i],b[i+1]);
			if(Lca!=b[i]&&Lca!=b[i+1])
			{
				int Now=b[i];
				ll ans=a[b[i]];
				while(depth[Now]>depth[Lca])
				{
					Now=GetKth(Now,c[i]);
					if(depth[Now]>=depth[Lca])
					ans+=a[Now];
				}
				ans+=a[b[i+1]];
				Now=b[i+1];
				while(depth[Now]>depth[Lca])
				{
					Now=GetKth(Now,c[i]);
					if(depth[Now]>=depth[Lca])
					ans+=a[Now];
				}
				if((depth[b[i]]-depth[Lca])%c[i]==0)
				{
					ans-=a[Lca];
				}
				cout<<ans<<'\n';
			}
			else
			{
				if(Lca==b[i])
				{
					int Now=b[i+1];
					ll ans=a[b[i+1]];
					while(Now!=b[i])
					{
						Now=GetKth(Now,c[i]);
						ans+=a[Now];
					}
					cout<<ans<<'\n';
				}
				else
				{
					int Now=b[i];
					ll ans=a[b[i]];
					while(Now!=b[i+1])
					{
						Now=GetKth(Now,c[i]);
						ans+=a[Now];
					}
					cout<<ans<<'\n';
				}
			}
		}
		else
		{
			int Lca=GetLca(b[i],b[i+1]);
			if((depth[b[i]]-depth[Lca])%c[i]==0)
			{
				cout<<Sum[b[i]][c[i]]+Sum[b[i+1]][c[i]]-2*Sum[Lca][c[i]]+a[Lca]<<'\n';
			}
			else
			{
				int K=depth[b[i]];
				ll ans=Sum[b[i]][c[i]]+Sum[b[i+1]][c[i]];
				for(int j=15;j>=0;j--)
				{
					if(K-(1ll<<j)*c[i]>=depth[Lca])
					{
						K-=(1ll<<j)*c[i];
					}
				}
				int Now=GetKth(b[i],depth[b[i]]-K);
				ans=ans-Sum[Now][c[i]]+a[Now];
				K=depth[b[i+1]];
				for(int j=15;j>=0;j--)
				{
					if(K-(1ll<<j)*c[i]>=depth[Lca])
					{
						K-=(1ll<<j)*c[i];
					}
				}
				Now=GetKth(b[i+1],depth[b[i+1]]-K);
				ans=ans-Sum[Now][c[i]]+a[Now];
				cout<<ans<<'\n';
			}
		}
	}
}

[POI 2015 R1] 影迷 Movie-goer

扫描线,维护左端点在 \(l\) 的时候每一个右端点的贡献。预处理同种电影下一部的位置不难维护。

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

const int MAXN=1e6+50;

int Next[MAXN],Last[MAXN],First[MAXN];
int N,M;
int f[MAXN];
ll w[MAXN];
ll Max[MAXN<<2],Lazy[MAXN<<2];
void PushDown(int u)
{
	Max[u<<1]+=Lazy[u];
	Max[u<<1|1]+=Lazy[u];
	Lazy[u<<1]+=Lazy[u];
	Lazy[u<<1|1]+=Lazy[u];
	Lazy[u]=0;
}
void Modify(int u,int l,int r,int x,int y,ll c)
{
	if(x<=l&&r<=y)
	{
		Max[u]+=c;
		Lazy[u]+=c;
		return;
	}
	if(Lazy[u])
	PushDown(u);
	int Mid=l+r>>1;
	if(x<=Mid)
	Modify(u<<1,l,Mid,x,y,c);
	if(y>=Mid+1)
	Modify(u<<1|1,Mid+1,r,x,y,c);
	Max[u]=max(Max[u<<1],Max[u<<1|1]);
}
ll GetMax(int u,int l,int r,int x,int y)
{
	if(x<=l&&r<=y)
	{
		return Max[u];
	} 
	if(Lazy[u])
	PushDown(u);
	int Mid=l+r>>1;
	ll Now=0;
	if(x<=Mid) 
	Now=max(Now,GetMax(u<<1,l,Mid,x,y));
	if(y>=Mid+1)
	Now=max(Now,GetMax(u<<1|1,Mid+1,r,x,y));
	return Now;
}

struct Dot
{
	int l,r;
	ll c;
	Dot(){}
	Dot(int _l,int _r,ll _c)
	{
		l=_l;
		r=_r;
		c=_c;
	}
};
vector<Dot>Vec[MAXN];
int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M;
	for(int i=1;i<=N;i++)
	{
		cin>>f[i];
	}
	for(int i=1;i<=M;i++)
	{
		cin>>w[i];
	}
	for(int i=1;i<=N;i++)
	{
		if(Last[f[i]]==0)
		{
			First[f[i]]=i;
		}
		Next[Last[f[i]]]=i;
		Last[f[i]]=i;
		Next[i]=N+1;
	}
	Next[0]=0;
	for(int i=1;i<=M;i++)
	{
		if(First[i])
		{
			Modify(1,1,N,First[i],Next[First[i]]-1,w[i]);
			Vec[First[i]].push_back(Dot(First[i],Next[First[i]]-1,-w[i]));
		}		
	}
	ll ans=0;
	for(int i=1;i<=N;i++)
	{
		ans=max(ans,GetMax(1,1,N,i,N));
		for(int j=0;j<Vec[i].size();j++)
		{
			Modify(1,1,N,Vec[i][j].l,Vec[i][j].r,Vec[i][j].c);
		}
		if(Next[i]<=N)
		{
			Modify(1,1,N,Next[i],Next[Next[i]]-1,w[f[i]]);
			Vec[Next[i]].push_back(Dot(Next[i],Next[Next[i]]-1,-w[f[i]]));
		}
	}
	cout<<ans;
}

[POI 2014] KAR-Cards

线段树维护每一段的信息,每一段仅有 \(2\) 种开头和对应的 \(2\) 种结尾,分别表示开头数字是较小值,以及对应的末尾最小可能值,开头数字是较大值,以及对应的末尾最大可能值。然后容易合并。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
static char buf[1000000],*p1=buf,*p2=buf,obuf[1000000],*p3=obuf;
#define getchar() p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++
template<typename item>
inline void read(register item &x)
{
	bool flag=false;
    x=0;register char c=getchar();
    while(c<'0'||c>'9')
	{
		if(c=='-')
		flag=true;
		c=getchar();
	}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    if(flag)
    x=-x;
}
const int MAXN=1e6+50,INF=1e9;
int N,M;

struct Segment
{
	int LMin,LMax,RMin,RMax;
}tr[MAXN<<2];

Segment operator +(Segment a,Segment b)
{
	if(b.LMin>=a.RMin)
	{
		a.RMin=b.RMin;
	}
	else
	{
		if(b.LMax>=a.RMin)
		{
			a.RMin=b.RMax;
		}
		else
		{
			a.RMin=INF;
		}
	}
	if(b.LMin>=a.RMax)
	{
		a.RMax=b.RMin;
	}
	else
	{
		if(b.LMax>=a.RMax)
		{
			a.RMax=b.RMax;
		}
		else
		{
			a.RMax=INF;
		}
	}
	return a;
}

int X[MAXN],Y[MAXN];

void Build(int u,int l,int r)
{
	if(l==r)
	{
		tr[u].LMin=tr[u].RMin=X[l];
		tr[u].LMax=tr[u].RMax=Y[l];
		return;
	}
	int Mid=l+r>>1;
	Build(u<<1,l,Mid);
	Build(u<<1|1,Mid+1,r);
	tr[u]=tr[u<<1]+tr[u<<1|1];
}

void Update(int u,int l,int r,int x)
{
	if(l==r)
	{
		tr[u].LMin=tr[u].RMin=X[l];
		tr[u].LMax=tr[u].RMax=Y[l];
		return;
	}
	int Mid=l+r>>1;
	if(x<=Mid)
	Update(u<<1,l,Mid,x);
	else
	Update(u<<1|1,Mid+1,r,x);
	tr[u]=tr[u<<1]+tr[u<<1|1];
}

int main()
{
	read(N);
	
	for(int i=1;i<=N;i++)
	{
		read(X[i]);
		read(Y[i]);
		if(X[i]>Y[i])
		swap(X[i],Y[i]);
	}
	Build(1,1,N);
	read(M);
	while(M--)
	{
		int a,b;
		read(a);
		read(b);
		
		swap(X[a],X[b]);
		swap(Y[a],Y[b]);
		Update(1,1,N,a);
		Update(1,1,N,b);
		if(tr[1].RMin!=INF||tr[1].RMax!=INF)
		{
			puts("TAK");
		}
		else
		{
			puts("NIE");
		}
	}
}

DP

[PA 2014] Pakowanie

显然包从大到小排序,先填大包更优。状压 dp 记录当前已经填进包的物品,dp 值第一维是用的包的数量,第二维是当前有填物品的最后一个包还剩的空间。第一维相同的时候第二维越大越好,然后常规状压 dp 即可。

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

const int MAXN=((1<<24)|50),MAXM=105;

int N,M;
int a[MAXM];
int c[MAXM];
struct Dot
{
	int Cnt,Leave;
	Dot(){}
	Dot(int _Cnt,int _Leave)
	{
		Cnt=_Cnt;
		Leave=_Leave;
	}
}f[MAXN];
bool operator <(Dot a,Dot b)
{
	if(a.Cnt==b.Cnt)
	return a.Leave>b.Leave;
	return a.Cnt<b.Cnt;
}
Dot min(Dot a,Dot b)
{
	return a<b?a:b;
}

bool cmp(int a,int b)
{
	return a>b;
}
int main()
{
	cin>>N>>M;
	for(int i=0;i<N;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<=M;i++)
	{
		cin>>c[i];
	}
	sort(c+1,c+M+1,cmp);
	
	f[0].Cnt=f[0].Leave=0;
	for(int S=1;S<(1<<N);S++)
	{
		f[S].Cnt=M+1;
		f[S].Leave=0;
		for(int i=0;i<N;i++)
		{
			if((S>>i)&1)
			{
				int Now=(S^(1<<i));
				if(f[Now].Leave>=a[i])
				{
					f[S]=min(f[S],Dot(f[Now].Cnt,f[Now].Leave-a[i]));
				}
				else
				{
					if(a[i]<=c[f[Now].Cnt+1])
					f[S]=min(f[S],Dot(f[Now].Cnt+1,c[f[Now].Cnt+1]-a[i]));
				}
			}
		}
	}
	if(f[(1<<N)-1].Cnt<=M)
	cout<<f[(1<<N)-1].Cnt;
	else
	cout<<"NIE";
	
}

[PA 2012 Finals] Two Cakes

\(f_{i,j}\) 表示第一个数组写到 \(i\),第二个数组写到 \(j\) 的最短时间,答案为 \(f_{n,n}\)。朴素转移是简单的。考虑到数组均为排列,特殊的 \((i,j)\) 仅有 \(n\) 对,对于这 \(n\) 对记忆化一下,非这 \(n\) 对的可以以 \(O(\log n)\) 的复杂度向左平移到最近的一对特殊的 \((i,j)\)。预处理特殊对的差值然后二分就行。记忆化搜索比较好写。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50;
int N;
int p[MAXN],q[MAXN]; 
int f[MAXN];
bool vis[MAXN];
vector<int>Id[2*MAXN];
int dfs(int i,int j)
{
	if(i==0)
	return j;
	if(j==0)
	return i;
	if(p[i]==q[j])
	{
		if(vis[i])
		return f[i];
		else
		{
			vis[i]=true;
			return f[i]=min(dfs(i-1,j),dfs(i,j-1))+1;
		}
		
	}
	else
	{
		int Cha=j-i+N;
		int Now=upper_bound(Id[Cha].begin(),Id[Cha].end(),i)-Id[Cha].begin();
		if(Now>0)
		{
			Now--;
			return dfs(Id[Cha][Now],Id[Cha][Now]+(Cha-N))+i-Id[Cha][Now];
		}
		else
		{
			return max(i,j);
		}
	}
}
int Pos[MAXN];
int main()
{
	ios::sync_with_stdio(false);
	cin>>N;
	for(int i=1;i<=N;i++)
	{
		cin>>p[i];
	}
	for(int i=1;i<=N;i++)
	{
		cin>>q[i];
		Pos[q[i]]=i;
	}
	for(int i=1;i<=N;i++)
	{
		Id[Pos[p[i]]-i+N].push_back(i);
	}
	cout<<dfs(N,N);
}

[CERC2014] Outer space invaders

显然只在出现和消失的端点处使用武器一定不劣。离散化。

考虑朴素的区间 dp,令 \(f_{i,j}\) 表示所有 \([a_k,b_k]\in [i,j]\) 的外星人都被消灭的最小代价,然后枚举断点 \(x\),代价为 \(i \leq a_k \leq x \leq b_k \leq j\) 的最大的一个 \(d_k\)。复杂度 \(O(n^4)\),过不去。

注意到消除顺序是无关紧要的,而每个外星人总会被消除,所以不妨钦定先消除 \([a_k,b_k]\in [i,j]\) 的外星人里 \(d_k\) 最大的,这样复杂度仅有枚举断点。时间复杂度 \(O(n^3)\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e3+50;
const ll INF=1e18;
int N;
int a[MAXN],b[MAXN];
ll d[MAXN];
bool vis[MAXN][MAXN];
ll f[MAXN][MAXN];
ll dfs(int l,int r)
{
	if(l>r)
	return 0;
	if(vis[l][r])
	return f[l][r];
	vis[l][r]=true;
	ll Max=0;
	int Id=0;
	for(int i=1;i<=N;i++)
	{
		if(l<=a[i]&&b[i]<=r&&d[i]>Max)
		{
			Max=d[i];
			Id=i;
		}
	}
	if(Max==0)
	{
		return f[l][r]=0;
	}
	for(int x=a[Id];x<=b[Id];x++)
	{
		f[l][r]=min(f[l][r],dfs(l,x-1)+dfs(x+1,r)+Max);
	}
	return f[l][r];
	
	
}
vector<int>lsh;
map<int,int>Mp;
void Solve()
{
	lsh.clear();
	Mp.clear();
	cin>>N;
	for(int i=1;i<=N;i++)
	{
		cin>>a[i]>>b[i]>>d[i];
		lsh.push_back(a[i]);
		lsh.push_back(b[i]);
	}
	sort(lsh.begin(),lsh.end());
	int Cnt=0;
	for(int i=0;i<lsh.size();i++)
	{
		if(i==0||lsh[i]!=lsh[i-1])	
		{
			Cnt++;
			Mp[lsh[i]]=Cnt;
		}
	}
	for(int i=1;i<=N;i++)
	{
		a[i]=Mp[a[i]];
		b[i]=Mp[b[i]];
	}
	for(int i=0;i<=Cnt+1;i++)
	{
		for(int j=0;j<=Cnt+1;j++)
		{
			f[i][j]=INF;
			vis[i][j]=false;
		}
	}
	cout<<dfs(1,Cnt)<<'\n';
}
int main()
{
	ios::sync_with_stdio(false);
	int T;
	cin>>T;
	while(T--)
	{
		Solve();
	}
}

[CERC2014] Parades

\(f_{u,S}\) 表示 \(u\) 子树内,其中 \(u\) 连到儿子的边集合被占用的情况为 \(S\) 的最多可能游行数量。只考虑 \(LCA\)\(u\) 子树内的游行,其中当前只需要考虑 \(LCA=u\) 的游行,假设其中一个是 \((x,y)\),则 \(u\to x\)\(u\to y\) 会占用两条 \(u\) 连到儿子的边,转移的值为 \(f_{x,full}+f_{y,full}+1+Other\),其中 \(full\) 是全集,\(Other\) 表示 \(x\)\(y\) 路径上的点在不占用该路径的情况下最多可能的游行数量(排除 \(u,x,y\) 后的所有点的 \(f_{v,full\oplus 该路径占用的连向儿子的边}\) 之和)。可以发现一个 \(f_{v,full\oplus 该路径占用的连向儿子的边}\) 只有在访问该儿子的子树内时会产生贡献,所以维护一个子树加和单点查的数据结构即可,比如在 dfs 序上跑树状数组。然后 \(Other\) 可以 \(O(\log n)\) 求出。

\(f_{u,S}\) 的初值还包括不选 \(lca=u\) 的路径,\(f_{u,v}=f_{v,full}\)\(v\) 是单个儿子。然后跑枚举子集 dp 可以把 \(f_{u}\) 的全集给跑出来。

时间复杂度 \(O(n(3^{10}+10\log n+n))\),其中 \(10\) 是题目保证的度数上限, \(n^2\) 是暴力预处理 \(LCA\)。本题用倍增求 \(LCA\) 的复杂度是 \(O(m\log n)\),不如 \(O(n^2)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
static char buf[1000000],*p1=buf,*p2=buf,obuf[1000000],*p3=obuf;
#define getchar() p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++
template<typename item>
inline void read(register item &x)
{
	bool flag=false;
    x=0;register char c=getchar();
    while(c<'0'||c>'9')
	{
		if(c=='-')
		flag=true;
		c=getchar();
	}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    if(flag)
    x=-x;
}
const int MAXN=2e3+50;
int N,M;
struct QUE
{
	int x,y;
	QUE(){}
	QUE(int _x,int _y)
	{
		x=_x;
		y=_y;
	}
};
vector<QUE>Vec[MAXN];

int tr[MAXN];
int lowbit(int x)
{
	return x&-x;
}
void Modify(int x,int c)
{
	for(int i=x;i<=N;i+=lowbit(i))
	{
		tr[i]+=c;
	}
}
int GetSum(int x)
{
	int Sum=0;
	for(int i=x;i;i-=lowbit(i))
	{
		Sum+=tr[i];
	}
	return Sum;
}
int Cnt;
int In[MAXN],Out[MAXN],LCA[MAXN][MAXN];
vector<int>Son[MAXN];
struct Edge
{
	int x,y,Next;
}e[MAXN];
int elast[MAXN],tot;
void Add(int x,int y)
{
	tot++;
	e[tot].x=x;
	e[tot].y=y;
	e[tot].Next=elast[x];
	elast[x]=tot;
}
int father[MAXN];
void dfs1(int u,int fa)
{
	father[u]=fa;
	In[u]=++Cnt;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		dfs1(v,u);
	}
	Out[u]=Cnt;
}
int f[MAXN];
int fMax[MAXN];
int Id[MAXN];
void dfs3(int u,int fa)
{
	int Num=0;
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		dfs3(v,u);
		Id[v]=Num;
		Num++;
	}
	
	int ALL=(1<<Num)-1;
	for(int S=0;S<=ALL;S++)
	{
		f[S]=0;
	}
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		f[1<<Id[v]]=fMax[v];
	}
	for(int i=0;i<Vec[u].size();i++)
	{
		int x=Vec[u][i].x,y=Vec[u][i].y;
		int Idx=0,Idy=0;
		if(x!=u)
		{
			for(int j=elast[u];j;j=e[j].Next)
			{
				int v=e[j].y;
				if(v!=fa&&In[v]<=In[x]&&In[x]<=Out[v])
				{
					Idx=(1<<Id[v]);
					break;
				}
			}
		}
		if(y!=u)
		{
			for(int j=elast[u];j;j=e[j].Next)
			{
				int v=e[j].y;
				if(v!=fa&&In[v]<=In[y]&&In[y]<=Out[v])
				{
					Idy=(1<<Id[v]);
					break;
				}
			}
		}
		f[Idx|Idy]=max(f[Idx|Idy],fMax[x]+fMax[y]+GetSum(In[x])+GetSum(In[y])+1);
	}
	for(int S=0;S<=ALL;S++)
	{
		for(int i=S;i;i=((i-1)&S))
		{
			f[S]=max(f[S],f[i]+f[S^i]);
		}
	}
	fMax[u]=f[ALL];
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
		if(v==fa)
		continue;
		Modify(In[v],f[ALL^(1<<Id[v])]);
		Modify(Out[v]+1,-f[ALL^(1<<Id[v])]);
	}
}
void Solve()
{
	read(N);
	for(int i=1;i<N;i++)
	{
		int x,y;
		read(x);
		read(y);
		Add(x,y);
		Add(y,x);
	}
	dfs1(1,0);
	for(int i=1;i<=N;i++)
	{
		int Now=i;
		while(Now!=0)
		{
			Son[Now].push_back(i);
			LCA[Now][i]=LCA[i][Now]=Now;
			Now=father[Now];
		}
	}
	for(int u=1;u<=N;u++)
	{
		vector<int>Next;
		for(int i=elast[u];i;i=e[i].Next)
		{
			int v=e[i].y;
			if(father[v]==u)
			{
				Next.push_back(v);
			}
		}
		int Size=Next.size();
		for(int j=0;j<Size-1;j++)
		{
			for(int k=j+1;k<Size;k++)
			{
				for(int l=0;l<Son[Next[j]].size();l++)
				{
					for(int r=0;r<Son[Next[k]].size();r++)
					{
						LCA[Son[Next[j]][l]][Son[Next[k]][r]]=LCA[Son[Next[k]][r]][Son[Next[j]][l]]=u;
					}
				}
			}
		}
	}
	read(M);
	for(int i=1;i<=M;i++)
	{
		int x,y;
		read(x);
		read(y);
		Vec[LCA[x][y]].push_back(QUE(x,y));
	}
	dfs3(1,0);
	cout<<fMax[1]<<'\n';
	tot=Cnt=0;
	for(int i=1;i<=N+1;i++)
	{
		elast[i]=In[i]=Out[i]=tr[i]=father[i]=fMax[i]=Id[i]=0;
		Son[i].clear();
		Vec[i].clear();
		for(int j=1;j<=N;j++)
		{
			LCA[i][j]=0;
		}
	}
}
int main()
{
	int T;
	read(T);
	while(T--)
	{
		Solve();
	}
}

[SCOI2013] 火柴棍数字

本题题意不清,新增的数字智能放在开头,不能插在中间。

需要打一个表 \(Cost_{i,j}\) 表示数字 \(i\) 变成数字 \(j\) 需要的最少火柴棍移动次数(包括拿走,拿入和换位)。一个简单的贪心是位数越多越好,尽可能把所有能拿出的火柴棍都拿出,而且由于 \(1\) 只需要两根火柴棍,且 \(7\) 只需要三根火柴棍,所以可以处理所有可以拿出的火柴棍 \(\geq 2\) 的情况,可以拿出的火柴棍 \(<2\) 的情况可以特判也可以顺带处理。

因为并不一定能拿出 \(K\) 根,况且如何拿出 \(K\) 根也很难决定,所以考虑从高到低枚举能拿出多少根(至少为 \(2\))组成 \(1\),如果是奇数根则第一位为 \(7\)。然后从高位到低位去记忆化搜索,记忆化搜索的参数是后续需要拿出多少根,返回值是最少需要的移动次数,本质是逆向背包。如果最少需要的移动次数 \(\leq K\) 则可以拿出当前枚举的火柴数。

输出就是贪心输出,从高位到低位从大到小枚举这一位能填什么数,就是钦定这一位填某个数后跑记忆化搜索看后续能不能满足上文的限制,如果能的话这一位一定能填这个数,后续怎么填是后续的事。

该做法也能解决一根火柴都拿不出来的情况。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=505,MAXK=3505;
string s;
int K;
int N;
int a[MAXN];
int f[MAXN][MAXK];
bool vis[MAXN][MAXK];
int Cnt[11]={6,2,5,5,4,5,6,3,7,6};
int Cost[10][10]=
{
{0,4,2,2,3,2,1,3,0,1},
{0,0,2,0,0,1,1,0,0,0},
{1,4,0,1,3,2,1,3,0,1},
{1,3,1,0,2,1,1,2,0,0},
{1,2,2,1,0,1,1,2,0,0},
{1,4,2,1,2,0,0,3,0,0},
{1,5,2,2,3,1,0,4,0,1},
{0,1,1,0,1,1,1,0,0,0},
{1,5,2,2,3,2,1,4,0,1},
{1,4,2,1,2,1,1,3,0,0}
};
const int INF=1e9;
int dfs(int Pos,int Now)
{
	if(Now>K)
	return INF;
	if(Pos>N)
	{
		if(Now==0)
		return 0;
		else
		return INF;
	}
	if(vis[Pos][Now])
	return f[Pos][Now];
	vis[Pos][Now]=true;
	f[Pos][Now]=INF;
	for(int i=9;i>=0;i--)
	{
		f[Pos][Now]=min(f[Pos][Now],dfs(Pos+1,Now-Cnt[a[Pos]]+Cnt[i])+Cost[a[Pos]][i]);
	}
	return f[Pos][Now];
}
void Out(int Pos,int Now)
{
	if(Pos>N)
	return;
	for(int i=9;i>=0;i--)
	{
		if(dfs(Pos+1,Now-Cnt[a[Pos]]+Cnt[i])+Cost[a[Pos]][i]<=K)
		{
			cout<<i;
			K-=Cost[a[Pos]][i];
			Out(Pos+1,Now-Cnt[a[Pos]]+Cnt[i]);
			break;
		}
	}
}
int main()
{
	cin>>s>>K;
	N=s.length();
	for(int i=1;i<=N;i++)
	{
		a[i]=s[i-1]-'0';
	}
	for(int i=K;i>=2;i--)
	{
		if(dfs(1,i)<=K)
		{
			int Now=i;
			if(Now&1)
			{
				Now-=3;
				cout<<7;
			}
			while(Now!=0)
			{
				cout<<1;
				Now-=2;
			}
			Out(1,i);
			return 0;
		}
	}
	Out(1,0);
}

数学

BZOJ-5027 数学题

特判无解,\(a=0\)\(b=0\)\((a,b)=(0,0)\)

exgcd 可以求 \(ax+by=c\) 的一个任意解(\(\gcd(a,b) | c\) 才有解,否则无解),然后可以求步长 \(\Delta x,\Delta y\),使得 \(a(x+k\Delta x)+b(y-k\Delta y)=c\) 恒成立。

通过调整步长可以将 \(x\) 移动到 \(\geq xl\) 的第一个位置、\(\leq xr\) 的第一个位置,或将 \(y\) 移动到 \(\geq yl\) 的第一个位置、\(\leq yr\) 的第一个位置。然后对于这 \(4\) 个位置,都可以求另一个变量的位置,如果另一个变量也在范围内,则答案为两个变量移出各自区间需要的步数的 \(\min\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll exgcd(ll a,ll b,ll &x,ll &y)
{
	if(b==0)
	{
		x=1;
		y=0;
		return a;
	}
	ll Now=exgcd(b,a%b,x,y);
	ll Nowx=y,Nowy=x-y*(a/b);
	x=Nowx;
	y=Nowy;
	return Now;
}
ll Up(ll x,ll c)
{
	if(c<0)
	c=-c;
	ll Now=x/c*c;
	if(Now>=x)
	return Now;
	else
	return Now+c;
}
ll a,b,c,x,y;
ll xl,xr,yl,yr;
void Solve()
{
	cin>>a>>b>>c>>xl>>xr>>yl>>yr;
	if(a==0&&b==0)
	{
		if(c==0)
		{
			cout<<(xr-xl+1)*(yr-yl+1);
			return;
		}
		else
		{
			cout<<0;
			return;
		}
	}
	if(a==0)
	{
		if(yl*b<=-c&&-c<=yr*b)
		{
			cout<<xr-xl+1;
		}
		else
		{
			cout<<0;
		}
		return;
	}
	if(b==0)
	{
		if(xl*b<=-c&&-c<=xr*b)
		{
			cout<<yr-yl+1;
		}
		else
		{
			cout<<0;
		}
		return;
	}
	if(a>=0)
	{
		c=-c;
	}
	else
	{
		a=-a;
		b=-b;
	}
	ll G=exgcd(a,b,x,y);
	if(c%G!=0)
	{
		cout<<0;
		return;
	}
	x*=c/G;
	y*=c/G;
	ll Lcm=a*b/G,Deltax=Lcm/a,Deltay=Lcm/b;
	ll ans=0;
	if(Deltax<0)
	{
		Deltax=-Deltax;
		Deltay=-Deltay;
	}
	ll Newx,Ci,Newy;
	if(x>=xl)
	{
		Newx=x-((x-xl)/Deltax*Deltax);
	}
	else
	{
		Newx=x+Up(xl-x,Deltax);
	}
	Ci=(Newx-x)/Deltax;
	Newy=y-Ci*Deltay;
	if(Newx<=xr&&Newy>=yl&&Newy<=yr)
	{
		if(((Deltax>=0)^(Deltay>=0))==0)
		{
			ans=min((xr-Newx)/llabs(Deltax),(Newy-yl)/llabs(Deltay))+1;
		}
		else
		{
			ans=min((xr-Newx)/llabs(Deltax),(yr-Newy)/llabs(Deltay))+1;
		}
	}
	if(x<=xr)
	{
		Newx=x+((xr-x)/Deltax*Deltax);
	}
	else
	{
		Newx=x-Up(x-xr,Deltax);
	}
	Ci=(Newx-x)/Deltax;
	Newy=y-Ci*Deltay;
	if(Newx>=xl&&Newy>=yl&&Newy<=yr)
	{
		if(((Deltax>=0)^(Deltay>=0))==0)
		{
			ans=min((Newx-xl)/llabs(Deltax),(yr-Newy)/llabs(Deltay))+1;
		}
		else
		{
			ans=min((Newx-xl)/llabs(Deltax),(Newy-yl)/llabs(Deltay))+1;
		}
	}
	if(Deltay<0)
	{
		Deltax=-Deltax;
		Deltay=-Deltay;
	}
	if(y>=yl)
	{
		Newy=y-((y-yl)/Deltay*Deltay);
	}
	else
	{
		Newy=y+Up(yl-y,Deltay);
	}
	Ci=(Newy-y)/Deltay;
	Newx=x-Ci*Deltax;
	if(Newy<=yr&&Newx>=xl&&Newx<=xr)
	{
		if(((Deltax>=0)^(Deltay>=0))==0)
		{
			ans=min((Newx-xl)/llabs(Deltax),(yr-Newy)/llabs(Deltay))+1;
		}
		else
		{
			ans=min((xr-Newx)/llabs(Deltax),(yr-Newy)/llabs(Deltay))+1;
		}	
	}
	
	if(y<=yr)
	{
		Newy=y+((yr-y)/Deltay*Deltay);
	}
	else
	{
		Newy=y-Up(y-yr,Deltay);
	}
	Ci=(Newy-y)/Deltay;
	Newx=x-Ci*Deltax;
	if(Newy>=yl&&Newx>=xl&&Newx<=xr)
	{
		if(((Deltax>=0)^(Deltay>=0))==0)
		{
			ans=min((xr-Newx)/llabs(Deltax),(Newy-yl)/llabs(Deltay))+1;
		}
		else
		{
			ans=min((Newx-xl)/llabs(Deltax),(Newy-yl)/llabs(Deltay))+1;
		}
	}
	cout<<ans<<'\n';
}
int main()
{
	ios::sync_with_stdio(false);
	int T=1;
	while(T--)
	{
		Solve();
	}
}

[CERC2012] The Dragon and the knights

不难算出所有直线能将平面分成多少块。只需要判断骑士所在的本质不同块的数量是否等于总块数。

一个巧妙的想法是将每个骑士与一个相同的,不在任何一条直线上的点连线段,对于相同区域内的骑士,这个线段穿过的直线集合是一样的,否则是不一样的。考虑到直线只有 \(100\) 条,所以可以 int128 状压 + map。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50;
mt19937 Rnd(1919810);
struct Line
{
	ll A,B,C;
	Line(){}
	Line(ll _A,ll _B,ll _C)
	{
		A=_A;
		B=_B;
		C=_C;
	}
}L[MAXN];
struct Dot
{
	ll x,y;
	Dot(){}
	Dot(ll _x,ll _y)
	{
		x=_x;
		y=_y;
	}
}d[MAXN];
bool On(Dot a,Line l)
{
	if(l.A*a.x+l.B*a.y+l.C==0)
	return true;
	else
	return false;
}
bool Jiao1(Line l,Line r)
{
	if(l.A*r.B==l.B*r.A)
	{
		return false;
	}
	else
	{
		return true;
	}
}
bool Jiao2(Line l,Line r,ll xl,ll xr)
{
	if(l.A*r.B==l.B*r.A)
	{
		return false;
	}
	
	ll Up=(r.C*l.B-l.C*r.B),Down=(l.A*r.B-r.A*l.B);
	
	if(Down>=0)
	{
		if(Up>=(__int128)xl*Down&&Up<=(__int128)xr*Down)
		return true;
		else
		return false;	
	}
	else
	{
		if(Up<=(__int128)xl*Down&&Up>=(__int128)xr*Down)
		return true;
		else
		return false;
	}
}
int N,M;
bool Check(ll x,ll y)
{
	for(int i=1;i<=N;i++)
	{
		if(On(Dot(x,y),L[i]))
		return true;
	}
	for(int i=1;i<=M;i++)
	{
		if(x==d[i].x)
		return true;
	}
	return false;
}

Line Lian(Dot a,Dot b)
{
	return Line(b.y-a.y,a.x-b.x,(b.x-a.x)*a.y-(b.y-a.y)*a.x);
}
map<__int128,int>Mp;
void Solve()
{
	Mp.clear();
	cin>>N>>M;
	for(int i=1;i<=N;i++)
	{
		cin>>L[i].A>>L[i].B>>L[i].C;
	}
	for(int i=1;i<=M;i++)
	{
		cin>>d[i].x>>d[i].y;
	}
	Dot Root=Dot(0,0);
	while(Check(Root.x,Root.y))
	{
		Root.x=Rnd()%1000000;
		Root.y=Rnd()%1000000;
	}
	int Cnt=1,ans=0;
	for(int i=1;i<=N;i++)
	{
		Cnt++;
		for(int j=1;j<i;j++)
		{
			if(Jiao1(L[i],L[j]))
			{
				Cnt++;
			}
		}
	}
	for(int i=1;i<=M;i++)
	{
		__int128 Pow=1,Hash=0;
		Line Now=Lian(d[i],Root);
		ll l=min(d[i].x,Root.x),r=max(d[i].x,Root.x);
		for(int j=1;j<=N;j++)
		{
			if(Jiao2(Now,L[j],l,r))
			{
				Hash+=Pow;
			}
			Pow*=2;
		}
		if(Mp.count(Hash)==false)
		{
			Mp[Hash]=1;
			ans++;
		}
	}
	if(ans==Cnt)
	{
		cout<<"PROTECTED\n";
	}
	else
	{
		cout<<"VULNERABLE\n";
	}
}
int main()
{
	ios::sync_with_stdio(false);
	int T;
	cin>>T;
	for(int i=1;i<=T;i++)
	{
		Solve();
	}
}

[ONTAK2015] Cięcie

一个大数可以表示成 \(\sum_{i=1}^{n} a_{i} \times 10^{n-i}\)。预处理出前缀和后缀哪些位置模 \(p\) 和模 \(r\)\(0\) 是简单的,考虑中间段。

若中间段为 \([l,r]\),\(\sum_{i=l}^{r} a_{i}\times 10^{r-i} = 10^{r} \times \sum_{i=l}^{r} a_{i}\times 10^{-i}\equiv 0 \pmod{q}\)。又 \(10^{r} \not\equiv 0 \pmod{q}\),所以 \(\sum_{i=l}^{r} a_{i}\times 10^{-i}\equiv 0 \pmod{q}\)

预处理前缀和,则前缀和相等的两个点中间这段区间是合法的。剩下的代码是实现问题,注意中间段如果首位为 \(0\) 则最多只有一位。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+50;
ll qpow(ll a,ll b,ll P)
{
	ll ans=1;
	while(b)
	{
		if(b&1)
		ans=ans*a%P;
		a=a*a%P;
		b>>=1;
	}
	return ans;
}
string s;
int N,p,q,r;
int a[MAXN];
ll Pow[MAXN],Inv[MAXN];
ll Sum[MAXN];
bool vis1[MAXN],vis2[MAXN];
map<int,int>Cnt2;
int main()
{
	cin>>N>>p>>q>>r>>s;
	for(int i=1;i<=N;i++)
	{
		a[i]=s[i-1]-'0';
	}
	for(int i=1;i<=N;i++)
	{
		Sum[i]=(Sum[i-1]*10+a[i])%p;
		if(Sum[i]==0)
		{
			if(a[1]!=0||i==1)
			{
				vis1[i]=true;
			}
		}
	}
	ll Now=1;
	for(int i=N;i>=1;i--)
	{
		Sum[i]=(Sum[i+1]+Now*a[i])%r;
		Now=Now*10%r;
		if(Sum[i]==0)
		{
			if(a[i]!=0||i==N)
			{
				vis2[i]=true;
			}
		}
	}
	Now=1;
	for(int i=1;i<=N;i++)
	{
		Now=Now*10%q;
		Sum[i]=(Sum[i-1]+a[i]*qpow(Now,q-2,q))%q;
	}
	for(int i=1;i<=N;i++)
	{
		if(vis2[i+1]==true)
		{
			Cnt2[Sum[i]]++;
		}
	}
	ll ans=0;
	for(int i=1;i<N-1;i++)
	{
		if(vis2[i+1]==true)
		{
			Cnt2[Sum[i]]--;
		}
		if(vis1[i])
		{
			if(a[i+1]!=0)
			ans+=Cnt2[Sum[i]];
			else
			{
				if(vis2[i+2])
				{
					ans++;
				}
			}
		}
	}
	cout<<ans;
}

[PA 2011] Prime prime power 质数的质数次方

事实上只需要会求最小的一个 \(>n\)\(a^b\) 即可。
对于 \(b>2\) 的情况,\(a\) 一定不会超过 \(3\times 10^6\)(基于调参,就是比 \(\sqrt[3]{10^{18}}=10^6\) 略大),所以暴力把 \(\leq 3\times 10^6\) 的每个质数的每个质数次方塞进一个 set 里。

对于 \(b=2\) 的情况,\(a^2 >n\) 且不会大太多的质数并不会太多,通过调参可得只需要选 \(>\sqrt{n}\) 的前 \(3 \times 10^6\) 个数中的质数(质数个数约为 \(\frac{3\times 10^6}{\ln 3 \times 10^6}\approx 2\times 10^5>k\) 个)即可。令 \(M=3\times 10^6\),用埃氏筛能以 \(O(M\log \log M)\) 的复杂度筛出这些质数,把他们的平方放进 set 里。

然后 set 执行 \(k\) 次 upper_bound 操作即可。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std; 
const int MAXN=1e7+50;
int prime[MAXN];
bool st[MAXN];
int tot;
set<ll>S;
ll Next(ll x)
{
	return *S.upper_bound(x);
}
ll N;
int K;
bool vis[MAXN];
int main()
{
	int MAX=6000000;
	st[1]=true;
	for(int i=2;i<=MAX;i++)
	{
		if(st[i]==false)
		{
			prime[++tot]=i;
		}
		for(int j=1;i*prime[j]<=MAX;j++)
		{
			st[i*prime[j]]=true;
			if(i%prime[j]==0)
			break;
		}
	}
	for(int i=1;i<=tot;i++)
	{
		int Cnt=1;
		ll Now=prime[i];
		while((__int128)Now*prime[i]<=8e18)
		{
			Now*=prime[i];
			Cnt++;
			if(st[Cnt]==false)
			{
				S.insert(Now);
			}
		}
	}
	cin>>N>>K;
	ll Sq=ceil(sqrt(N));
	ll l=Sq,r=Sq+3000000;
	for(int i=1;1ll*prime[i]*prime[i]<=Sq+3000000;i++)
	{	
		for(ll j=((l-1)/prime[i]+1)*prime[i];j<=r;j+=prime[i])
		{
			vis[j-l]=true;
		}
	}
	for(int i=0;i<=3000000;i++)
	{
		if(vis[i]==false)
		{
			S.insert(1ll*(i+l)*(i+l));
		}
	}	
	ll Now=N;
	while(K--)
	{
		Now=Next(Now);
	}
	cout<<Now;
}

模拟

[PA 2013] Konduktorzy

只需要考虑每个检票员最后若干次跳动,具体地说,只需要考虑 \(\max(a_i)\) 的最后一次跳动。可以简单二分找出最大的一个位置使得每个检票员都在这之后才会停下,然后强制往回退 \(\max(a_i)\) 步,此时所在的位置离每个检票员结束的位置的距离不会超过 \(2\times \max(a_i)\)。然后暴力模拟跳跃即可,因为 \(a_i\) 互不相同,所以跳跃次数是调和级数级别的,又因为模拟需要用堆,总复杂度为 \(O(k (\log n +\log^2 k )\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50;
ll N;
int K;
ll a[MAXN];
ll Max;
struct Dot
{
	int Id;
	ll Val;
	Dot(int _Id,ll _Val)
	{
		Id=_Id;
		Val=_Val;
	}
};
bool operator >(Dot a,Dot b)
{
	if(a.Val==b.Val)
	{
		return a.Id>b.Id;
	}
	return a.Val>b.Val;
}
priority_queue<Dot,vector<Dot>,greater<Dot> >q;
bool Check(ll X)
{
	ll Sum=0;
	for(int i=1;i<=K;i++)
	{
		Sum+=(X-1)/a[i]+1;
		Sum=min(Sum,(ll)3e18);
	}
	if(Sum<=N)
	return true;
	else
	return false;
}
ll Now[MAXN];
ll Last[MAXN];
int main()
{
	scanf("%lld%d",&N,&K);
	for(int i=1;i<=K;i++)
	{
		scanf("%lld",&a[i]);
		Max=max(Max,a[i]);
	}
	ll l=1,r=2e18,Mid,ans=0;
	while(l<=r)
	{
		Mid=l+r>>1;
		if(Check(Mid))
		{
			ans=Mid;
			l=Mid+1;
		}
		else
		{
			r=Mid-1;
		}
	}
	ans=max(0ll,ans-Max);
	ll Cnt=0;
	for(int i=1;i<=K;i++)
	{
		if(ans==0)
		{
			Now[i]=0;
			q.push(Dot(i,0));
		}
		else
		{
			Cnt+=((ans-1)/a[i]+1);
			Now[i]=((ans-1)/a[i]+1)*a[i];
			q.push(Dot(i,Now[i]));
		}
	}
	while(Cnt<N)
	{
		Cnt++;
		Dot u=q.top();
		q.pop();
		Last[u.Id]=Cnt;
		u.Val+=a[u.Id];
		q.push(u);
	}
	for(int i=1;i<=K;i++)
	{
		printf("%lld",Last[i]); 
		if(i<K)
		putchar(' ');
	}
}

[CERC2015] Ice Igloos

本题任意两个圆的圆心均不相同。但是就算有相同,只需要用 vector 存一下每个圆心对应的半径有哪些,然后二分一下就好。另外这题我题读错了,以为 \(r\in (0,1]\),所以代码较为复杂一点,事实上 \(r\in (0,1)\)

考虑到到一条线段的距离 \(\leq 1\) 的点的数量是 \(O(线段长度)\) 个。所以暴力枚举这些点即可,我的枚举方法是考虑横坐标每增加 \(1\),可以求出纵坐标的移动范围,每次枚举该移动范围内的点即可。时间复杂度 \(O(500\sqrt{2}m)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e5+50;
struct Line
{
	int A,B,C;
	Line(){}
	Line(int _A,int _B,int _C)
	{
		A=_A;
		B=_B;
		C=_C;
	}
};
Line Get(int X1,int Y1,int X2,int Y2)
{
	int dx=X2-X1;
    int dy=Y2-Y1;
    int A=dy;
    int B=-dx;
    int C=dx*Y1-dy*X1;
    return Line(A,B,C);
}
double Vec[505][505];
bool Have[505][505];
int N,M;
int vis[505][505];
int Color;
int ans;
int dx[5]={0,-1,1,0,0},dy[5]={0,0,0,-1,1};
double Dis(int x,int y,Line Now)
{
	return 1.0*(Now.A*x+Now.B*y+Now.C)*(Now.A*x+Now.B*y+Now.C)/(Now.A*Now.A+Now.B*Now.B);
}
void CheckDot(int x,int y,int X,int Y)
{
	if(Have[x][y]==false||vis[x][y]==Color)
	return;
	double NowDis=(x-X)*(x-X)+(y-Y)*(y-Y);
	if(Vec[x][y]>=NowDis)
	{
		vis[x][y]=Color;
		ans++;
	}
}
bool cmp(double a,double b)
{
	return a>b;
}
int main()
{
	scanf("%d",&N);
	for(int i=1;i<=N;i++)
	{
		int x,y;
		double r;
		scanf("%d%d%lf",&x,&y,&r);
		Have[x][y]=true;
		Vec[x][y]=r*r;
	}
	scanf("%d",&M);
	for(int i=1;i<=M;i++)
	{
		ans=0;
		Color++;
		int xl,yl,xr,yr;
		scanf("%d%d%d%d",&xl,&yl,&xr,&yr);
		if(xl>xr)
		{
			swap(xl,xr);
			swap(yl,yr);
		}
		if(xl==xr)
		{
			if(yl>yr)
			{
				swap(yl,yr);
			}
			
			for(int j=yl;j<=yr;j++)
			{
				int x,y;
				x=xl-1,y=j;
				CheckDot(x,y,xl,j);
				x=xl+1,y=j;
				CheckDot(x,y,xl,j);
				x=xl,y=j;
				CheckDot(x,y,xl,j);
			}
			CheckDot(xl,yl-1,xl,yl);
			CheckDot(xl,yr+1,xl,yr);
			printf("%d\n",ans);
			continue;
		}
		if(yl==yr)
		{
			for(int j=xl;j<=xr;j++)
			{
				int x,y;
				y=yl-1,x=j;
				CheckDot(x,y,j,yl);
				y=yl+1,x=j;
				CheckDot(x,y,j,yl);
				y=yl,x=j;
				CheckDot(x,y,j,yl);
			}
			CheckDot(xl-1,yl,xl,yl);
			CheckDot(xr+1,yl,xl,yr);
			printf("%d\n",ans);
			continue;
		}
		Line Now=Get(xl,yl,xr,yr);
		int LastMin=yl,LastMax=yl;
		for(int x=xl+1;x<=xr;x++)
		{
			int yMin=floor(1.0*(-Now.A*x-Now.C)/Now.B);
			int yMax=ceil(1.0*(-Now.A*x-Now.C)/Now.B);
			if(yMin==yMax)
			{
				for(int op=0;op<=4;op++)
				{
					int fx=x+dx[op],fy=yMin+dy[op];
					CheckDot(fx,fy,x,yMin);
				}	
			}
			int Min=min(LastMin,yMin),Max=max(LastMax,yMax);
			for(int y=Min;y<=Max;y++)
			{
				int fx=x,fy=y;
				double NowDis;
				if(Have[fx][fy]&&vis[fx][fy]!=Color)
				{
					NowDis=Dis(fx,fy,Now);
					if(Vec[fx][fy]>=NowDis)
					{
						ans++;
						vis[fx][fy]=Color;
					}
				}
				fx=x-1,fy=y;
				if(Have[fx][fy]&&vis[fx][fy]!=Color)
				{
					NowDis=Dis(fx,fy,Now);
					if(Vec[fx][fy]>=NowDis)
					{
						ans++;
						vis[fx][fy]=Color;
					}
				}
			}
			LastMin=yMin;
			LastMax=yMax;
		}
		for(int op=0;op<=4;op++)
		{
			int x=xl+dx[op],y=yl+dy[op];
			CheckDot(x,y,xl,yl);
			x=xr+dx[op],y=yr+dy[op];
			CheckDot(x,y,xr,yr);
		}
		printf("%d\n",ans);
	}
}

[POI 2015 R1] 印章 Seal

读题易错。墨水不能印在外面但是印章是可以有部分在外面的,只需要那一部分没有凸起即可。

所以本题只需要维护印章的凸起位置,然后方格纸贪心地从左上到右下,遇到需要墨水的位置就跟印章的从左上到右下第一个凸起匹配,然后依次匹配,维护纸上每个点是否有被墨水印过。

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

using namespace std;
const int MAXN=2e3+50;
int N,M,A,B;
char s[MAXN][MAXN],t[MAXN][MAXN];
bool vis[MAXN][MAXN];
bool Can(int x,int y)
{
	if(x<1||y<1||x>N||y>M||vis[x][y])
	return false;
	return true;
}
struct Dot
{
	int x,y;
	Dot(){}
	Dot(int _x,int _y)
	{
		x=_x;
		y=_y;
	}
};
void Solve()
{
	scanf("%d%d%d%d",&N,&M,&A,&B);
	for(int i=1;i<=N;i++)
	{
		scanf("%s",&s[i][1]);
		for(int j=1;j<=M;j++)
		{
			vis[i][j]=false;
		}
	}
	for(int i=1;i<=A;i++)
	{
		scanf("%s",&t[i][1]);
	}
	vector<Dot>Vec;
	for(int i=1;i<=A;i++)
	{
		for(int j=1;j<=B;j++)
		{
			if(t[i][j]=='x')
			{
				Vec.push_back(Dot(i,j));
			}
		}
	}
	if(Vec.size()==0)
	{
		for(int i=1;i<=N;i++)
		{
			for(int j=1;j<=M;j++)
			{
				if(s[i][j]=='x')
				{
					puts("NIE");
					return;
				}
			}
		}
		puts("TAK");
		return;
	}
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<=M;j++)
		{
			if(vis[i][j]==false&&s[i][j]=='x')
			{
				int Beginx=i-Vec[0].x+1,Beginy=j-Vec[0].y+1;
				for(int k=0;k<Vec.size();k++)
				{
					int Nowx=Beginx+Vec[k].x-1,Nowy=Beginy+Vec[k].y-1;
					if(Can(Nowx,Nowy)==false)
					{
						puts("NIE");
						return;
					}
					if(s[Nowx][Nowy]!='x')
					{
						puts("NIE");
						return;
					}
					vis[Nowx][Nowy]=true;
				}
			}
		}
	}
	puts("TAK");
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		Solve();
	}
}
posted @ 2026-07-11 03:04  0htoAi  阅读(36)  评论(1)    收藏  举报