2021“MINIEYE杯”中国大学生算法设计超级联赛(4)(字符串,最小树形图,莫队未补)

2021“MINIEYE杯”中国大学生算法设计超级联赛(4)

导语

做出的题不多,有许多新的知识点需要学习

涉及的知识点

思维、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

题目大意:

思路:莫队

代码


参考文献

  1. 2021“MINIEYE杯”中国大学生算法设计超级联赛(4)1002. Kanade Loves Maze Designing(DFS/树状数组)
  2. HDU6992/ 2021“MINIEYE杯”中国大学生算法设计超级联赛(4)Lawn of the Dead(线段树/好题)详细题解
  3. 最小树形图——朱刘算法
  4. 朱刘算法学习笔记
  5. 学习笔记:朱刘算法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值