一、知识点:
并查集、单调栈、最小生成树。
(一)并查集
主要讲解种类并查集:在基础并查集的基础上多了一个relation【】数组来记录x与其根节点的关系。若x,y在同一集合中,则可以判断x与y的关系,换言之,若x与y可以判断关系,则将他们归并到同一集合中。
例如poj1182,食物链
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
relation【x】表示x与rootx(x的根节点)的三种关系
relation【x】==0 表示x与rootx是同类
(relation数组的初始状态确定该关系:起初x的根节点就是x,所以x与rootx是同类,所以re【x】==0表示同类关系)
relation【x】==1表示x吃rootx
relation【x】==2表示x被rootx吃
本题是判断假话数量,做法是判断x与y是不是在一个集合中,若在统一集合中,判断他们的关系与题中所给关系是否一致,不一致则假话数量加1;若不在同一集合中,则按题中所给他们的关系归并到同一集合中(也就是假设这句话正确,再判断后面的话是否与这句话产生矛盾)。
代码有两个注意点
(1)路径压缩时的relation数组处理
int getf(int u)
{
if(u==root[u])
return u;
int t=getf(root[u]);
relation[u]=(relation[u]+relation[root[u]])%3;
root[u]=t;
return t;
}
在递归时,先层层推进寻找根节点,再递归回去实现路径压缩,有意思的是由于的递归的特性需要路径压缩的点与始终与根节点隔着一个节点,利用这一特性可推导出relation数组的关系式。
目前遇到的两种、三种关系的路径压缩关系式都符合relation【x】=(relation【x】+relation【a【x】】)%(关系数)的规律
(2)关系域合并时的relation数组处理
这里学习到一种新的思维方式,也算是一种记忆方式:向量思维法
在基础并查集区间合并时,会对x,y两个节点进行寻祖,再让他们的祖先直接合并。祖先在合并时也要更新祖先之间的关系。
推导方式就是向量思维法了:
我们把x--->rootx看作一个向量,偏移量为relation【x】,由此推倒出:rootx--->rooty=rootx--->x + x--->y + y--->rooty。
易看出x--->y的偏移量就是d-1(原题中标明的关系)
用代码写出就是relation【x】= (-relation【x】+d-1+3 + relation【y】)%3;
void merge(int x,int y,int d)
{
int t1=getf(x);
int t2=getf(y);
if(t1!=t2)
{
a[t2]=t1;
re[t2]=(re[x]+1-d+3-re[y])%3;
}
else
{
if(d==1&&re[x]!=re[y])
ans++;
else if(d==2)
{
if (re[x]==1 && re[y]!=0)
ans++;
if (re[x]==2 && re[y]!=1)
ans++;
if (re[x]==0 && re[y]!=2)
ans++;
}
}
注意了这两个地方,基本就出来了。AC Code:
#include<cstdio>
using namespace std;
int a[50005];
int re[50005];
int n,k;
int ans;
void init()
{
for(int i=1;i<=n;i++)
a[i]=i;
}
int getf(int x)
{
if(a[x]==x)
return x;
int temp=a[x];
a[x]=getf(a[x]);
re[x]=(re[x]+re[temp])%3;
return a[x];
}
void merge(int x,int y,int d)
{
int t1=getf(x);
int t2=getf(y);
if(t1!=t2)
{
a[t2]=t1;
re[t2]=(re[x]+1-d+3-re[y])%3;
}
else
{
if(d==1&&re[x]!=re[y])
ans++;
else if(d==2)
{
if (re[x]==1 && re[y]!=0)
ans++;
if (re[x]==2 && re[y]!=1)
ans++;
if (re[x]==0 && re[y]!=2)
ans++;
}
}
}
int main()
{
ans=0;
scanf("%d%d",&n,&k);
init();
int d,x,y;
for(int i=0;i<k;i++)
{
scanf("%d %d %d",&d,&x,&y);
if(x>n||y>n||(d==2&&x==y))
{
ans++;
continue;
}
merge(x,y,d);
}
printf("%d\n",ans);
return 0;
}
(二)最小生成树
最小生成树有两种算法:Kruskal算法,Prim算法。
先说kruskal算法,先把所有已知边按边权值进行排序,再按照边顺序进行合并,合并过程中,若两点连接会构成圈(会不符合树的定义)则跳过,直到连通了一棵树,即为最小生成树。边数==点数-1。基本属于模板题。。
prim算法很类似最短路算法,首先选择任意一个点加入生成树,接下来要找出一条边添加到生成树,这需要枚举每一个树顶点到非生成树顶点所有的边,然后找到最短边加入生成树。重复操作n-1次直到所有点都加入生成树。
附模板:
kruskal:
hdu 1863 畅通工程
#include<cstdio>
#include<algorithm>
using namespace std;
struct node{
int u,v;
int w;
}a[1010];
int b[1010];
bool cmp(node a,node b)
{
return a.w<b.w;
}
void init(int n)
{
for(int i=1;i<=n;i++)
b[i]=i;
}
int getf(int x)
{
if(b[x]!=x)
return b[x]=getf(b[x]);
else
return x;
}
void merge(int u,int v)
{
int t1,t2;
t1=getf(u);
t2=getf(v);
if(t1!=t2);
b[t2]=t1;
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0)
return 0;
init(m);
for(int i=0;i<n;i++)
scanf("%d %d %d",&a[i].u,&a[i].v,&a[i].w);
sort(a,a+n,cmp);
int ans=0,cnt=0;
for(int i=0;i<n;i++)
{
if(getf(a[i].u)!=getf(a[i].v))
{
merge(a[i].u,a[i].v);
ans+=a[i].w;
cnt++;
}
}
if(cnt<m-1)
{
printf("?\n");
continue;
}
printf("%d\n",ans);
}
return 0;
}
prim:
emmm....以后再补吧。。^-^
(三)单调栈
一种可以记录左边或右边比第一个比本身大或小的元素的位置的数据结构
具体有四种:
(1)从左向右维护一个递减的单调栈,L数组中存的是左边第一个比自己大的元素的位置
(2)从左向右维护一个递增的单调栈,L数组中存的是左边第一个比自己小的元素的位置
(3)从右向左维护一个递减的单调栈,R数组中存的是右边第一个比自己大的元素的位置
(4)从右向左维护一个递增的单调栈,R数组中存的是右边第一个比自己小的元素的位置
以从左到右维护递减栈为例:
栈中的元素呈递减态,新元素入栈时会pop掉比自己比自己小的元素,pop出去的元素的数量就是到比自己大的元素之前元素的数量,最后一个pop出去的元素就是栈中比自己小的最大的元素。
对一个序列维护完后,栈底元素为该序列的最大值。
上一道经典例题
HDU 1506
对,没错,就是那道求最大矩形面积的题
Sample Input
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
Sample Output
8
4000
思路就是找出每个高度的最大延伸区间,枚举各个高度可形成的面积,记录下最大值即可。
求延伸区间就用到单调栈了,两边维护递增栈,求出两边比当前高H小的元素的位置,r-l-1就是矩形的底。
#include<cstdio>
#include<stack>
#include<algorithm>
using namespace std;
stack<int>s;
long long l[100010];
long long r[100010];
long long h[100010];
int main()
{
int n;
while(~scanf("%d",&n))
{
if(n==0)
return 0;
for(int i=0;i<n;i++)
scanf("%d",&h[i]);
while(s.size()) s.pop();
for(int i=0;i<n;i++)
{
while(!s.empty()&&h[s.top()]>=h[i])
s.pop();
if(s.empty())
l[i]=0;
else
l[i]=s.top()+1;
s.push(i);
}
while(s.size()) s.pop();
for(int i=n-1;i>=0;i--)
{
while(!s.empty()&&h[s.top()]>=h[i])
s.pop();
if(s.empty())
r[i]=n;
else
r[i]=s.top();
s.push(i);
}
long long ans=0;
for(int i=0;i<n;i++)
ans=max(ans,h[i]*(r[i]-l[i]));
printf("%lld\n",ans);
}
return 0;
}
由这道题延伸出两道题,和矩形这个题一样的思路
POJ 3494 求出0 1矩阵中1组成的最大矩形
把矩形题二维化,再多一个纵向上的遍历求最大值
POJ 2796 求出一个数列中的一个区间,这个区间的最小值乘以这个区间元素的和是这个数列中最大的
和矩形题不要太相似。。。把连续同高的矩形挤压在一起了。。。
再来个另一种类型的:
poj 3250 站队看人问题:每个人都向右看,只能看见比自己矮的人的头顶,看不见比自己高的人和被高的人挡住的头顶,求每个人可看见的人的数量的总和。
思路:找出右边第一个比自己高的人位置,r【i】-i-1就是当前这个人看见的人数。
#include<cstdio>
#include<stack>
using namespace std;
long long a[80010];
long long r[80010];
stack<long long>s;
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
for(int i=n;i>=0;i--)
{
while(s.size()&&a[s.top()]<a[i])
s.pop();
if(s.empty())
r[i]=n+1;
else
r[i]=s.top();
s.push(i);
}
long long sum=0;
for(int i=1;i<=n;i++)
sum+=r[i]-i-1;
printf("%lld\n",sum);
return 0;
}
还有另一种思路:别人看见的人的人数和==被看见的别人的人数和。所以可以求出每个人被多少人看见的数量的和。
从左向右维护递减栈,新元素入栈前维护后,栈内元素的数量就是左边比当前元素大的数量
#include<cstdio>
#include<stack>
using namespace std;
stack<int>s;
long long a[80010];
int main()
{
int n;
scanf("%d",&n);
long long ans=0;
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
while(!s.empty()&&a[s.top()]<=a[i])
s.pop();
ans+=s.size();
s.push(i);
}
printf("%lld\n",ans);
return 0;
}
最后一道:HDU 3410
题意是找出每个元素两边比本元素小的最大的元素的位置,若没有就是0。
两边维护递减栈,利用新元素入栈时会pop出比自己的小的元素,pop出的最后一个元素就是左或右比自己小的最大的元素。
#include<cstdio>
#include<stack>
using namespace std;
int h[50010];
int l[50010];
int r[50010];
stack<int>s;
int main()
{
int t;
scanf("%d",&t);
int count=1;
while(t--)
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&h[i]);
while(s.size()) s.pop();
for(int i=0;i<n;i++)
{
if(!s.size()) l[i] = 0;
else if(h[i] > h[s.top()])
{
while(s.size() && h[i] > h[s.top()])
{
l[i] = s.top()+1;
s.pop();
}
}
else l[i] = 0;
s.push(i);
}
while(s.size()) s.pop();
for(int i=n-1;i>=0;i--)
{
if(!s.size()) r[i] = 0;
else if(h[i] > h[s.top()])
{
while(s.size() && h[i] > h[s.top()])
{
r[i] = s.top()+1;
s.pop();
}
}
else r[i] = 0;
s.push(i);
}
printf("Case %d:\n",count++);
for(int i=0;i<n;i++)
{
printf("%d %d\n",l[i],r[i]);
}
}
return 0;
}
差不多就是学了这些吧。。。
二、实验经验
1、限时训练注意跟榜
2、开数组注意大小,尤其是复制模板时。。
3、TLE并不一定是程序超时,可能是数组开小,int类型与long long类型用的不对,或是结构体数组重复声明
4、不要紧张^-^
三、额外收获
1、与单调栈类似的数据结构:单调队列,,并不是特别理解,附一个链接吧
https://blog.csdn.net/a_bright_ch/article/details/77076228
2、unique函数,会将容器元素去重,并将重复元素移至数组尾部,返回去重后的元素尾部的地址
size=unique(b,b+n)-b。
附舶来品:
#include <iostream>
#include <cassert>
#include <algorithm>
#include <vector>
#include <string>
#include <iterator>
using namespace std;
int main()
{
const int N=11;
int array1[N]={1,2,0,3,3,0,7,7,7,0,8};
vector<int> vector1;
for (int i=0;i<N;++i)
vector1.push_back(array1[i]);
vector<int>::iterator new_end;
new_end=unique(vector1.begin(),vector1.end()); //"删除"相邻的重复元素
assert(vector1.size()==N);
vector1.erase(new_end,vector1.end()); //删除(真正的删除)重复的元素
copy(vector1.begin(),vector1.end(),ostream_iterator<int>(cout," "));
cout<<endl;
return 0;
}
第一周结束了,,感觉自己和大佬差距好大,,革命尚未成功,同志仍需努力啊!!!