导语
做出的题不多,有许多新的知识点需要学习
涉及的知识点
思维、DFS、后缀数组/后缀自动机、单调栈,有向生成树、线段树、莫队
链接:2021“MINIEYE杯”中国大学生算法设计超级联赛(4)
题目
1002
题目大意:n个点的树,每个点有权值,权值都为1 ~ n之间,多次查询,每次询问点对之间有多少种不同的值,输入第一行为样例个数,每个样例第一行为点数,接下来一行为n-1个点代表当前第i个点与第pi个点相连,第三行为点权,输出详见原题
思路:DFS+树状数组,最后输出的形式是一个遍历的结果,因此本题对时间应该卡的不是很严,而且点的范围也不大,考虑以每个点为树根进行DFS,问题被转换为当以当前点为树根时,其余点到达当前点的路径上有多少种点权,由于点权的范围给定了为一个较小的确定范围,可以用树状数组来统计已经出现的点权,每搜索过一个点后进行树状数组的更新操作,遍历该点关联的边,搜索过一条边后后需要重新减去已经被记录的值,为下一条边连接的点做准备
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
#define int long long
using namespace std;
const int maxn=1e5+10;
const int mod2=1e9+9,mod1=1e9+7;
const int C=19560929;
int head[maxn],cnt,T,n,val[maxn];
int tree[maxn],num[maxn],p1[maxn],p2[maxn];
struct node {//链式前向星
int to,next;
} e[maxn];
void Add(int to,int from) {//存边
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
void update(int x,int y) {//更新
for(; x<=n; x+=x&-x)
tree[x]+=y;
}
int query(int x) {//询问
int ans=0;
for(; x; x-=x&-x)
ans+=tree[x];
return ans;
}
void DFS(int u,int fa) {
num[u]=query(n);//统计在数值小于n的范围内出现过多少种值
//即到达u的时候当前已经出现了多少种值
for(int i=head[u]; i; i=e[i].next) {//遍历关联的边
int v=e[i].to;
if(v==fa)continue;
int t=query(val[v])-query(val[v]-1);//判断这个值有没有出现过,获得出现个数
if(!t)//如果没出现过,标记
update(val[v],1);
DFS(v,u);
if(!t)//如果没出现过,去除标记
update(val[v],-1);
}
}
signed main() {
scanf("%lld",&T);
p1[1]=p2[1]=1;
for(int i=2; i<=2000; i++) {//预处理给定的值的幂数取余
p1[i]=p1[i-1]*C%mod1;
p2[i]=p2[i-1]*C%mod2;
}
while(T--) {
cnt=0;
memset(head,0,sizeof(head));//清空
scanf("%lld",&n);
for(int i=1; i<=n+10; i++)tree[i]=0;
for(int i=2; i<=n; i++) {//建树,注意i的取值
int p;
scanf("%lld",&p);
Add(i,p);
Add(p,i);
}
for(int i=1; i<=n; i++)//录入值
scanf("%lld",&val[i]);
for(int i=1; i<=n; i++) {
update(val[i],1);//代表当前值已经出现过
DFS(i,0);
int ans1=0,ans2=0;
for(int j=1; j<=n; j++) {
ans1=(ans1+num[j]*p1[j]%mod1)%mod1;//累和
ans2=(ans2+num[j]*p2[j]%mod2)%mod2;
}
update(val[i],-1);//去掉当前值
printf("%lld %lld\n",ans1,ans2);
}
}
return 0;
}
1004
题目大意:
思路:二分+后缀树组/后缀自动机
代码
1006
题目大意:求以各个点为根节点的有向最小生成树
思路:有向最小生成树
代码
#include <iostream>
#include <vector>
#define N 1000001
using namespace std;
typedef long long ll;
const ll inf = 4e13;
struct edge {
int u, v;//起点,重点
ll w;//权值
} es[N];
int ls[N], rs[N], dis[N];
ll val[N], tag[N];
void update(int x, ll t) {
val[x] += t;
tag[x] += t;
}
void push_down(int x) {
if (ls[x]) update(ls[x], tag[x]);
if (rs[x]) update(rs[x], tag[x]);
tag[x] = 0;
}
int Merge(int x, int y) {
if (!x || !y) return x | y;
if (val[x] > val[y]) swap(x, y);
push_down(x);
rs[x] = Merge(rs[x], y);
if (dis[ls[x]] < dis[rs[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
int top[N], fa[N], ine[N];
int f[N];
int Find(int x) {
return f[x] ? f[x] = Find(f[x]) : x;
}
vector<int> ch[N];
ll ans[N];
void dfs(int u, ll s) {
if (ch[u].empty())
ans[u] = s >= inf ? -1 : s;
else for (int v : ch[u])
dfs(v, s - val[ine[v]]);
}
void solve(int n, int m) {//n个节点,m条边
for (int i = 1; i <= 2 * n; ++i) top[i] = fa[i] = ine[i] = f[i] = 0, ch[i].clear();
//初始化
for (int i = 1; i <= n; ++i) es[++m] = { i % n + 1, i, inf };//构造无穷大边,便于排序
for (int i = 1; i <= m; ++i) {
ls[i] = rs[i] = tag[i] = dis[i] = 0;
val[i] = es[i].w;
top[es[i].v] = Merge(top[es[i].v], i);
}
int x = 1;
while (top[x]) {
int i = top[x], y = Find(es[i].u);
top[x] = Merge(ls[i], rs[i]);
if (y == x) continue;
ine[x] = i;
if (!ine[es[i].u]) x = y;
else for (int z = ++n; x != z; x = Find(es[ine[x]].u)) {
fa[x] = f[Find(x)] = z;
ch[z].push_back(x);
if (top[x]) update(top[x], -val[ine[x]]);
top[z] = Merge(top[z], top[x]);
}
}
ll sum = 0;
for (int i = 1; i <= n; ++i)
sum += val[ine[i]];
dfs(n, sum);
}
int main(void) {
int T;
scanf("%d", &T);//扫描样例数量
while (T--) {
int n, m;
scanf("%d %d", &n, &m);//节点数,边数
for (int i = 1; i <= m; ++i)//扫有向边
scanf("%d %d %lld", &es[i].u, &es[i].v, &es[i].w);
solve(n, m);
for (int i = 1; i <= n; ++i)//输出结果
printf("%lld\n", ans[i]);
}
return 0;
}
1008
题目大意:一个 n × m n×m n×m的矩阵,从起点出发,只能向下或者向右走,有些格子不能走,统计矩阵中能走的各自的数量
思路:线段树,通过算出不能到达的点的数目,之后用总数去减,对于一个点不可达,其充分必要条件为其上方和左方均不可达,由于相邻行彼此间存在影响,所以以行为单位处理,每一行被不可达格分成若干区间,对于第i行的区间[l,r],假设l-1和r+1为不可达格,找到i-1行的一个不可达区域[l+1,x],可以推出第i行[l+1,x]也不可达,第i行[x+1,r]可达,使用线段树节点对应区间左端点连续的最长不可达区间长度,每个节点维护区间不可达数量以及左端点连续最长不可达区间长度,标记设置-1为初始值,0为清空,1位不可达,其余见代码
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
const int maxn=1e6+10;
int t,n,m,k;
vector<int>level[maxn];
struct node {
int val,lmax,tag;//值,节点对应区间左端点开始最长连续1长度,标记
} Seg[2][maxn];//滚动数组思想,两行线段树
void PushDown(int l,int r,int p,int rt) {
if(Seg[p][rt].tag!=-1) {//tag为0代表把子树lmax清零
Seg[p][rt<<1].val=l*Seg[p][rt].tag;
Seg[p][rt<<1].lmax=l*Seg[p][rt].tag;
Seg[p][rt<<1|1].val=r*Seg[p][rt].tag;
Seg[p][rt<<1|1].lmax=r*Seg[p][rt].tag;
Seg[p][rt<<1].tag=Seg[p][rt<<1|1].tag=Seg[p][rt].tag;
Seg[p][rt].tag=-1;
}
}
void Build(int l,int r,int rt,int p) {
Seg[p][rt].tag=-1;
if(l==r) {
Seg[p][rt].val=Seg[p][rt].lmax=0;
return;
}
int mid=(l+r)>>1;
Build(l,mid,rt<<1,p);
Build(mid+1,r,rt<<1|1,p);
Seg[p][rt].val=Seg[p][rt<<1].val+Seg[p][rt<<1|1].val;
Seg[p][rt].lmax=0;
}
void Update(int p,int L,int R,int v,int l,int r,int rt) {
if(l>r||L>r||l>R)
return ;
if(L<=l&&R>=r) {//更新
Seg[p][rt].val=(r-l+1)*v;
Seg[p][rt].lmax=(r-l+1)*v;
Seg[p][rt].tag=v;
return ;
}
int mid=(l+r)>>1;
PushDown(mid-l+1,r-mid,p,rt);
Update(p,L,R,v,l,mid,rt<<1);
Update(p,L,R,v,mid+1,r,rt<<1|1);
Seg[p][rt].val=Seg[p][rt<<1].val+Seg[p][rt<<1|1].val;
if(Seg[p][rt<<1].val==mid-l+1) Seg[p][rt].lmax=Seg[p][rt<<1].val+Seg[p][rt<<1|1].lmax;
//如果左子树表示的区间全不可达,当前节点的lmax为左区间+右子树lmax
else Seg[p][rt].lmax=Seg[p][rt<<1].lmax;
//否则只用左子树即可
}
int Query(int p,int L,int R,int l,int r,int rt) {
if(l>=L&&R>=r)//返回给定节点上方最长连续1长度
return Seg[p][rt].lmax;
int mid=(l+r)>>1;
PushDown(mid-l+1,r-mid,p,rt);
if(R<=mid)return Query(p,L,R,l,mid,rt<<1);//未真正查询,查询区间不变,找到符合的区间
if(L>mid)return Query(p,L,R,mid+1,r,rt<<1|1);
int tmp=Query(p,L,mid,l,mid,rt<<1);//首先查询给定区间[L,mid]在左子树的情况,只有左子树区间全不可达,右子树的查询才有意义,理解这里非常重要
if(tmp==mid-L+1)return tmp+Query(p,mid+1,R,mid+1,r,rt<<1|1);
//更改了查询长度和查询区间
return tmp;
}
int main() {
//freopen("1008.in","r",stdin);
scanf("%d",&t);
while(t--) {
scanf("%d%d%d",&n,&m,&k);
Build(1,m,1,0);//建好两层树,初始化都不可达
Build(1,m,1,1);
for(int i=0; i<=n+10; i++)level[i].clear();
for(int t=0; t<k; t++) {
int x,y;
scanf("%d%d",&x,&y);
level[x].push_back(y);//以行为基准放入
}
ll ans=0;
Update(0,1,m,1,1,m,1);//0号树,更新范围,更新值,查找范围,根节点
for(int i=1; i<=n; i++) {//按行处理
sort(level[i].begin(),level[i].end());//排序纵坐标获得递增区间
level[i].push_back(m+1);//加入边界
int lst=0;//记录前一个的位置
Update(i&1,1,m,0,1,m,1);//清空之前信息,初始化全可达
for(int j=0; j<level[i].size(); j++) {
int x=level[i][j];
if(i==1&&lst==0) {//特判第一行第一个位置
lst=x;
continue;
}
int lpos=lst+1,rpos=x-1,len=0;//lpos~rpos为需要判断的区间
if(lpos<=rpos) {//说明区间长度存在
len=Query(1^(i&1),lpos,rpos,1,m,1);//上一线段树,查询区间,总区间,根节点
//获得不能走的区间长度
ans+=1ll*len;
} else len=0;
if(!(lpos-1==0&&lpos-1+len==0))//
Update(i&1,lpos-1,lpos-1+len,1,1,m,1);
lst=x;
}
}
printf("%lld\n",1ll*n*m-ans-1ll*k);//总数减去不可达
}
return 0;
}
1011
题目大意:
思路:莫队
代码