天梯赛高频考点3:DFS深搜

L2-020 功夫传人

作者 陈越

单位 浙江大学

一门武功能否传承久远并被发扬光大,是要看缘分的。一般来说,师傅传授给徒弟的武功总要打个折扣,于是越往后传,弟子们的功夫就越弱…… 直到某一支的某一代突然出现一个天分特别高的弟子(或者是吃到了灵丹、挖到了特别的秘笈),会将功夫的威力一下子放大N倍 —— 我们称这种弟子为“得道者”。

这里我们来考察某一位祖师爷门下的徒子徒孙家谱:假设家谱中的每个人只有1位师傅(除了祖师爷没有师傅);每位师傅可以带很多徒弟;并且假设辈分严格有序,即祖师爷这门武功的每个第i代传人只能在第i-1代传人中拜1个师傅。我们假设已知祖师爷的功力值为Z,每向下传承一代,就会减弱r%,除非某一代弟子得道。现给出师门谱系关系,要求你算出所有得道者的功力总值。

输入格式:

输入在第一行给出3个正整数,分别是:N(≤105)——整个师门的总人数(于是每个人从0到N−1编号,祖师爷的编号为0);Z——祖师爷的功力值(不一定是整数,但起码是正数);r ——每传一代功夫所打的折扣百分比值(不超过100的正数)。接下来有N行,第i行(i=0,⋯,N−1)描述编号为i的人所传的徒弟,格式为:

Ki​ ID[1] ID[2] ⋯ ID[Ki​]

其中Ki​是徒弟的个数,后面跟的是各位徒弟的编号,数字间以空格间隔。Ki​为零表示这是一位得道者,这时后面跟的一个数字表示其武功被放大的倍数。

输出格式:

在一行中输出所有得道者的功力总值,只保留其整数部分。题目保证输入和正确的输出都不超过1010。

输入样例:

10 18.0 1.00
3 2 3 5
1 9
1 4
1 7
0 7
2 6 1
1 8
0 9
0 4
0 3

输出样例:

404
#include 
using namespace std;

const int MAX_PEOPLE = 1e5 + 5;
vector lineage[MAX_PEOPLE];
int total_people;
int amplification[MAX_PEOPLE];
double initial_power, discount_rate, total_power;

// 深度优先搜索函数
void dfs(int person_id, double current_power) {
    if (amplification[person_id]) {
        total_power += current_power * amplification[person_id];
    }
    for (int &apprentice_id : lineage[person_id]) {
        dfs(apprentice_id, current_power * discount_rate);
    }
}

int main() {
    cin >> total_people >> initial_power >> discount_rate;
    discount_rate = (100.0 - discount_rate) / 100.0;

    for (int i = 0; i < total_people; i++) {
        int num_apprentices;
        cin >> num_apprentices;
        if (num_apprentices == 0) {
            cin >> amplification[i];
        } else {
            for (int j = 0; j < num_apprentices; j++) {
                int apprentice_id;
                cin >> apprentice_id;
                lineage[i].push_back(apprentice_id);
            }
        }
    }

    dfs(0, initial_power); //从祖师爷开始计算功力

    cout << (int)total_power; //输出所有得道者的总功力,保留整数部分

    return 0;
}

L2-043 龙龙送外卖

作者 DAI, Longao

单位 杭州百腾教育科技有限公司

龙龙是“饱了呀”外卖软件的注册骑手,负责送帕特小区的外卖。帕特小区的构造非常特别,都是双向道路且没有构成环 —— 你可以简单地认为小区的路构成了一棵树,根结点是外卖站,树上的结点就是要送餐的地址。

每到中午 12 点,帕特小区就进入了点餐高峰。一开始,只有一两个地方点外卖,龙龙简单就送好了;但随着大数据的分析,龙龙被派了更多的单子,也就送得越来越累……

看着一大堆订单,龙龙想知道,从外卖站出发,访问所有点了外卖的地方至少一次(这样才能把外卖送到)所需的最短路程的距离到底是多少?每次新增一个点外卖的地址,他就想估算一遍整体工作量,这样他就可以搞明白新增一个地址给他带来了多少负担。

输入格式:

输入第一行是两个数 N 和 M (2≤N≤105, 1≤M≤105),分别对应树上节点的个数(包括外卖站),以及新增的送餐地址的个数。

接下来首先是一行 N 个数,第 i 个数表示第 i 个点的双亲节点的编号。节点编号从 1 到 N,外卖站的双亲编号定义为 −1。

接下来有 M 行,每行给出一个新增的送餐地点的编号 Xi​。保证送餐地点中不会有外卖站,但地点有可能会重复。

为了方便计算,我们可以假设龙龙一开始一个地址的外卖都不用送,两个相邻的地点之间的路径长度统一设为 1,且从外卖站出发可以访问到所有地点。

注意:所有送餐地址可以按任意顺序访问,且完成送餐后无需返回外卖站

输出格式:

对于每个新增的地点,在一行内输出题目需要求的最短路程的距离。

输入样例:

