2022“杭电杯”中国大学生算法设计超级联赛(8)部分题题解

Ironforge

这个题真的是苦思冥想,想到点什么...最后还是没想清楚...(队友搞出来的,队友NB!)
这个可以算是可达性问题,就是一个点是否能到达另一个点,并且是在序列上的,所以我们可以预处理出一个点能够达到的最大的边界,然后O(1)回答问题。
怎么搞出每个点到达的l,r呢?对于这种两边都可以扩展的问题,我们可以先只按一边来,然后再搞另一边(或者两边同时迭代?).
我们先假设只能向右走,最远能走到哪?这样的话,我们可以从大到小枚举,依次处理每个点,如果点i能到达i+1,则i继承i+1的向右的范围,然后继续暴力跳。注意可达性问题中,继承性是一个很大的特点,利用这个特点我们可以快速的进行跳。发现每个点只会被跳过一次。
之后考虑向左走,同时用上向右走的信息去更新这个点的最远左右边界。
这个时候从小到大枚举,当处理i时,我们假设前面的点都已经处理出边界了。
若i用上右边的点,都到达不了i-1,则i的范围就是\([i,r_i]\)
如果i用上右边的点可以到达i-1,这个时候继续看i-1的情况:
1)若i-1的范围内有i的话,说明i-1和i可以互相到达,则i的范围和i-1保持一致。
2)若i-1的范围内没有i的话,说明i-1的右边界就是它本身,则我们同样让i继承i-1的范围,然后暴力进行扩展i的范围,可以发现,由于我们每次都是继承的关系,每个点还是被扩展一次的。
综上,完毕。
对于可达性问题的小结:注意顺序,注意继承性的特点,注意连通性。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int T,n,m,l[N],r[N];
int vis[N],prime[N],k,L[N],R[N];
vector<int>v[N]; 
void get_primes(int n)
{
	for(int i=2;i<=n;i++) {
		if(vis[i]==0) { vis[i]=i; prime[++k]=i; }
		for(int j=1;j<=k;j++) {
			if(prime[j]>vis[i]||prime[j]>n/i) break;
			vis[i*prime[j]]=prime[j];
		}
	}
}
inline void expand(int x)
{
	while(1)
	{
		if(l[x]>1&&R[l[x]-1]<=r[x])
		{
			int t=l[x]-1;
			l[x]=min(l[x],l[t]);
			r[x]=max(r[x],r[t]);
			continue;
		}
		if(r[x]<n&&L[r[x]]>=l[x])  
		{
			int t=r[x]+1;
			r[x]=max(r[x],r[t]);
			continue;
		}
		return;
	}
}
int main() 
{
//	freopen("1.in","r",stdin);
//	freopen("1.out","w",stdout);
	scanf("%d",&T);
	get_primes(200000);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=1;i<=200000;i++) v[i].clear(); 
		for(int i=1;i<=n;i++)
		{
			int x;scanf("%d",&x);
			while(x>1)
			{
				v[vis[x]].push_back(i);
				int z=vis[x];
				while(x%z==0) x/=z;
			}
		}
		for(int i=1;i<n;++i) 
		{
			int x;scanf("%d",&x);
			if(v[x].size()==0) L[i]=0,R[i]=n+1;
			else
			{
				int t=upper_bound(v[x].begin(),v[x].end(),i)-v[x].begin();
				if(t>0) L[i]=v[x][t-1];
				else L[i]=0;
				if(t<v[x].size()) R[i]=v[x][t];
				else R[i]=n+1;
			}	
		}
		for(int i=n;i>=1;--i)
		{
			r[i]=i;
			while(r[i]<n&&L[r[i]]>=i) r[i]=r[r[i]+1];	
			//暴力向后跳.
		}	
		for(int i=1;i<=n;++i)
		{
			l[i]=i;
			if(i!=1)
			{
				if(R[i-1]<=r[i])//i能到达i-1. 
				{	
					l[i]=min(l[i],l[i-1]);
					r[i]=max(r[i],r[i-1]);
					if(r[i-1]<i) expand(i);//如果i-1不能到达i. 
				}
			}
		}
		for(int i=1;i<=m;++i)
		{
			int a,b;
			scanf("%d%d",&a,&b);
			if(b>=l[a]&&b<=r[a]) puts("Yes");
			else puts("No");
		}
	}
	return 0;	
}

Darnassus

