【CSDN竞赛第23期】简要题解

文章提供了四道编程竞赛题目,包括网络故障排查的奥数问题、零钱兑换的动态规划解决方案、清理磁盘空间的树形dp策略以及交际圈的无向图连通块计数。每道题均附带题解思路和C++或Golang的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


我明明记得比赛时最后一题一开始做错了,但我加了一条代码改对了,提交后马上(因为相同分数按用时排名)在另外界面点了交卷,但最后没记上分?

1.排查网络故障(小学奥数)

题目

A地跟B地的网络中间有n个节点(不包括A地和B地),相邻的两个节点是通过网线连接。
正常的情况下,A地和B地是可以连通的,有一天,A地和B地突然不连通了,已知只有一段网线出问题(两个相邻的节点)小明需要排查哪段网线出问题。
他的排查步骤是:
1。 选择某个中间节点
2。 在这个节点上判断跟A地B地是否连通,用来判断那一边出问题 请问小明最少要排查多少次,才能保证一定可以找到故障网线

题解

感觉像是小学奥数“找出唯一较轻的假币需要几次”那一类问题……
每次检查某个中间节点,若与A(或B)不连通,说明有问题的网线在节点和A(或B)之间。
每次尽可能检查中间点,从而每次将搜索范围缩小一半,这样在最坏情况下最好。
容易发现,答案就是n的二进制最高位在第几位。

代码

#include <iostream>
#include <string>
#include <sstream>
#include <vector>

int solution(long int n) {
    int result = 0;
    while (n) {
        result++;
        n >>= 1;
    }
    return result;
}

int main() {
    long int n;
    std::cin >> n;
    int result = solution(n);
    std::cout << result << std::endl;
    return 0;
}

2.零钱兑换(动态规划)

题目

给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个aim,代表要找的钱数,求组成aim的最少货币数。
如果无解,请返回-1.
数据范围:
数组大小满足 0 <= n <=10000 , 数组中每个数字都满足 0 < val <=10000,0 <= aim <=100000
要求:时间复杂度 O(n×aim) ,空间复杂度 O(aim)。

题解

动态规划
dp[面值]=最小张数或-1(无答案,其实简单起见也可以设成足够大的特殊值)
初始dp[0]=0, dp[i]=-1
dp[aim] = min(1<=i<=n, dp[aim-val[i]]!=-1){dp[aim-val[i]]+1}
因为“每种面值的货币可以使用任意张”,所以更新时aim从小到大更新,特殊处理-1的情况。

代码

package main

import "fmt"
import "strings"

var arr []int
var dp []int

func str2int(s string) int {
	var x = 0
	for _, ch := range []byte(s) {
		x = x*10 + int(ch-'0')
	}
	return x
}
func main() {
	var str string
	fmt.Scan(&str)
	str = str[1:]
	str2 := strings.Split(str, "]")
	arrS := strings.Split(str2[0], ",")
	aimS := str2[1][1:]
	//fmt.Println(arrS, aimS)
	for _, as := range arrS {
		arr = append(arr, str2int(as))
	}
	aim := str2int(aimS)
	dp = make([]int, aim+1)
	for i := range dp {
		dp[i] = -1
	}
	dp[0] = 0
	for i := range arr {
		for j := 0; j <= aim; j++ {
			if j < arr[i] || dp[j-arr[i]] == -1 {
				continue
			}
			if dp[j] == -1 || (dp[j-arr[i]] != -1 && dp[j-arr[i]]+1 < dp[j]) {
				dp[j] = dp[j-arr[i]] + 1
			}
		}
	}
	fmt.Println(dp[aim])
}

3.清理磁盘空间(树形dp)

题目

小明电脑空间满了,决定清空间。
为了简化问题,小明列了他个人文件夹(/data)中所有末级文件路径和大小,挑选出总大小为 m 的删除方案,求所有删除方案中,删除操作次数最小是多少。
一次删除操作:删除文件或者删除文件夹。如果删除文件夹,那么该文件夹包含的文件都将被删除。
文件夹的大小:文件夹中所有末级文件大小之和。
笔者注:无答案时输出-1

题解

把路径按/切割,建立目录树,将每个文件加到目录树中,同时增加沿途文件夹的大小。

