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;
}