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是有区别的
该死的子任务捆绑测试
所以先等等