洛谷 P1031 [NOIP 2002 提高组] 均分纸牌

题目描述

有 N 堆纸牌,编号分别为 1,2,…,N。每堆上有若干张,但纸牌总数必为 N 的倍数。可以在任一堆上取若干张纸牌,然后移动。

移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,只能移到编号为 N−1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。

现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。

例如 N=4 时,4 堆纸牌数分别为 9,8,17,6。

移动 3 次可达到目的:

  • 从第三堆取 4 张牌放到第四堆,此时每堆纸牌数分别为 9,8,13,10。
  • 从第三堆取 3 张牌放到第二堆,此时每堆纸牌数分别为 9,11,10,10。
  • 从第二堆取 1 张牌放到第一堆,此时每堆纸牌数分别为 10,10,10,10。

输入格式

第一行共一个整数 N,表示纸牌堆数。
第二行共 N 个整数 A1​,A2​,…,AN​,表示每堆纸牌初始时的纸牌数。

输出格式

共一行,即所有堆均达到相等时的最少移动次数。

输入输出样例

输入 #1

4
9 8 17 6

输出 #1

3

说明/提示

对于 100% 的数据,1≤N≤100,1≤Ai​≤10000。

【题目来源】

NOIP 2002 提高组第一题


题目理解

首先,我们需要清楚地理解题目要求。题目描述如下:

有N堆纸牌,编号分别为1,2,...,N。每堆上有若干张纸牌,但纸牌总数必为N的倍数。可以在任一堆上取若干张纸牌,然后移动到相邻的堆上。要求找出一种移动方法,用最少的移动次数使每堆纸牌数相同。

关键点分析

  1. 纸牌总数是N的倍数:这意味着我们可以计算出每堆纸牌应该有多少张(平均数)。

  2. 移动规则:只能从一堆移动到相邻的堆,每次移动可以移动任意张数。

  3. 目标:找到移动次数最少的方案。

解题思路

基本思想

这道题可以使用贪心算法来解决。贪心算法的核心思想是:在每一步选择中,都采取当前状态下最优的选择,从而希望导致结果是全局最优的。

具体步骤

  1. 计算平均数:首先计算所有堆的平均值,即每堆最终应该有的纸牌数。

  2. 从左到右处理:对于每一堆纸牌,计算它与平均数的差值,并将这个差值传递给右边的堆(即移动纸牌)。

  3. 统计移动次数:每次传递(移动)都算作一次移动操作。

为什么贪心算法有效

因为题目允许我们只能向相邻的堆移动纸牌,所以从左到右(或从右到左)依次处理可以确保每次移动都是必要的,并且不会产生多余的移动。每一步都使当前堆达到平均数,然后将多余的或不足的部分传递给下一个堆。

代码实现

cpp

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;  // 纸牌的堆数
    cin >> n;
    
    vector<int> cards(n);  // 存储每堆纸牌的数量
    int total = 0;         // 纸牌总数
    
    // 输入每堆纸牌的数量并计算总数
    for (int i = 0; i < n; ++i) {
        cin >> cards[i];
        total += cards[i];
    }
    
    int avg = total / n;  // 计算每堆应该有的纸牌数
    int moves = 0;        // 记录移动次数
    
    // 从左到右处理每一堆纸牌
    for (int i = 0; i < n - 1; ++i) {
        if (cards[i] != avg) {
            // 计算需要移动的纸牌数
            int diff = cards[i] - avg;
            // 将多余的或不足的纸牌移动到右边相邻的堆
            cards[i + 1] += diff;
            // 移动次数加1
            moves++;
        }
    }
    
    // 输出最少移动次数
    cout << moves << endl;
    
    return 0;
}

代码详细解析

变量定义

  1. n:纸牌的堆数。

  2. cards:一个动态数组,用于存储每堆纸牌的数量。

  3. total:所有纸牌的总数。

  4. avg:每堆纸牌应该达到的平均数。

  5. moves:记录移动的次数。

输入处理

首先读取纸牌的堆数n,然后读取每堆纸牌的数量并存储在cards数组中,同时计算纸牌的总数total

计算平均数

通过total / n计算每堆纸牌应该达到的平均数avg

贪心处理

从第一堆开始,依次处理每一堆纸牌:

  1. 如果当前堆的纸牌数不等于平均数,计算差值diff

  2. 将这个差值加到右边相邻的堆上(相当于移动纸牌)。

  3. 每次移动操作都会使moves加1。