最小生成树问题,就是总的边数很多,把全部边数搞出来跑kruskal是不行的。
对于这种题,感觉有点小套路,可能找边的过程用到的知识点是各不相同的,但总的知识点是一致的,就是找出kn条边,使得kn(符合复杂度)条边内一定存在最小生成树,然后用这些边去做最小生成树。我们看着个题,任意两个点的边权位\(|i-j|*|p_i-p_j|\)。考虑相邻的两个点,对于他们之间的边,边权<=n-1.并且他们之间能根据这些边形成一个生成树。那么根据kruskal,最小生成树的边权一定<=n-1.所以我们只关注<=n-1以内的边即可。
由于边权又是两者相乘的形式,则必定会存在一个数<=sqrt(n-1)。
则我们对这两个分别枚举到sqrt(n-1)即可。总的边数nsqrt(n).
这个题是通过已经存在的生成树来对最小生成树进行限制。总的来说我们的想法就是将边的个数减小,方便我们计算即可。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=50010,M=2e7+5e6;
int T,n,m,f[N];
struct wy{int id,p;}a[N];
struct bian{int x,y,val;}b[M];
map<pair<int,int>,bool>mp; 
inline int getf(int x){return f[x]==x?x:f[x]=getf(f[x]);}
int main() 
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);m=0;
		int mx=0;
		for(int i=1;i<=n;++i) 
		{
			scanf("%d",&a[a[i].id=i].p);
			f[i]=i;	
			if(i>1) mx=max(mx,abs(a[i].p-a[i-1].p));
		}
		for(int i=1;i<=n;++i)
		{
			for(int j=i+1;j<=n&&j-i<=sqrt(mx);++j)
			{
				ll t=(ll)(j-i)*abs(a[i].p-a[j].p);
				if(t<=mx)  b[++m]={i,j,t};
			}
		}
		sort(a+1,a+n+1,[&](wy a,wy b){return a.p<b.p;});
		for(int i=1;i<=n;++i)
		{
			for(int j=i+1;j<=n&&j-i<=sqrt(mx);++j)
			{
				ll t=(ll)(j-i)*abs(a[i].id-a[j].id);
				if(t<=mx)  b[++m]={a[i].id,a[j].id,t};	
			}
		}
		sort(b+1,b+m+1,[&](bian a,bian b){return a.val<b.val;});
		ll ans=0;int cnt=0;
		for(int i=1;i<=m;++i)
		{
			int tx=getf(b[i].x),ty=getf(b[i].y);
			if(tx!=ty) 
			{
				ans+=b[i].val;
				f[tx]=ty;
				if(++cnt==n-1) break;
			}
		}
		printf("%lld\n",ans);
	}
	return 0;	
}

Darkmoon Faire

这个题整体的框架还是很简单的,设f[i]表示前i个划分完毕的方案数。
显然f[i]+=f[j].其中[j,i]是合法的区间。
考虑优化,发现合法的区间是和区间最大值,区间最小值有关的。
并且是前面所有j到到i的最大值和最小值,用什么数据结构去处理。
单调栈,为什么用单调栈,因为单调栈可以随时维护之前所有的j到i的最大值和最小值信息。并且是以区间的形式告诉你的。例如,我们建立一个从栈底到栈顶单调递减的栈,比如说目前栈为2,4,6.当前i是6.(这里以下标入栈)。则说明区间[1,6]和[2,6]的最大值是a[2].区间[3,6],[4,6]最大值是a[4].区间[5,6],[6,6]的最大值是a[6].他能维护以i结尾的所有区间的最大值和最小值。具体的就是栈中相邻的两个元素x,y,我们可以得到以[x+1,y]开头,以i结尾的区间的最大值是a[y].
这个题要求最大值和最小值,所以我们同时维护两个,一个单调递减,一个单调递增。另外单调栈的DP优化题目一般与线段树结合,因为区间操作比较方便。
并且这个题还有限制,是从新划分区间内开始奇偶的统计,这也就是说我们要把一个区间内进行奇偶划分,具体的就是你确定一堆区间的最大值都是某一个位置,但他们区间开头不同,导致这个位置也是一个合法一个不合法,这需要考虑区间开头和你最大值位置的奇偶关系。然后一个区间需要最大值和最小值都合法才行。我们用线段树打标记的形式去实现,最大值符合打上1的标记,最小值符合打上2的标记,只有两个标记都符合才能被统计答案。线段树的叶子节点x表示x到i这个区间被打标记情况和存的DP值。
有一说一,真NM难写...

