20240704数据结构“水”题选讲

“水”的Konata

Konata可爱捏——语出唐老师

但是模拟赛,就不可爱了,qwq

T1.New Year and Conference

我们看到有一个跟“区间”有关的关键词

所以我们可以很自然的想到要对区间进行排序

在进行排序过后进行一个离线的扫描

然后你使用区间查询求和就可以了

当然这是第一种思路

我们观察到对于两个类似于“串”的结构

对相似进行比较

显然就是对两个集合进行一个xor的操作,而hash函数(或者是hash表)就可以很好的满足我们的需求

我们要求的东西只需要按照端点位置排序之后,求一个 前缀 / 后缀 异或和就好了。加上排序的时间复杂度是O(nlogn)的

T1
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll,ll> pll;
const int N=2e6+10;
const int M=2e6+10;
const int inf=0x3f3f3f3f;
struct Q
{
    ll a,b,c,d;
}s[N];
vector<int> num;
int n;
struct node
{
    int l,r;
    ll mi;
    ll mx;
}tr[N<<1];
bool cmp(Q a,Q b)
{
    if(a.b==b.b)
      return a.a<b.a;
    return a.b<b.b;
}
void pushup(int u)
{
    tr[u].mi=min(tr[u<<1].mi,tr[u<<1|1].mi);
    tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void build(int u,int l,int r)
{
	if(l==r)
	{
		tr[u]={l,r,s[l].d,s[l].c};
	}
	else
	{
		tr[u]={l,r};
		int mid=l+r>>1;
		build(u<<1,l,mid);
		build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
ll query_max(int u,int l,int r)
{
    if(tr[u].l>=l and tr[u].r<=r)
	{
        return tr[u].mx;
    }
    int mid=tr[u].l+tr[u].r>>1;
    ll ans=0;
    if(l<=mid)
      ans=query_max(u<<1,l,r);
    if(r>mid)
      ans=max(ans,query_max(u<<1|1,l,r));
    return ans;
}
ll query_min(int u,int l,int r)
{
    if(tr[u].l>=l and tr[u].r<=r)
	{
        return tr[u].mi;
    }
    int mid=tr[u].l+tr[u].r>>1;
    ll ans=1e9;
    if(l<=mid)
      ans=query_min(u<<1,l,r);
    if(r>mid)
      ans=min(ans,query_min(u<<1|1,l,r));
    return ans;
}
int solve()
{
    sort(s+1,s+1+n,cmp);
    int i;
    num.clear();
    for(i=1;i<=n;i++)
	{
        num.push_back(s[i].b);
    }
    build(1,1,n);
    for(i=1;i<=n;i++)
	{
        if(i==1)
          continue;
        int pos=lower_bound(num.begin(),num.end(),s[i].a)-num.begin()+1;
        if(pos>=i)
          continue;
        int mi=query_min(1,pos,i-1);
        int mx=query_max(1,pos,i-1);
        if(mi<s[i].c||mx>s[i].d)
          return false;
    }
    return true;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
	{
        cin>>s[i].a>>s[i].b>>s[i].c>>s[i].d;
    }
    int ok=1;
    ok&=solve();
    for(int i=1;i<=n;i++)
	{
        swap(s[i].a,s[i].c);
        swap(s[i].b,s[i].d);
    }
    ok&=solve();
    if(ok)
	{
        cout<<"YES"<<endl;
    }
    else
	{
        cout<<"NO"<<endl;
    }
}

T2. Lexicographically Small Enough

给定长度为n的两个字符串s和t,然后每次操作可以交换s中的相邻的两个字符,求使得s比t的字典序小的操作最少要多少次。

我们将两个字符串画出来(请自行脑补

对于最终状态的前一个状态

我们肯定有前i个字符都是相同的,然后s的第i+1个字符的字典序小于t的第i+1个字符的字典序

我们进行一个递归的烧烤

对于一个未操作的字符x(x∈S),我们想对他进行一个排序,势必是要将x向前移的,也可以是将y移动到x+2处

所以我们可以很自然的想到一个桶,对S和T的两个字符串开桶,对于相同的字符移除,最后就是我们想要的结果

T2
 #include<bits/stdc++.h>
#define ll long long
using namespace std;
const int qs=2e5+7;
int T,n;
string a,b;
queue<int> q[500];
int val[qs];
ll manw()
{
	ll x=0,f=1;
    char ch=getchar();
    while(ch<'0' or ch>'9')
    {
        if(ch=='-')
          f=-1;
        ch=getchar();
    }
    while(ch>='0' and ch<='9')
      x=x*10+ch-'0',ch=getchar();
    return x*f;
}
int lowbit(int x)
{
	return (x&(-x));
}
void add(int x,int k)
{
	for(;x<=n;x+=lowbit(x))
	  val[x]+=k;
}
ll canyou(ll x)
{
	ll ret=0;
	for(;x>0;x-=lowbit(x))
	  ret+=val[x];
	return ret;
}
int main()
{
	T=manw();
	while(T--)//qatar 2022world cup final
	{
		n=manw();
		a=manw();
		a=" "+a;
		b=manw();
		b=" "+b;
		for(int i='a';i<='z';i++)
		{
			while(q[i].size())
			  q[i].pop();
		}
		for(int i=1;i<=n;i++)
		{
			val[i]=0;
		}
		for(int i=1;i<=n;i++)
		{
			q[a[i]].push(i); 
			add(i,1);
		}
		ll kmss=1e12,sum=0;
		for(int i=1;i<=n;i++)
		{
			for(int j='a';j<b[i];j++)
			{
				if(q[j].size())
				{
					ll p=canyou(q[j].front()-1);
					kmss=min(kmss,sum+p);
				}
			}
			if(!q[b[i]].size())
			  break;
			ll p=canyou(q[b[i]].front()-1);
			sum+=(p);
			add(q[b[i]].front(),-1);
			q[b[i]].pop();
		}
		if(kmss==1e12)
		  kmss=-1;
		printf("%lld\n",kmss);
	}
	return 0;
}

T3.令人感伤的红雨

其实就是对三个式子进行化简

然后按照题意进行模拟就行了

所以考验的是式子化简的功力(您对OI的MO化怎么评价?

对于第一个式子数组中区间  内的最大值位置,相同的取靠右的

然后就得到了B(l,r)=A(1,r)-l,Ω(l,r)=min{min{|A(1,j)-i|}}(第一个min,i从l到r,第二个min,j从i到r)

(不会用数学公式的蒟蒻)

但是这只能过前三个subtask(该死的子任务捆绑测试!伏笔

同时由于本题前缀加的特殊性,集合之间只会发生合并而不会发生分裂一类的事情

因此考虑维护这些区间。我们需要存储一个区间内  的最大值、最左侧元素的位置、最右侧元素的位置。为了将一个元素和其区间对应上,我们还需要一个并查集。

这些区间如果发生合并,那么肯定是从修改位置  往后的区间开始依次合并,直到碰到一个不能合并的区间。还有一个重点,就是对于前缀加操作打上 。一个区间 被打上的  的含义是,这个区间比右侧区间整体大上 。那么在执行合并判断时,需要加上该 ;同时在合并区间以后,需要将它的 加在自己的  上作为更新。

还是并查集

T3
 #include<bits/stdc++.h>
using namespace std;
int n,q,a[6000001];
int nxt[6000001],w[6000001];//单向链表 
int fa[6000001];
int read()
{
	int x=0,f=1;
    char ch=getchar();
    while(ch<'0' or ch>'9')
    {
        if(ch=='-')
          f=-1;
        ch=getchar();
    }
    while(ch>='0' and ch<='9')
      x=x*10+ch-'0',ch=getchar();
    return x*f;
}
int find(int x)//寻找父亲节点 
{
	if(fa[x]==x)
	  return x;
	else
	  return fa[x]=find(fa[x]);
}
int main()
{
	n=read(),q=read();
	for(int i=1;i<=n;i++)
	  scanf("%d",a+i),nxt[i]=n+1,w[i]=-1,fa[i]=i;//初始化并查集
	int lastID=0;
	for(int i=1;i<=n;i++)//依次询问 
	  if(a[i]<a[lastID])
	    fa[i]=lastID;
	  else
	  {
	  	  nxt[lastID]=i;
	  	  w[lastID]=a[i]-a[lastID];
	  	  lastID=i;
	  }
	while(q--)
	{
		int op,x,y;
		op=read(),x=read(),y=read();
		if(op==1)
		{
			lastID=find(x),w[lastID]-=y;
			while(nxt[lastID]!=n+1 and w[lastID]<0)
			{
				int nextID=nxt[lastID];
				w[lastID]+=w[nextID];
				nxt[lastID]=nxt[nextID],fa[nextID]=lastID;
				w[nextID]=-1,nxt[nextID]=n+1;
			}
		}
		if(op==2)
		  printf("%d\n",max(0,x-find(y)));
	}
	return 0;
}

T4.Guessing Permutation for as Long as Possible

及其长的题目(确信

还是并查集吧

我们充分发扬人类智慧

题目中说的是一个只有两个数的数对

但是我们发现了一条性质:

如果我们想要a,b,c三个数中,满足(a,c)数对最后出现

那么b就一定不能在a,c中间,归纳一下可以发现,这是排列合法的充要条件

证明懂了,但是不想写,数学公式太难了

等拿到手机补个图吧

反正就拿并查集维护就行,

然后发现,由于边数是n*(n-1)/2每组询问都是有效的,所以能唯一确定一组排列。也就是说,每一种数对的染色方案都唯一对应了一个排列。因此最终的答案就是  是独立的数对集合数(即强连通分量的个数)。

T4
 #include<bits/stdc++.h>
using namespace std;
const int N=800,M=1e5+5,mod=1e9+7;
int n,m,ans,anss=1;
int u[M],v[M];
int fa[N];
int read()
{
	int x=0,f=1;
    char ch=getchar();
    while(ch<'0' or ch>'9')
    {
        if(ch=='-')
          f=-1;
        ch=getchar();
    }
    while(ch>='0' and ch<='9')
      x=x*10+ch-'0',ch=getchar();
    return x*f;
}
int find(int x)//并查集查询 
{
	if(fa[x]==x)
	  return x;
	else
	  return fa[x]=find(fa[x]);
}
void merge(int x,int y)//并查集合并 
{
	int fx=find(x),fy=find(y);
	if(fx==fy) 
	  return ;
	ans--;
	fa[fx]=fy;
}
int ls[N],rs[N],siz[N];//左子树,右子树,子树大小 
bool son[N][N];//儿子节点 
int lca[N][N];//求lca的数组 
bool g[N][N]; 
int dir[N];
int main()
{
	n=read();
	m=n*(n-1)/2;
	for(int i=1;i<=m;i++)
	  u[i]=read(),v[i]=read();
	for(int i=1;i<=2*n;i++)
	  fa[i]=i;//建图 
	int nw=n;
	for(int i=m;i>=1;i--)
	{
		int U=u[i],V=v[i];
		int fu=find(U),fv=find(V);
		if(fu!=fv)
		{
			nw++;
			fa[fu]=fa[fv]=nw;
			ls[nw]=fu,rs[nw]=fv;//寻找公共祖先 
		}
	}
	for(int u=1;u<=nw;u++)
	{
		son[u][u]=1;
		vector<int>L,R;
		for(int t=1;t<=nw;t++)
		{
			if(son[ls[u]][t])
			  son[u][t]=1,L.push_back(t);
			if(son[u][t])
			  son[u][t]=1,R.push_back(t);//优化寻找lca 
		}
		for(int t=1;t<=nw;t++)
		  siz[u]+=son[u][t];
		for(auto x:L)
		  for(auto y:R)
		    lca[x][y]=lca[y][x]=u;//递归求lca 
	}
	for(int i=1;i<=n*2;i++)
	  fa[i]=i;
	ans=2*(n-1);
	memset(dir,-1,sizeof(dir));
	for(int i=m;i>=1;i--)
	{
		int U=u[i],V=v[i];
		for(int t=1;t<=n;t++)
		{
			if(t!=U and t!=V and !g[U][t] and !g[V][t])
			{
				int x=U,y=V;
				if(lca[x][t]==lca[y][t])
				  continue;
				else
				{
					if(siz[lca[x][t]]>siz[lca[y][t]])
					  swap(x,y);
					int lcx=lca[x][t],lcy=lca[y][t];
					int val=son[ls[lcx]][x]^son[rs[lcy]][x];
					if(!val)
					  merge(lcx,lcy),merge(lcx-n,lcy-n);
					else
					  merge(lcx-n,lcy),merge(lcx,lcy-n);//按照题意进行模拟
					  //也就是说,
					  //每一种数对的染色方案都唯一对应了一个排列。
					  //因此最终的答案就是 2^k; 
				}
			}
		}
	}
	for(int i=n+1;i<=nw;i++)
	  if(find(i)==find(i-n))
	    return puts("0"),0;//寻找lca,路径压缩,合并 
	for(int i=1;i<=ans/2;i++)
	  anss=anss*2%mod;
	  cout<<anss;
	return 0;
}

T5.生日礼物

我们先定义f(i,j)是前i个中出j的最大值

我们要处理完后对最后一下进行枚举

如果一个正数大于m,那么m肯定全选

如果小于m,就进行一个贪心,选取绝对值最少的进行合并

但是要分左右符号的正负进行讨论

但是最后发现无论如何,cnt的数量总会少一个

所以对于负号来说其实是一样的

你只是多进行一轮罢了,也就是奇偶关系的转换(特判就行

用优先队列+链表进行维护

T5
 #include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,m,a[N],zz;
int read()
{
	int x=0,f=1;
    char ch=getchar();
    while(ch<'0' or ch>'9')
    {
        if(ch=='-')
          f=-1;
        ch=getchar();
    }
    while(ch>='0' and ch<='9')
      x=x*10+ch-'0',ch=getchar();
    return x*f;
}
struct no
{
    long long sum;
    int mid;
    bool friend operator > (no a,no b)
    {
        return a.sum>b.sum;
    }
}node[N];
int pre[N],fro[N];
bool fw[N];
priority_queue<no,vector<no>,greater<no > > q1;
int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++)
      a[i]=read();
    long long sm=0,ans=0,js=0,la=0;
    for(int i=1;i<=n;i++)
    {
        if(a[i]==0)
		  continue;
        if(a[i]*la<0)
        {
            zz++;
            node[zz].sum=sm;
            if(sm>0)
            {
                js++;
                ans+=sm;
            }
            sm=0;
            if(zz==1 and la<0)
			  zz=0;
        }
        la=a[i];
        sm+=a[i];
    }
    if(sm>0)
    {
        js++,ans+=sm;
        zz++;
        node[zz].sum=sm;
    }
    if(js<=m)
    {
        printf("%lld\n",ans);
        exit(0);
    }
    m=js-m;
    for(int i=1;i<=zz;i++)
    {
        node[i].mid=i;
        node[i].sum=abs(node[i].sum);
        q1.push(node[i]);
        pre[i]=i-1,fro[i]=i+1;
    }
    fro[zz]=0;
    node[0].sum=0x7fffffff;
    while(m--)
    {
        while(fw[q1.top().mid]) 
		  q1.pop();
        no tt=q1.top();q1.pop();
        int x=tt.mid;
        ans-=tt.sum;
        tt.sum=-tt.sum;
        tt.sum+=node[pre[x]].sum+node[fro[x]].sum;
        node[x].sum=tt.sum;
        fw[pre[x]]=fw[fro[x]]=1;
        pre[x]=pre[pre[x]];
        fro[x]=fro[fro[x]];
        pre[fro[x]]=x;
        fro[pre[x]]=x;
        q1.push(tt);
    }
    printf("%lld\n",ans);
    return 0;
}

T6.蚯蚓

你先别急

这篇我喝的题解

但是呢,他说的是100代码

所以当我满心欢喜的摆烂完后,

嗯100和ac是有区别的

该死的子任务捆绑测试

所以先等等

posted @ 2024-07-04 18:14  Merlin·Lee  阅读(15)  评论(0编辑  收藏  举报