一部分内容在另一篇文章中已经写过了:高级数据结构(算法提高课),这里把剩下的补一下
总结与练习
一,并查集
1,关押罪犯
题目链接:https://www.acwing.com/problem/content/description/259/
带权并查集+二分判定答案
这题要求我们求冲突事件的影响力的尽可能的小,也就是最大值最小,看到这类要求,我们就要考虑是否能用二分判定答案来做,显然这题是可以的,首先我们二分答案,对于所有大于我们二分的结果mid的影响力,我们就将它们加入到一个集合中,然后将其两个罪犯分别关押到两个房间,用不同的边权来表示,如果二者的边权和模2为0,说明这两个罪犯关押在一个房间中,模2为1,说明关押在不同房间中。对于满足条件的影响力,我们都判断其两个罪犯是否在一个集合中,如果在一个集合中,就要判断一下它们的边权和是否满足条件,如果不在一个集合中,就合并到一个集合,并且更新一下边权,因为异或运算是不进位的加法,所以这里用异或运算可以直接省略取模
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=20010,M=100010;
int n,m;
int f[N],d[N];//f[i]表示点i所在的集合,d[i]表示点i的边权
struct Node//记录罪犯之间的怨气值
{
int a,b,c;
}node[M];
int find(int x)//找到x的根节点,并且更新x所在的集合中的所有点的边权
{
if(x!=f[x])
{
int root=find(f[x]);
d[x]^=d[f[x]];
f[x]=root;
}
return f[x];
}
bool check(int mid)//check函数判断当前二分的答案是否满足条件
{
for(int i=1;i<=n;i++)f[i]=i,d[i]=0;//因为有多次二分,每次二分都要初始化
for(int i=1;i<=m;i++)
{
if(node[i].c>mid)//对于满足条件的点就加入到一个集合中去
{
int a=node[i].a,b=node[i].b;
int fa=find(a),fb=find(b);
if(fa==fb)//判断是否在一个集合
{
if(d[a]^d[b]==0)//判断是否有冲突
return false;
}
else//不在一个集合就合并这两个集合
{
f[fa]=fb;//将a的根节点指向b的根节点
d[fa]^=d[a]^d[b]^1;//记得更新边权
}
}
}
return true;//都没问题就返回true
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&node[i].a,&node[i].b,&node[i].c);
int l=0,r=1e9+10;
while(l<r)
{
int mid=l+r>>1;
if(check(mid))r=mid;//如果当前mid可以满足条件,就可以进一步缩小答案
else l=mid+1;
}
printf("%d",r);
return 0;
}
2,石头剪刀布
题目链接:https://www.acwing.com/problem/content/description/260/
带权并查集
假定一定有裁判,那么在没有裁判的对局中一定都是合法的,有裁判的对局一定都是不合法的,那么我们就枚举每个人作为裁判,如果不包含裁判的对局中没有出现矛盾,那么这个人就有可能是裁判,我们记录下来可能是裁判的人数。如果不包含裁判的对局中出现了矛盾,说明裁判在其中的对局中,这个人一定不是裁判。同时我们可以记录一下我们最早能在第几轮对局中确定这个人不是裁判,最终如果可能的裁判数为1,我们就可以枚举所有不是裁判的人,找到一个对局排除非裁判的那些人不是裁判
可以通过带权并查集来判断不包含裁判的对局中是否会出现矛盾,规定模2的大于模1的,模1的大于模0的,模0的大于模2的。可以参考食物链那题
因此最终,如果可能是裁判的人数大于1,就输出Can not determine
可能是裁判的人数为1,就输出裁判是谁和最早能找到的对局确定该裁判
如果为0,说明裁判可能必须为0或者裁判必须为多个
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int N=510,M=2010;
int n,m;
int f[N],d[N],t[N];//f[i]表示第i个人所在的集合,d[i]表示第i个人的边权,t[i]表示最早能排除第i个人不是裁判的轮数
int a[M],c[M];
char b[M];
int find(int x)//找到x所在集合的根节点,并更新集合中其他点的边权和其父节点,将其父节点指向根节点,即路径压缩
{
if(x!=f[x])
{
int root=find(f[x]);
d[x]+=d[f[x]];
f[x]=root;//!!!!!注意,这里一定要写在最后,否则上一步的边权修改会出错
}
return f[x];
}
bool check(int j)//判断第j轮对局是否满足条件
{
int x=a[j],y=c[j];
int fx=find(x),fy=find(y);//分别找到x所在的集合和y所在的集合
int op=0;//初始假定它们相等
if(b[j]=='>')op=1;//如果x大于y,op为1
else if(b[j]=='<')op=-1;//如果x小于y,op为-1
//如果x和y之前已经在一个集合中,在集合中的关系与给定的关系出现了矛盾,就说明第j轮对局出现了矛盾
//注意这里的判断条件,一定要这么写,因为可能会出现负数!!!!
if(fx==fy&&(d[x]-d[y]-op)%3)return false;
else
{
f[fx]=fy;//将x的根节点指向y的根节点
d[fx]=d[y]-d[x]+op;//更新x的根节点到y的根节点的距离
}
return true;//说明第j轮符合条件
}
int main()
{
while(cin>>n>>m)
{
for(int i=1;i<=m;i++)
cin>>a[i]>>b[i]>>c[i];
memset(t,0,sizeof t);//多组测试数据,每组要初始化
int cnt=0,num=0;//cnt用于记录可能是裁判的人数,num用来记录裁判是谁
for(int i=0;i<n;i++)//枚举每个人作为裁判时
{
for(int i=0;i<=n;i++)f[i]=i,d[i]=0;//枚举每个人都要初始化
bool is_umpire=true;//首先假定第i个人可以作为裁判
for(int j=1;j<=m;j++)//枚举每一轮的对局
{
if(a[j]!=i&&c[j]!=i&&!check(j))//判断不包含裁判的对局是否会出现冲突
{
is_umpire=false;//如果出现冲突说明第i个人不为裁判
t[i]=j;//记录在哪一轮对局能排除第i个人不是裁判
break;
}
}
if(is_umpire)//如果第i个人是裁判的话,就将可能的裁判数量加1
{
cnt++;
num=i;//并且记录裁判是谁
}
}
if(!cnt)puts("Impossible");//说明必须没有裁判或者必须裁判为多个
else if(cnt>1)puts("Can not determine");//说明裁判的人选多于1个
else//说明裁判为1个
{
int res=0;
//找到一个最大的轮数,满足能排除所有人不是裁判,这个最大的数就是最早能找到裁判的轮数
for(int i=0;i<n;i++)
if(i!=num)
res=max(res,t[i]);
printf("Player %d can be determined to be the judge after %d lines\n",num,res);
}
}
return 0;
}
3,真正的骗子
题目链接:https://www.acwing.com/problem/content/description/261/
带权并查集加上DP加上DP回溯
首先观察一下题目,假设由a b yes,假设a是天使,那么b也是天使,假设a是恶魔,那么b也是恶魔。如果为a b no,假设a为天使,那么b为恶魔,假设a为恶魔,那么b为天使。
因此我们可以发现,如果是yes,说明a和b是一类人,如果是no,说明a和b不是一类人。那么我们可以用带权并查集将a和b合并到一个集合中,并且确定a和b的边权,因为只有两类人,因此这里用异或计算边权会方便很多,0代表是一类人,1代表不是一类人。所有条件给完以后,所有的人会被分到几个集合中,我们称其为连通块,连通块的数量记为cnt,对于每个连通块,计算一下边权为0的人数和边权为1的人数。那么题目就转换成了,给定cnt个连通块,每个连通块中有两个数,在每个连通块中最多选择一个数,判断在cnt个连通块中选择,能凑出p1个天使的方案数。得到方案数以后,在回溯一遍,看是由哪些状态转移过来的,就可以知道哪些人是天使。
这里要注意一下,如果方案数不唯一,就不能判断哪些人是天使,哪些人是恶魔。例如有三个连通块,每个连通块中的两类人的人数都是1,天使的数量为1,那么对于每个连通块,我们既可以选择其中一类,也可以选择另一类,方案数很多,因此就无法判断哪一类人是天使,哪一类人是恶魔
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_map>
#include<vector>
using namespace std;
const int N=1010;
int n,p1,p2;//p1为天使数,p2为恶魔数
int a,b;
char str[5];
//f[i]表示点i所在的集合,d[i]表示点i的边权,dp[i][j]表示在前i个连通块中选择,能凑成j个天使的方案数
//num[i][0]表示第i个连通块中边权为0的数量,num[i][1]表示第i个连通块中边权为1的数量
int f[N],d[N],dp[N][N],num[N][2];
int find(int x)//找到x所在的集合,并且更新集合中所有点的边权且将集合中所有点路径压缩,指向根节点
{
if(x!=f[x])
{
int root=find(f[x]);
d[x]^=d[f[x]];
f[x]=root;
}
return f[x];
}
int main()
{
while(scanf("%d%d%d",&n,&p1,&p2),n||p1||p2)//多组测试数据
{
//初始化
for(int i=1;i<=p1+p2;i++)f[i]=i,d[i]=0;
memset(dp,0,sizeof dp);
memset(num,0,sizeof num);
//输入数据,合并集合,更新集合中点的边权
for(int i=1;i<=n;i++)
{
scanf("%d%d%s",&a,&b,str);
int t=0;//初始时默认时yes,表示a和b的边权相同
if(str[0]=='n')t=1;//如果是no,表示a和b的边权不同
int fa=find(a),fb=find(b);//找到a的根节点和b的根节点
f[fa]=fb;//合并集合
d[fa]=d[a]^d[b]^t;//更新边权
}
int cnt=0;//记录连通块的数量
unordered_map<int,int>mp;//用map给每个连通块编号
for(int i=1;i<=p1+p2;i++)
{
int x=find(i);
if(mp.count(x)==0)mp[x]=++cnt;//如果当前连通块之前没出现过,就编一个号
num[mp[x]][d[i]]++;//记录当前连通块中不同边权的数量
}
//DP过程
dp[0][0]=1;//边界初始化,从前0个连通块中选择,能凑出0个人的方案数为1
for(int i=1;i<=cnt;i++)//遍历连通块的数量
{
for(int j=0;j<=p1;j++)//遍历天使的数量
{
if(j>=num[i][0])dp[i][j]+=dp[i-1][j-num[i][0]];//如果当前天使的数量能由第i个连通块中边权为0的人数凑成的话
if(j>=num[i][1])dp[i][j]+=dp[i-1][j-num[i][1]];//如果当前天使的数量能由第i个连通块中边权为1的人数凑成的话
}
}
//DP回溯,找到转移的过程
//如果方案数不等于1,说明不满足题意,因为如果方案数大于1
//那么至少会有一处不同,不能确定不同的那处的连通块中谁为天使,谁为恶魔
if(dp[cnt][p1]!=1)puts("no");
else
{
bool ans[N][2];//用一个ans记录答案
memset(ans,false,sizeof ans);//初始化
for(int i = cnt, j =p1; i >= 1; i --)//倒着查找转移的过程
{
if(dp[i - 1][j - num[i][0]] == 1)//说明由第i个连通块中边权为0的方案数转移过来的;
{
ans[i][0] = true;
j -= num[i][0];
}
else if(dp[i - 1][j - num[i][1]] == 1)//说明由第i个连通块中边权为1的方案数转移过来的
{
ans[i][1] = true;
j -= num[i][1];
}
}
for(int i = 1; i <= p1+p2; i ++)//先找到第i个人所在的连通块和边权,看这个连通块的此边权是否时转移的点
if(ans[mp[find(i)]][d[i]])
cout << i << endl;
cout << "end" << endl;
}
}
return 0;
}
二,树状数组
1,买票
题目链接:https://www.acwing.com/problem/content/262/
树状数组+二分
这题与之前算法提高课中谜一样的牛那题一模一样,就不在重复赘述了,高级数据结构(算法提高课)
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=200010;
int n;
int tr[N],a[N],b[N];//tr为树状数组
int ans[N];//ans 记录答案
int lowbit(int x)
{
return x&-x;
}
void add(int x,int c)//单点修改
{
for(int i=x;i<=n;i+=lowbit(i))tr[i]+=c;
}
int sum(int x)//区间查询
{
int res=0;
for(int i=x;i;i-=lowbit(i))res+=tr[i];
return res;
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<=n;i++)tr[i]=0;//多组测试数据,所以每次要初始化
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&b[i]);
a[i]++;//便于后面在tr数组中查询第a[i]大的数
add(i,1);//表示每个位置都能用1次
}
for(int i=n;i>0;i--)//一定要逆序遍历
{
int l=1,r=n;//二分
while(l<r)
{
int mid=l+r>>1;
if(sum(mid)>=a[i])r=mid;//查询tr数组中第a[i]大的数的位置
else l=mid+1;
}
ans[l]=b[i];
add(l,-1);//用过的数要删去
}
for(int i=1;i<=n;i++)printf("%d ",ans[i]);
printf("\n");
}
return 0;
}
三,线段树
1,旅馆
题目链接:https://www.acwing.com/problem/content/description/263/
这题与算法提高课中得“你能回答这些问题吗”相似,我们要维护几个信息,当前节点得最长连续得空房间,以区间左端点开始得最长连续空房间,以区间右端点结尾得最长连续空房间
其他得与那题类似,不同得是返回时要返回房间号尽可能小得时,因此我们查询时递归得顺序可以设置成先查询左儿子,再查询左儿子得后缀加右儿子得前缀,最后查询右儿子
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=50010;
int n,m;
struct Node
{
int l,r;
int d,ld,rd,lazy;
//d表示当前节点表示的区间的最长连续空房间
//ld表示当前节点表示的区间的左端点开始的最长连续空房间
//rd表示当前节点所表示的区间的右端点结尾的最长连续空房间
//lazy为懒标记,-1表示没有蓝表示,1表示当前节点的子节点的房间都有人,0表示当前节点的子节点的房间都没有人
}tr[N*4];
//函数重载,用子节点更新父节点的信息,参数分别为根节点,左儿子,右儿子
void pushup(Node& root,Node& left,Node& right)
{
root.ld=left.ld;//根节点的最长前缀等于左儿子的最长前缀
if(left.ld==left.r-left.l+1)root.ld+=right.ld;//如果左儿子的最长前缀等于左儿子的区间大小,根节点的最长前缀还要加上右儿子的最长前缀
root.rd=right.rd;//根节点的最长后缀等于右儿子的最长后缀
if(right.rd==right.r-right.l+1)root.rd+=left.rd;//如果右儿子的最长后缀等于右儿子的区间大小,根节点的最长后缀还要加上左儿子的最长后缀
//根节点的最长空房间数等于左右儿子的最长空房间数的max和左儿子的最长后缀加右儿子的最长前缀树
root.d=max(max(left.d,right.d),left.rd+right.ld);
}
void pushup(int u)//用子节点更新父节点的信息
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void pushdown(int u)//用父节点更新子节点的信息
{
if(tr[u].lazy==-1)return;//如果没有懒标记,就直接返回
else//有懒标记看看是哪个懒标记,分类讨论
{
if(tr[u].lazy)//为1说明该节点的子节点的房间都有人
{
//那么该节点的左右儿子节点的d,ld,rd都为0
tr[u<<1].d=tr[u<<1].ld=tr[u<<1].rd=0;
tr[u<<1|1].d=tr[u<<1|1].ld=tr[u<<1|1].rd=0;
}
else//为0说明该节点的子节点的房间都没有人
{
//那么该节点的左右儿子节点的d,ld,rd为该节点的区间大小
tr[u<<1].d=tr[u<<1].ld=tr[u<<1].rd=tr[u<<1].r-tr[u<<1].l+1;
tr[u<<1|1].d=tr[u<<1|1].ld=tr[u<<1|1].rd=tr[u<<1|1].r-tr[u<<1|1].l+1;
}
tr[u<<1].lazy=tr[u<<1|1].lazy=tr[u].lazy;//将父节点的懒标记下传给子节点
tr[u].lazy=-1;//清楚父节点的懒标记
}
}
void build(int u,int l,int r)//建立一个线段树的骨架,初始时,所有房间都是空的,所以d,ld,rd都为区间大小
{
if(l==r)tr[u]={l,r,r-l+1,r-l+1,r-l+1,-1};
else
{
tr[u]={l,r,r-l+1,r-l+1,r-l+1,-1};
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
}
}
int query(int u,int d)//查询大于d的连续个空房间
{
if(tr[u].l==tr[u].r)return tr[u].l;//如果只有一个房间,直接返回
pushdown(u);//查询前要先pushdown更新一下儿子节点的信息
if(tr[u<<1].d>=d)return query(u<<1,d);//先从左儿子中找
//在从左儿子的后缀与右儿子的前缀中找,返回左儿子那边的下标
if(tr[u<<1].rd+tr[u<<1|1].ld>=d)return tr[u<<1].r-tr[u<<1].rd+1;
if(tr[u<<1|1].d>=d)return query(u<<1|1,d);//最后从右儿子中找
}
void modify(int u,int l,int r,int op)//修改l~r区间的房间状态
{
if(tr[u].l>=l&&tr[u].r<=r)//当前节点的区间完全被需要修改的区间覆盖
{
if(op==1)//说明l~r房间有人
{
tr[u].d=tr[u].ld=tr[u].rd=0;//修改当前节点的房间状态
tr[u].lazy=1;//并打上懒标记
}
else//说明l~r房间没人
{
tr[u].d=tr[u].ld=tr[u].rd=tr[u].r-tr[u].l+1;//修改当前节点的房间状态
tr[u].lazy=0;//并打上懒标记
}
}
else//说明当前节点的左儿子或者右儿子与需要修改的区间有交集
{
pushdown(u);//需要先pushdown更新一下儿子节点的信息
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid)modify(u<<1,l,r,op);//与左儿子有交集
if(r>mid)modify(u<<1|1,l,r,op);//与右儿子有交集
pushup(u);//最后回溯时记得用子节点更新父节点得信息
}
}
int main()
{
scanf("%d%d",&n,&m);
build(1,1,n);//先建立线段树
while(m--)
{
int op,d,x;
scanf("%d",&op);
if(op==1)
{
scanf("%d",&d);
if(tr[1].d<d)printf("0\n");//说明没有大于d得连续空房间
else//一定有大于等于d得连续空房间
{
int res=query(1,d);
printf("%d\n",res);
modify(1,res,res+d-1,1);//入住了要改变房间得状态
}
}
else
{
scanf("%d%d",&x,&d);
modify(1,x,x+d-1,0);//退房了也要更新房间得状态
}
}
return 0;
}