点击查看代码
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
#define ll long long
using namespace std;
const int N=3e5+10,P=998244353;
int T,n,a[N],Stack1[N],top1,Stack2[N],top2;
ll f[N],sum[2][N];
struct Tree
{
	int l[2],r[2],lazy[2][3];
	ll dat[2][3];
	#define l(x,p) t[p].l[x]
	#define r(x,p) t[p].r[x]
	#define dat(y,p,x) t[p].dat[y][x] 
	#define lazy(y,p,x) t[p].lazy[y][x]
}t[N<<2];
//1记录奇数位置的情况,0记录偶数位置的情况. 
inline void build(int p,int l,int r)
{
	for(int i=0;i<2;++i)
	{
		l(i,p)=l;r(i,p)=r;
		dat(i,p,0)=dat(i,p,1)=dat(i,p,2)=0;
		lazy(i,p,1)=lazy(i,p,2)=0;
	}
	if(l==r) return;
	int mid=l+r>>1;
	build(ls,l,mid);
	build(rs,mid+1,r);
}
inline void add(int kp,int p,int k)
{
	lazy(kp,p,k)=1;
	dat(kp,p,0)=dat(kp,p,3-k);
	dat(kp,p,k)=((sum[kp^1][r(kp,p)-1]-(l(kp,p)-2<0?0:sum[kp^1][l(kp,p)-2]))%P+P)%P;
}
inline void del(int kp,int p,int k)
{
	lazy(kp,p,k)=-1;
	dat(kp,p,k)=dat(kp,p,0)=0;
}
inline void push(int k,int p)
{
	for(int i=1;i<=2;++i)
	{
		if(lazy(k,p,i))
		{	
			if(lazy(k,p,i)==1) add(k,ls,i),add(k,rs,i);
			if(lazy(k,p,i)==-1) del(k,ls,i),del(k,rs,i);
			lazy(k,p,i)=0;
		}
	}
}
inline void alter(int k,int p,int l,int r,int op,int val)
{
	if(l<=l(k,p)&&r>=r(k,p))
	{
		if(val==1) add(k,p,op);
		else del(k,p,op);
		return;
	}
	push(k,p);
	int mid=l(k,p)+r(k,p)>>1;
	if(l<=mid) alter(k,ls,l,r,op,val);
	if(r>mid) alter(k,rs,l,r,op,val);
	for(int i=0;i<3;++i) dat(k,p,i)=dat(k,ls,i)+dat(k,rs,i);
	return;
}
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;++i) scanf("%d",&a[i]);
		build(1,1,n);
		top1=top2=0;
		f[0]=1;sum[0][0]=1;sum[1][0]=0;
		for(int i=1;i<=n;++i)
		{
			while(top1&&a[i]>a[Stack1[top1]]) top1--;
			Stack1[++top1]=i;//维护当前的向上递减的栈
			if(i&1) 
			{	
				alter(1,1,Stack1[top1-1]+1,i,1,1);
				alter(0,1,Stack1[top1-1]+1,i,1,0);
			}
			else
			{
				alter(0,1,Stack1[top1-1]+1,i,1,1);
				alter(1,1,Stack1[top1-1]+1,i,1,0);
			} 
			while(top2&&a[i]<a[Stack2[top2]]) top2--;
			Stack2[++top2]=i;
			if(i&1) 
			{	
				alter(0,1,Stack2[top2-1]+1,i,2,1);
				alter(1,1,Stack2[top2-1]+1,i,2,0);
			}
			else
			{
				alter(1,1,Stack2[top2-1]+1,i,2,1);
				alter(0,1,Stack2[top2-1]+1,i,2,0);
			} 
			f[i]=(dat(1,1,0)+dat(0,1,0))%P;
			sum[0][i]=sum[0][i-1];
			sum[1][i]=sum[1][i-1];
			if(i&1) sum[1][i]=(sum[1][i]+f[i])%P;
			else sum[0][i]=(sum[0][i]+f[i])%P;
		}
		printf("%lld\n",f[n]); 
	}
	return 0;
} 

Undercity

首先观察数据范围,6*6的网格,这不就是....搜索!!!
考虑具体怎么搜索,我们可以先预处理出所有的回文路径。怎么存储呢?考虑一共只有36个格子,对于每个格子,路径经过则记为1,不经过记为0.那么一个路径可以用一个long long范围内的数去表示。存在起点处。之后开始DP,用上记忆化搜索,每次找到最小的点,用它在集合中路径去划分状态。具体可以看代码(真的简单易懂).

点击查看代码
#include<bits/stdc++.h>
#define db double
#define ll long long
using namespace std;
const int N=10;
int T,n,m,id[N][N];
vector<ll>g[50];
map<ll,ll>f,pos;
char c[N][N];
inline bool check(string s)
{
	int len=s.size();
	for(int i=0,j=len-1;i<j;++i,--j)
		if(s[i]!=s[j]) return false;
	return true;
}
inline void dfs(int t,int x,int y,ll state,string s)
{
	if(check(s)) g[t].push_back(state);
	if(x!=n-1) dfs(t,x+1,y,state|(1ll<<id[x+1][y]),s+c[x+1][y]);
	if(y!=m-1) dfs(t,x,y+1,state|(1ll<<id[x][y+1]),s+c[x][y+1]);
}
inline ll get(ll x)
{
	if(f.find(x)!=f.end()) return f[x];
	ll ans=0,t=pos[x&(-x)]; 
	for(auto s:g[t])//找到最小的1.
		if((s&x)==s) ans+=get(x-s);
	return f[x]=ans;
}
int main()
{
//	freopen("1.in","r",stdin);
	scanf("%d",&T);
	for(int i=0;i<=35;++i) pos[(1ll<<i)]=i;	
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=0;i<n;++i) scanf("%s",c[i]);
		for(int i=0,k=0;i<n;++i)
			for(int j=0;j<m;++j,k++) 
			{
				id[i][j]=k;
				g[k].clear();
			}
		for(int i=0;i<n;++i)//预处理出所有的路径. 
			for(int j=0;j<m;++j)
			{
				string s="";
				s+=c[i][j];
				dfs(id[i][j],i,j,1ll<<id[i][j],s);	
			}	
		f.clear();f[0]=1;
		printf("%lld\n",get((1ll<<(n*m))-1));
	}
	return 0;
} 
posted @ 2022-08-11 20:41  逆天峰  阅读(68)  评论(0编辑  收藏  举报
作者:逆天峰
出处:https://www.cnblogs.com/gcfer//