7 4
-1 1 1 1 2 2 3
5
6
2
4

输出样例:

2
4
4
6
#include 
using namespace std;

const int MAX_NODES = 100010;
int parentNode[MAX_NODES];    //存储每个节点的父节点编号,外卖站的父节点为 -1
int visitDepth[MAX_NODES];    //记录从节点到已访问或根节点的距离
int maxReachedDepth = 0;      //当前访问路径中访问到的最深深度

//深度优先搜索,返回新增的边数贡献
int dfsCompute(int currentNode, int currentDepth) {
    //如果到达根节点(父节点为 -1)或节点已被访问
    if (parentNode[currentNode] == -1 || visitDepth[currentNode] != 0) {
        //更新最大深度:当前深度 + 已访问深度
        maxReachedDepth = max(maxReachedDepth, currentDepth + visitDepth[currentNode]);
        //到根节点来回的边数为 currentDepth * 2
        return currentDepth * 2;
    }
    //继续向父节点探索,深度加一
    int addedEdges = dfsCompute(parentNode[currentNode], currentDepth + 1);
    //设置当前节点的已访问深度为父节点的已访问深度 + 1
    visitDepth[currentNode] = visitDepth[parentNode[currentNode]] + 1;
    return addedEdges;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int numNodes, numQueries;
    cin >> numNodes >> numQueries;

    //读入每个节点的父节点
    for (int i = 1; i <= numNodes; i++) {
        cin >> parentNode[i];
    }

    int totalDistance = 0;  //累积的双向边数总和
    while (numQueries--) {
        int deliveryPoint;
        cin >> deliveryPoint;
        //计算新增的双向边数
        totalDistance += dfsCompute(deliveryPoint, 0);
        //最短路径 = 累积双向边数 - 最大深度(单程)
        int minTravel = totalDistance - maxReachedDepth;
        cout << minTravel << '\n';
    }

    return 0;
}

L2-047 锦标赛

作者 DAI, Longao

单位 杭州百腾教育科技有限公司

有 2k 名选手将要参加一场锦标赛。锦标赛共有 k 轮,其中第 i 轮的比赛共有 2k−i 场,每场比赛恰有两名选手参加并从中产生一名胜者。每场比赛的安排如下:

  • 对于第 1 轮的第 j 场比赛,由第 (2j−1) 名选手对抗第 2j 名选手。
  • 对于第 i 轮的第 j 场比赛(i>1),由第 (i−1) 轮第 (2j−1) 场比赛的胜者对抗第 (i−1) 轮第 2j 场比赛的胜者。

第 k 轮唯一一场比赛的胜者就是整个锦标赛的最终胜者。
举个例子,假如共有 8 名选手参加锦标赛,则比赛的安排如下:

  • 第 1 轮共 4 场比赛:选手 1 vs 选手 2,选手 3 vs 选手 4,选手 5 vs 选手 6,选手 7 vs 选手 8。
  • 第 2 轮共 2 场比赛:第 1 轮第 1 场的胜者 vs 第 1 轮第 2 场的胜者,第 1 轮第 3 场的胜者 vs 第 1 轮第 4 场的胜者。
  • 第 3 轮共 1 场比赛:第 2 轮第 1 场的胜者 vs 第 2 轮第 2 场的胜者。

已知每一名选手都有一个能力值,其中第 i 名选手的能力值为 ai​。在一场比赛中,若两名选手的能力值不同,则能力值较大的选手一定会打败能力值较小的选手;若两名选手的能力值相同,则两名选手都有可能成为胜者。

令 li,j​ 表示第 i 轮第 j 场比赛 败者 的能力值,令 w 表示整个锦标赛最终胜者的能力值。给定所有满足 1≤i≤k 且 1≤j≤2k−i 的 li,j​ 以及 w,请还原出 a1​,a2​,⋯,an​。

输入格式:

第一行输入一个整数 k(1≤k≤18)表示锦标赛的轮数。
对于接下来 k 行,第 i 行输入 2k−i 个整数 li,1​,li,2​,⋯,li,2k−i​(1≤li,j​≤109),其中 li,j​ 表示第 i 轮第 j 场比赛 败者 的能力值。
接下来一行输入一个整数 w(1≤w≤109)表示锦标赛最终胜者的能力值。

输出格式:

输出一行 n 个由单个空格分隔的整数 a1​,a2​,⋯,an​,其中 ai​ 表示第 i 名选手的能力值。如果有多种合法答案,请输出任意一种。如果无法还原出能够满足输入数据的答案,输出一行 No Solution
请勿在行末输出多余空格。

输入样例1:

3
4 5 8 5
7 6
8
9

输出样例1:

7 4 8 5 9 8 6 5

输入样例2:

2
5 8
3
9

输出样例2:

No Solution
#include 
using namespace std;

static const int MAX_ROUNDS = 18;
// loserStrength[round][match] 存储第 round 轮第 match 场比赛的败者能力
int loserStrength[MAX_ROUNDS + 1][1 << MAX_ROUNDS];
// playerStrength[i] 存储第 i 号选手的能力值(1-indexed)
vector playerStrength;
int rounds;  // 总轮数

