2022暑期杭电第六场

本文详细解析了2022年暑期杭电算法竞赛中涉及的图论问题,包括1006题Maex的树形DP、1007题Shinobu loves trip的质数模幂判断、1010题Planar graph的最少边删除策略以及1012题Loop的数组操作优化。通过深入探讨解题思路和代码实现,帮助读者理解图论在算法竞赛中的应用。
摘要由CSDN通过智能技术生成

1006题

Maex

题目链接

题目大意

给出一棵树,以 1 1 1 为根节点,树上的每个节点都有自己的点权 a i a_i ai,以及一个价值 b i b_i bi , b i b_i bi i i i 子树中 a a a 值集合的 M E X MEX MEX (集合中未出现的最小非负整数), a i a_i ai 的值可以为任意的自然数,题目保证 a i a_i ai 的值不同。求 b i b_i bi 加和的最大值。

思路

由题意可以看出,这是一道典型的树形dp问题,通过对样例的模拟可知,只有从根节点到某个叶子节点路径上的点才会对答案有所贡献,并且贡献值为该子树的节点个数。所以我们应当找到一条最长的路径来使得 b i b_i bi 的贡献最大。
所以我们可以用 v a l [ i ] val[i] val[i] 表示以 i i i 叶子节点的子树中得到的 b i b_i bi 加和最大值, c n t [ i ] cnt[i] cnt[i] 记录该节点的节点个数,最后 d f s dfs dfs 即可得到结果。

代码

#include<iostream>
#include<vector>
#define int long long
using namespace std;

const int N = 5e5+10;

vector<int> v[N];
int cnt[N];//节点个数
int val[N];//价值

void dfs(int now,int root){
    int maxm = 0;
    for(auto x:v[now]){//遍历子节点,并找到子节点中的最大值
        if(x == root)continue;
        dfs(x,now);
        cnt[now] += cnt[x] + 1;
        maxm = max(maxm,val[x]);
    }
    val[now] = maxm + cnt[now] + 1;
}