输出结果

最后输出最少的移动次数moves

示例分析

假设输入如下:

text

4
9 8 17 6

计算步骤

  1. 计算总数:9 + 8 + 17 + 6 = 40。

  2. 平均数:40 / 4 = 10。

  3. 处理每一堆:

    • 第一堆:9 - 10 = -1,需要从第二堆移动1张到第一堆。移动后:10, 7, 17, 6。moves = 1。

    • 第二堆:7 - 10 = -3,需要从第三堆移动3张到第二堆。移动后:10, 10, 14, 6。moves = 2。

    • 第三堆:14 - 10 = 4,需要移动4张到第四堆。移动后:10, 10, 10, 10。moves = 3。

  4. 输出结果:3。

边界情况考虑

  1. 所有堆已经平均:此时不需要任何移动,输出0。

  2. 只有一堆纸牌:显然不需要移动,输出0。

  3. 纸牌数非常大:由于我们只进行简单的算术运算和遍历,算法的时间复杂度是O(n),可以高效处理大规模数据。

算法复杂度分析

  • 时间复杂度:O(n)。我们只需要遍历数组两次(一次输入,一次处理),每次都是线性时间。

  • 空间复杂度:O(n)。需要存储每堆纸牌的数量,空间与堆数成正比。

优化与扩展

虽然当前的算法已经非常高效,但在某些情况下可以进行微优化:

  1. 提前终止:如果在处理过程中发现后面的堆已经满足条件,可以提前终止循环。但在最坏情况下,这不会改变时间复杂度。

  2. 并行处理:对于非常大的n,可以考虑并行计算部分和,但实现复杂且对于本题不必要。

总结

这道题通过贪心算法高效地解决了纸牌均分的问题。关键在于理解每次移动的最优选择是如何影响全局的,以及如何通过简单的差值传递来实现最少移动次数。代码实现简洁明了,适合初学者理解和掌握贪心算法的基本应用。

通过这道题,我们不仅学会了如何解决具体的纸牌移动问题,还掌握了贪心算法在实际问题中的应用方法,这对于解决其他类似的最优化问题具有重要的启发意义。

信息传递问题涉及图论中的最小环查找,具体来说是寻找一个有向图中最小的环的长度。在 P2661 [NOIP2015 提高] 信息传递中,每个节点仅有一个出边,因此可以简化为寻找最小环的问题。 ### 问题分析 本题可以理解为在一个有向图中,每个节点的出度为1,因此图的结构是由若干个环和指向这些环的链成。由于题目要求找出最小的环,因此可以忽略链的部分,仅关注环的长度。 ### 解决思路 1. **图的构建**:使用数 `a` 来表示每个节点的出边,数 `b` 来记录每个节点的入度。 2. **删除链**:通过递归函数 `cut()` 删除入度为0的节点,从而将图中所有链的部分删除,仅保留环。 3. **计算最小环**:遍历剩下的节点,找到最小的环长度。 ### 代码实现 ```cpp #include <bits/stdc++.h> using namespace std; typedef long long LL; const int MAXN = 200010; LL n, ans = 0x7f7f7f7f; LL a[MAXN], b[MAXN]; // 删除入度为0的节点及其边 void cut(LL s) { b[a[s]]--; if (b[a[s]] == 0) cut(a[s]); a[s] = -1; } // 计算环的长度 LL Sum(LL bgn, LL idx, LL s) { if (idx == bgn && s != 0) return s; LL idx_ = a[idx]; a[idx] = 0; return Sum(bgn, idx_, s + 1); } int main() { cin >> n; for (LL u = 1; u <= n; u++) { LL v; cin >> v; a[u] = v; b[v]++; } // 删除入度为0的节点 for (LL i = 1; i <= n; i++) { if (b[i] == 0) cut(i); } // 寻找最小环 for (LL i = 1; i <= n; i++) { if (a[i] > 0) { LL sum_ = Sum(i, i, 0); if (sum_ < ans) ans = sum_; } } cout << ans; return 0; } ``` ### 算法复杂度 - **时间复杂度**:由于每个节点和边仅被处理一次,因此总的时间复杂度为 $O(n)$。 - **空间复杂度**:使用了两个数 `a` 和 `b`,空间复杂度为 $O(n)$。 ### 总结 本题通过图论的最小环问题进行建模,利用递归删除链部分,简化问题后直接寻找最小的环。该方法简单且高效,适用于每个节点出度为1的特殊图结构。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值