// 尝试在第 roundIndex 轮、第 matchIndex 场比赛中,胜者能力为 winnerStrength 的情况下,重构选手能力值
bool reconstructBracket(int roundIndex, int matchIndex, int winnerStrength) {
    int loserVal = loserStrength[roundIndex][matchIndex];
    // 第一轮:直接分配两名选手
    if (roundIndex == 1) {
        if (winnerStrength >= loserVal) {
            int leftPlayer  = 2 * matchIndex - 1;
            int rightPlayer = 2 * matchIndex;
            playerStrength[leftPlayer]  = loserVal;
            playerStrength[rightPlayer] = winnerStrength;
            return true;
        }
        return false;
    }
    // 胜者能力必须不小于败者能力
    if (winnerStrength < loserVal) {
        return false;
    }
    // 子比赛索引
    int leftMatch  = 2 * matchIndex - 1;
    int rightMatch = 2 * matchIndex;
    // 情况1:胜者在左子区,败者在右子区
    if (reconstructBracket(roundIndex - 1, leftMatch, winnerStrength)
        && reconstructBracket(roundIndex - 1, rightMatch, loserVal)) {
        return true;
    }
    // 情况2:胜者在右子区,败者在左子区
    if (reconstructBracket(roundIndex - 1, leftMatch, loserVal)
        && reconstructBracket(roundIndex - 1, rightMatch, winnerStrength)) {
        return true;
    }
    return false;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> rounds;
    // 读取每轮的败者能力
    for (int r = 1; r <= rounds; ++r) {
        int matchCount = 1 << (rounds - r);
        for (int m = 1; m <= matchCount; ++m) {
            cin >> loserStrength[r][m];
        }
    }
    int championStrength;
    cin >> championStrength;

    int playerCount = 1 << rounds;
    playerStrength.assign(playerCount + 1, 0);

    // 从最终比赛开始重构
    if (!reconstructBracket(rounds, 1, championStrength)) {
        cout << "No Solution";
        return 0;
    }
    // 输出所有选手的能力值
    for (int i = 1; i <= playerCount; ++i) {
        cout << playerStrength[i] << (i < playerCount ? ' ' : '\n');
    }
    return 0;
}

L2-052 吉利矩阵

作者 陈越

单位 浙江大学

所有元素为非负整数,且各行各列的元素和都等于 7 的 3×3 方阵称为“吉利矩阵”,因为这样的矩阵一共有 666 种。
本题就请你统计一下,把 7 换成任何一个 [2,9] 区间内的正整数 L,把矩阵阶数换成任何一个 [2,4] 区间内的正整数 N,满足条件“所有元素为非负整数,且各行各列的元素和都等于 L”的 N×N 方阵一共有多少种?

输入格式:

输入在一行中给出 2 个正整数 L 和 N,意义如题面所述。数字间以空格分隔。

输出格式:

在一行中输出满足题目要求条件的方阵的个数。

输入样例:

7 3

输出样例:

666

#include 
using namespace std;
const int MAX_SIZE = 5; // 最大的矩阵阶数
int targetSum, matrixSize, solutionCount = 0; // 目标和、矩阵阶数、符合条件的方阵数量
int rowSum[MAX_SIZE], colSum[MAX_SIZE]; // 每行和每列的当前累积和

// 深度优先搜索递归函数
void search(int cellIndex) {
    // 如果所有单元格都已经填写完毕,说明找到一个符合条件的方阵
    if (matrixSize * matrixSize == cellIndex) {
        solutionCount++; // 统计结果
        return;
    }

    // 遍历当前单元格可以填入的所有可能的非负整数值
    for (int value = 0; value <= targetSum; value++) {
        int rowIndex, colIndex; // 当前单元格所在的行和列
        rowIndex = cellIndex / matrixSize; // 行索引
        colIndex = cellIndex % matrixSize; // 列索引

        // 如果填入当前值会导致行或列的和超过目标和,则跳过
        if (rowSum[rowIndex] + value > targetSum || colSum[colIndex] + value > targetSum) continue;

        // 如果是最后一行,则确定当前列的值,使得列的和正好等于目标和
        if (rowIndex == matrixSize - 1) value = targetSum - colSum[colIndex];
        // 如果是最后一列,则确定当前行的值,使得行的和正好等于目标和
        if (colIndex == matrixSize - 1) value = targetSum - rowSum[rowIndex];

        // 更新当前行和列的累积和
        rowSum[rowIndex] += value;
        colSum[colIndex] += value;

        // 递归处理下一个单元格
        search(cellIndex + 1);

        // 回溯,撤销当前选择
        rowSum[rowIndex] -= value;
        colSum[colIndex] -= value;
    }
}

int main() {
    // 输入目标和和矩阵阶数
    cin >> targetSum >> matrixSize;

    // 从第一个单元格开始搜索
    search(0);

    // 输出符合条件的方阵总数
    cout << solutionCount;
}

你可能感兴趣的:(深度优先,算法)