树形dp:
dp[目录树节点号[被删总文件大小]=最小删除次数
对于每个子树(每个文件夹),可以整个删除,或执行以子节点为根子树的中的多个删除:
sum{size} == size[root],有dp[root][sum{sizeii}] = 1
否则dp[root][sum{sizeii}] = min{dp[son1][size1], ..., dp[soni][sizei],...}

代码

这个实现效率不高,因为叶子节点dp[leaf][]只有一个不是-1,整个dp[][]中-1预计也很多,估计用C++的map会好一些。
(因为我不熟悉C++的字符串操作。golang的map是无序的,相当于C++的unordered_map。当时想可能有顺序相关的优化,所以用的数组)

package main

import (
	"fmt"
	"strings"
)

type Node struct {
	str []string
	sz  int
}

var nodes []Node

func str2int(s string) int {
	var x = 0
	for _, ch := range []byte(s) {
		x = x*10 + int(ch-'0')
	}
	return x
}

type Tree struct {
	id int
	mp map[string]*Tree
	sz int
}

var root Tree
var tot int

func add(rt *Tree, node Node, step int) {
	rt.sz += node.sz
	if rt.mp == nil {
		rt.mp = make(map[string]*Tree)
	}
	if len(node.str) == step {
		return
	}
	if rt.mp[node.str[step]] == nil {
		tree := new(Tree)
		tot++
		tree.id = tot
		rt.mp[node.str[step]] = tree
	}
	add(rt.mp[node.str[step]], node, step+1)
}

var dp [][]int
var n, m int

func main() {
	var temp_arr [2]int
	for i := 0; i < 2; i++ {
		fmt.Scan(&temp_arr[i])
	}
	n = temp_arr[0]
	m = temp_arr[1]
	matrix := make([][2]string, n)
	for i := 0; i < n; i++ {
		for j := 0; j < 2; j++ {
			fmt.Scan(&matrix[i][j])
		}
		node := Node{}
		node.str = strings.Split(matrix[i][0], "/")
		node.sz = str2int(matrix[i][1])
		nodes = append(nodes, node)
		add(&root, node, 0)
	}
	dp = make([][]int, tot+1)
	for i := 0; i <= tot; i++ {
		dp[i] = make([]int, m+1)
		for j := range dp[i] {
			dp[i][j] = -1
		}
		dp[i][0] = 0
	}
	dfs(&root)
	fmt.Println(dp[root.id][m])
	//fmt.Println(dp)
}

func dfs(rt *Tree) {
	//fmt.Println(rt.id)
	if rt.sz <= m {
		dp[rt.id][rt.sz] = 1
	}
	if rt.mp == nil {
		// dp[rt.id][rt.sz] = 1
		return
	}
	for _, v := range rt.mp {
		dfs(v)
		for aim := m; aim >= 1; aim-- {
			// 注:叶子节点显然-1数量特别多,应该用map保存更好
			// 没有 son <= v.sz 只能得 90%
			for son := 1; son <= aim && son <= v.sz; son++ {
				if dp[v.id][son] == -1 {
					continue
				}
				if dp[rt.id][aim-son] != -1 &&
					(dp[rt.id][aim] == -1 || dp[rt.id][aim] > dp[rt.id][aim-son]+dp[v.id][son]) {
					dp[rt.id][aim] = dp[rt.id][aim-son] + dp[v.id][son]
				}
			}
		}
	}
}

4.交际圈(无向图连通块个数)

题目

小明参加了个大型聚会。聚会上有n个人参加,我们将他们编号为1…n,有些人已经互相认识了,有些人还不认识。
聚会开始后,假设A跟B认识,A会给所有他认识的人介绍B,原先跟A认识,但不认识B的人,都会在此时,跟B互相认识。
当所有人都把自己认识的人介绍一遍后,此时n个人就会形成k个交际圈,同一个交际圈中,两两互相认识,不同的交际圈之间,互相不认识
问题:当所有人都把自己认识的人介绍一遍后,形成了多少个交际圈.

题解

每一对互相认识关系让两拨人互相认识,抽象就是一条无向边让无向图中两个连通块变成一个连通块。
对于无向图连通块,可以从任何一点开始遍历到所有点
建无向图遍历,标记遍历过的点,总遍历次数就是连通块个数,就是答案。

答案

我明明记得比赛时最后一题一开始做错了,但我加了一条代码改对了,提交后马上(因为相同分数按用时排名)在另外界面点了交卷,但最后没记上分?

#include <iostream>
#include <string>
#include <sstream>
#include <vector>

using namespace std;
vector<int> e[100010];
bool vis[101000];

void dfs(int u) {
    vis[u] = true;
    for (int i = 0; i < (int) e[u].size(); i++) {
        if (vis[e[u][i]])continue;
        dfs(e[u][i]);
    }
}

int main() {
    int n;
    int m;
    std::cin >> n;
    std::cin >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        e[x].push_back(y);
        e[y].push_back(x);
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (vis[i]) continue; // 我明明记得比赛时一开始错了,但我加了这条代码改对了,但最后没记上分?
        ++ans;
        dfs(i);
    }
    cout << ans;
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值