void solve(){
    int n;
    cin>>n;
    for(int i = 1;i <= n;i ++){
        v[i].clear();
        val[i] = 0;
        cnt[i] = 0;
    }
    for(int i = 1;i <= n-1;i ++){
        int x,y;
        cin>>x>>y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
	dfs(1,0);
    cout<<val[1]<<endl;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    cin>>t;
    while(t--)solve();
    system("pause");
    return 0;
}

1007题

Shinobu loves trip

题目链接

题目大意

给定常数 a a a p p p,你会进行 n n n 次旅行,每次旅行的起点是 s s s,第一天的位置在 s s s d d d 天之后的位置是 ( s ⋅ a d )   m o d   P (s\cdot a^d)\ mod\ P (sad) mod P。有 q q q 个询问,每次询问一个位置 x x x,问你一共经过多少次 x x x

思路

判断 x x x 是否会被经过,只需要判断是否存在 0 ≤ k ≤ d 0\le k\le d 0kd 使得 ( s ⋅ a k )   m o d   p = x (s\cdot a^k)\ mod\ p=x (sak) mod p=x,因为 p p p 是质数,所以知道 x x x s s s 就能求出 a k a^k ak

我们可以先预处理出 a 0 , a 1 , ⋯   , a d a^0,a^1,\cdots,a^d a0,a1,,ad 保存在 map 中,然后判断算出来的 a k a^k ak 是否在 map 中,如果不在,则此次旅行一定不经过 x x x;否则,可以求出 k k k,判断 k k k 是否 ≤ d \le d d 即可。

代码

int n, a, p, q, s[N], d[N];
LL inv[N];

LL powmod(LL a, LL b) {
    LL ans = 1;
    while (b) {
        if (b & 1) (ans *= a) %= p;
        (a *= a) %= p;
        b >>= 1;
    }
    return ans;
}

void solve() {
    scanf("%d%d%d%d", &p, &a, &n, &q);
    for (int i = 1; i <= n; i++) {
        scanf("%d%d", &s[i], &d[i]);
        if (s[i]) inv[i] = powmod(s[i], p - 2);
    }
    std::unordered_map<int, int> mp;
    int x = 1, t = 0;
    while (t <= 200000) {
        if (mp.count(x)) break;
        mp[x] = t;
        x = 1LL * x * a % p;
        t++;
    }
    while (q--) {
        int x;
        scanf("%d", &x);
        int ans = 0;
        if (!x) {
            for (int i = 1; i <= n; i++) ans += (s[i] == 0);
        } else {
            for (int i = 1; i <= n; i++)
                if (s[i]) {
                    int k = x * inv[i] % p;
                    if (mp.count(k) && mp[k] <= d[i]) ans++;
                }
        }
        printf("%d\n", ans);
    }
}

1010题

Planar graph

题目链接

题目大意:

给定一个平面图(即图中任意两条边之间不相交),该平面图有 n n n 个顶点, m m m 条边,每条边均有一个编号,编号为 1 , 2 , … , m 1,2,\dots,m 1,2,,m 。设给定平面图中的边围成的平面区域有 k k k 个,分别标记为 1 , 2 , … , k 1,2,\dots,k 1,2,,k 。现在,我们通过给一些边打上通道(相当于将这些边删去),使得平面区域两两之间可以相互到达。求最少需要删除多少边,输出需要删除的边的数量,同时输出所有可行方案中字典序最小的方案。

分析:

通过对图的分析,我们知道,一条边是否需要要被打通(删除),取决于该边是否在一个环上,即取决于该边是否对围成一片新的平面区域有贡献。由于我们要输出字典序最小的方案,所以,在所有的边当中,我们取边的编号最小的那个即可。

判定是否成环有多种方式,这里我们使用并查集来判定是否成环。参考例题——格子游戏。具体实现:我们先将所有的边存起来,初始时,我们将每个点作为一个单独的集合,即令 p [ i ] = i p[i] = i p[i]=i 。之后,我们从编号最大的边开始,从大到小遍历所有的边。 每遍历到一条边,若两个点不在同一个集合,我们便将两个点所在的集合合并;若在同一个集合,则说明这条边的加入刚好使得图中形成了一个新的环,这个时候,我们便将该边的编号加入到答案中。由于是倒序遍历,每次形成一个新的环时,加入的边必定是环上编号最小的边。整体代码实现还算简单。

参考代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
typedef long long ll;

struct Edge {
	int x, y;
}edge[M];

int p[N];

int find(int x) {
	if(p[x] == x) return x;
	else return p[x] = find(p[x]);
}

void solve() {
	int n, m; scanf("%d %d", &n, &m);
	for(int i = 1; i <= m; i ++ ) {
		scanf("%d %d", &edge[i].x, &edge[i].y);
	}
	for(int i = 1; i <= n; i ++ ) p[i] = i;
	vector<int> ans;
	for(int i = m; i > 0; i -- ) {
		int a = edge[i].x, b = edge[i].y;
		int x = find(a), y = find(b);
		if(x == y) ans.push_back(i);
		else p[y] = x;
	}
	sort(ans.begin(), ans.end());
	printf("%d\n", ans.size());
	for(int i = 0; i < ans.size(); i ++ ) {
		printf("%d ", ans[i]);
	}
	puts("");
}

int main() {
	int t; scanf("%d", &t);
	while(t -- ) {
		solve();
	}
	return 0;
}

1012题

Loop

题目链接

题目大意

每次操作可以将数组中的一个数向后移 n n n 位,问 k k k 次操作后字典序的最大值

分析

数据范围较大,故基本采用 O ( n ) O(n) O(n) 的做法。我们可以先把要取的数取出来,然后再一起插到后面。我们可以发现,从前往后比较两个数,当前数比后数小时,就可以把前数取出,然后再比较后数和前数的前数,直到前数比后数大为止,然后再往后比较。可以发现我们是在进行一个维护单调递减栈的操作,当新插入的数比栈顶大时,就将栈顶弹出,直到插入的数比栈顶小,或者是已经进行了 k k k 次操作为止。接下来就可以将弹出的数插入到后面,首先可以对弹出的数降序排列,然后依然是从前往后依次比较,当要插入的数比当前数大的时候,就将数插入到这个数之前,直到插入的数小于当前数为止。

代码

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef double db;

const int N=3e5+1;

int T,n,k;
int a[N],b[N],c[N],s[N];

bool cmp(int a,int b){return a>b;}

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&k);
        for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
        int top=0,now=0;
        //维护单调栈
        for(register int i=1;i<=n;i++)
        {
            if(now>=k) s[++top]=a[i];//已弹出k个数
            else
            {
                while(top&&now<=k&&s[top]<a[i]) b[++now]=s[top--];
                s[++top]=a[i];
            }
        }
        //将弹出数组插入
        sort(b+1,b+now+1,cmp);
        int nows=1,nowb=1,nowc=0;
        for(register int i=1;i<=top;i++)
        {
            if(nowb>now) c[++nowc]=s[i];
            else
            {
                while(b[nowb]>s[i]) c[++nowc]=b[nowb++];
                c[++nowc]=s[i];
            }
        }
        while(nowb<=now) c[++nowc]=b[nowb++];
        
        printf("%d",c[1]);
        for(register int i=2;i<=n;i++) printf(" %d",c[i]);
        printf("\n");
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值