洛谷P2882 [USACO07MAR]Face The Right Way G 贪心思想的数学证明

牛的方向翻转优化
本文介绍了如何通过贪心策略解决一个关于翻转牛的方向的问题,以使所有牛都面向前方,同时给出了数学证明和代码实现。算法核心是每次翻转以遇到的第一个向后牛为首的K头牛,通过枚举K的值找到最小操作次数。

题目

题目描述

Farmer John has arranged his N (1 ≤ N ≤ 5,000) cows in a row and many of them are facing forward, like good cows. Some of them are facing backward, though, and he needs them all to face forward to make his life perfect.

Fortunately, FJ recently bought an automatic cow turning machine. Since he purchased the discount model, it must be irrevocably preset to turn K (1 ≤ K ≤ N) cows at once, and it can only turn cows that are all standing next to each other in line. Each time the machine is used, it reverses the facing direction of a contiguous group of K cows in the line (one cannot use it on fewer than K cows, e.g., at the either end of the line of cows). Each cow remains in the same *location* as before, but ends up facing the *opposite direction*. A cow that starts out facing forward will be turned backward by the machine and vice-versa.

Because FJ must pick a single, never-changing value of K, please help him determine the minimum value of K that minimizes the number of operations required by the machine to make all the cows face forward. Also determine M, the minimum number of machine operations required to get all the cows facing forward using that value of K.

N 头牛排成一列(1<=N<=5000)。每头牛或者向前或者向后。为了让所有牛都面向前方,农夫每次可以将 KK 头连续的牛转向 1<=K<=N,求使操作次数最小的相应 K 和最小的操作次数 M。F 为朝前,B 为朝后。

请在一行输出两个数字 K 和 M,用空格分开。

输入格式

Line 1: A single integer: N

Lines 2..N+1: Line i+1 contains a single character, F or B, indicating whether cow i is facing forward or backward.

输出格式

Line 1: Two space-separated integers: K and M

输入输出样例

输入 #1

7
B
B
F
B
F
B
B

输出 #1

3 3

说明/提示

For K = 3, the machine must be operated three times: turn cows (1,2,3), (3,4,5), and finally (5,6,7)

题解

(引自Delva)

贪心,枚举,差分

因为同一个点翻转两次就与没有翻转的效果相同了,因此我们有一个贪心策略为:

从左到右对于出现的每一个B翻转一次从当前点开始的区间,就能保证是最优解。

样例的模拟:

如果当前翻转的区间长度为3

粗体表示当前下标

B B F B F B B

(B B F) B F B B

F F B B F B B

F B B F B B

F F B B F B B

F F (B B F) B B

F F F F B B B

F F F F B B B

F F F F B B B

F F F F (B B B)

F F F F F F F

F F F F F F F

F F F F F F F

但是我们会发现这样是n^2的,再枚举长度,就变为了n^3.因此,需要对区间翻转差分一下,总时间复杂度就是n^2的了。

代码(自己实现) 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

#ifndef INT_MAX
#define INT_MAX 0x7fffffff
#endif

using namespace std;

const int MAXN = 5005;
int N, M = INT_MAX, ans;
bool a[MAXN], b[MAXN];

int main()
{
    cin >> N;
    for(int i = 1; i <= N; ++i)
    {
        char ch;
        cin >> ch;
        a[i] = ch == 'F';
    }
    for(int K = 1; K <= N; ++K)
    {
        memset(b, 0, sizeof(b)); // b:差分数组
        bool succ = true, g = false;
        int cnt = 0;
        for(int i = 1; i <= N; ++i)
        {
            g ^= b[i]; // '^'相当于'+'或'-'
            if((a[i] ^ g) == false)
            {
                if(i + K - 1 > N)
                {
                    succ = false;
                    break;
                }
                g ^= 1;
                b[i + K] ^= 1;
                ++cnt;
            }
        }
        if(succ)
        {
            if(cnt < M)
            {
                M = cnt;
                ans = K;
            }
        }
    }
    printf("%d %d\n", ans, M);
    return 0;
}

贪心思想数学证明

定理1 对于任意K(1<=K<=N),下面的算法总能使翻转次数最小:

从前往后扫描每只牛,只要发现某只牛朝后,那么就把以这只牛为首的K只牛转向。

同时,如果发现剩余的牛的数量小于K,那么这个K一定不可行。

证明:采用数学归纳法

 ① 当N=K时,若所有牛都朝前,则命题显然成立。

若所有牛都朝后,命题亦显然成立。

若非所有牛朝前或朝后,则不论翻转多少次,总有一部分牛朝前,一部分牛朝后,因此K不可行,命题成立。

② 假设当N=K到L时命题都成立。则当N=L+1时:

(1) 若第一头牛朝前,则这一步不用翻转,往后找到向后的牛(如果存在),假设从这头牛到最后一头牛共有P头,由于K<=P<=L(当P<K时命题K不可行,命题成立),当N=P时所做的翻转最少,因此在N=L+1时所做的翻转仍为最少,故命题成立。

(2) 若第一头牛朝后,则必须翻转以该头牛为首的K头牛。翻转之后往后找到朝后的牛,假设从这头牛到最后一头牛共有P头,正如我们刚才提到的,N=P时命题成立。由于第一头牛朝后,故必须翻转至少一次,而我们只在P头牛的基础上翻转了一次,故仍为最优。

证毕。

<think>题目:洛谷P1700 [USACO19OPEN] Milk Factory B 题目描述:有一个牛奶加工厂,有N个站点(编号1到N),站点之间有传送带,传送带是单向的。现在要找一个站点,使得从这个站点出发,能够到达所有其他站点(允许通过传送带间接到达)。如果存在多个这样的站点,则选择编号最小的。如果没有这样的站点,则输出-1。 输入格式: 第一行:整数N(1≤N≤100),表示站点的数量。 接下来N-1行:每行两个整数a和b,表示有一条从a到b的传送带(注意是单向边,方向是a->b)。 输出格式: 输出满足条件的站点编号,如果没有则输出-1。 分析: 题目要求找到一个点,使得从这个点出发可以到达所有其他点。实际上,这就是在图中寻找一个“根”节点,使得从该节点出发可以遍历整个图(注意图是有向的,且边数为N-1,所以图是树?但注意:题目中并没有保证是树,因为可能有多个父节点?但题目只给了N-1条边,所以实际上是一个有向图,且边数比节点数少1,因此它可能是一个有向无环图(DAG)或者有环?但题目没有说,不过N-1条边,如果有环则无法形成连通图(因为树是无环的),但这里是有向图,所以可能形成树或者森林?然而题目要求能够到达所有其他站点,所以图必须是连通的(弱连通?)。 注意:题目中只给出了N-1条边,所以整个图是一个有向图,且边数比节点数少1,那么它可能是一棵有向树(但方向不一定是从根向叶子,因为题目输入是a->b,所以可能是从a指向b)。但是,题目没有保证边的方向是从根向叶子,所以我们需要考虑。 由于图只有N-1条边,所以整个图可能是一个有向无环图(DAG)并且是一棵树(即基图是一棵树,但方向任意)。但注意:树中边的方向通常是从根向叶子,但这里可能反过来?所以我们需要判断。 然而,题目要求的是:存在一个点,从它出发可以到达所有其他点。那么我们可以尝试对每一个点进行DFS/BFS,检查从这个点出发能否遍历所有节点。因为N最大为100,所以对每个点做一次DFS/BFS,总时间复杂度为O(N*(N+E)) = O(100*(100+99))100*200 = 20000,可以接受。 步骤: 1. 读入N。 2. 构建图:使用邻接表。 3. 对于每个节点i(从1到N),执行BFS/DFS,记录从i出发能够访问到的节点数。 4. 如果能够访问到所有N个节点(包括自己),则记录这个节点。 5. 在记录的所有节点中,选择编号最小的输出;如果没有,输出-1。 注意:图是有向的,所以边a->b,只能从a到b,不能反向。 代码实现: 我们将使用BFS,因为BFS使用队列,非递归,且对于100个节点来说很安全。 具体步骤: 1. 读入N,然后读入N-1行,每行两个整数a和b,构建有向边:graph[a].push_back(b) (注意:题目中传送带方向是a->b)。 2. 对于每个节点i(1~N): 初始化visited数组(大小为N+1,全部为false),初始化队列,将i入队,标记visited[i]=true。 然后进行BFS:每次从队列中取出一个节点u,遍历u的所有邻居v,如果v没有被访问过,则标记并加入队列。 统计访问过的节点数(count)。 3. 如果count等于N,则说明节点i满足条件。 4. 遍历完所有节点后,如果存在满足条件的节点,输出最小的节点编号;否则输出-1。 注意:可能存在多个满足条件的节点,我们只需要最小的那个,所以一旦找到一个满足条件的节点,我们可以记录它的编号,然后继续找,最后取最小的(或者我们按节点编号从小到大遍历,那么第一个满足条件的节点就是最小的,但题目要求多个时取最小,所以我们可以按节点编号从小到大遍历,找到第一个满足条件的就输出并退出?不行,因为可能有多个,但我们需要最小的,所以即使找到一个,后面可能有更小的?不对,节点编号是从1到N,我们从小到大遍历,所以第一个满足条件的节点就是最小的节点?不对,因为节点编号是1,2,...,N,我们按节点编号顺序遍历,那么第一个满足条件的节点就是最小的节点吗?但是,我们遍历的顺序是1,2,...,N,所以当我们找到第一个满足条件的节点时,它一定是编号最小的(因为编号小的先遍历)。所以我们可以这样:从1到N遍历,一旦找到一个节点满足条件,就记录答案并跳出循环?不行,因为可能节点1不满足,节点2满足,但节点3也满足,而节点2比节点3小,所以我们应该记录所有满足条件的节点?然后取最小值?或者因为我们是按从小到大的顺序遍历,所以第一个满足条件的节点就是最小的?不对,因为节点1满足条件,那么它就是最小的;节点1不满足,节点2满足,那么节点2就是当前最小的;节点3满足条件但是比节点2大,所以我们在遍历到节点2时就可以记录并继续找更小的?不对,因为节点编号是递增的,后面不会出现比2更小的节点了。所以当我们按节点编号从小到大遍历时,第一个满足条件的节点就是最小的节点,可以直接输出并结束?不对,因为可能节点1不满足,节点2满足,然后节点3也满足,但是节点2比节点3小,所以我们在节点2满足时就可以输出节点2并结束循环(因为后面节点编号都比2大,所以最小的就是2)。因此,我们可以按节点编号从小到大遍历,一旦遇到一个满足条件的节点,就输出它并结束程序(因为后面即使有满足条件的节点,编号也更大,所以第一个满足条件的节点就是最小的)。如果没有遇到,最后输出-1。 但是,如果我们从1到N遍历,那么第一个满足条件的节点就是最小的,所以我们可以: for (int i=1; i<=N; i++) { 进行BFS,统计从i出发能到达的节点数count if (count == N) { cout << i << endl; return 0; // 结束程序 } } // 如果循环结束还没找到 cout << -1 << endl; 这样是可行的。 但是,注意:题目要求是“如果存在多个这样的站点,则选择编号最小的”,而我们的遍历顺序是1到N,所以第一个找到的满足条件的节点就是最小的,然后直接输出并结束程序。 代码实现: 注意:图可能有环吗?题目中只有N-1条边,所以不可能有环(因为环的存在需要至少与节点数相同的边数,比如一个环需要n条边,而这里只有n-1条边),所以图是无环的,因此不会出现重复访问的问题(但我们的BFS已经用visited数组避免重复访问了,所以即使有环也没关系)。 但是,图可能是不连通的吗?题目要求能够到达所有站点,所以如果图不连通,那么任何一个节点出发都不可能到达所有节点。所以我们的BFS在某个节点开始,如果访问的节点数不足N,就跳过。 由于边数只有N-1,所以整个图可能是一个有向树(基图是无向树,但边有方向)。但是,方向可能不是从根向叶子,所以可能有些节点有多个入度?题目没有说,所以按照输入处理。 我们按照上述思路编写代码。 注意:输入是N-1行,每行两个整数a和b,表示a->b的边。 代码示例:</think>### 解法思路 对于洛谷P1700 [USACO19OPEN] Milk Factory B 问题,需要找到一个站点(节点),使得从该站点出发能通过单向传送带到达所有其他站点。由于图只有 N-1 条边(树形结构),可能存在多个解,需输出编号最小的解,若无解则输出 -1。 #### 关键点 1. **图建模**:将站点视为节点,传送带视为有向边(a→b)。 2. **连通性检查**:对每个节点进行 BFS/DFS,检查是否能访问所有节点。 3. **最优选择**:从小到大遍历节点,找到第一个满足条件的节点即可(因编号递增)。 ### C++ 代码实现 ```cpp #include <iostream> #include <vector> #include <queue> #include <cstring> using namespace std; int main() { int N; cin >> N; vector<vector<int>> graph(N + 1); // 邻接表(索引0不使用) // 构建有向图(注意:共N-1条边) for (int i = 0; i < N - 1; i++) { int a, b; cin >> a >> b; graph[a].push_back(b); // 添加边 a→b } // 检查每个节点作为起点 for (int start = 1; start <= N; start++) { vector<bool> visited(N + 1, false); // 访问标记数组 queue<int> q; int count = 0; // 统计可达节点数 // BFS初始化 q.push(start); visited[start] = true; while (!q.empty()) { int u = q.front(); q.pop(); count++; // 遍历当前节点的所有出边 for (int v : graph[u]) { if (!visited[v]) { visited[v] = true; q.push(v); } } } // 若可达所有节点,输出并结束 if (count == N) { cout << start << endl; return 0; } } // 无解情况 cout << -1 << endl; return 0; } ``` ### 代码解释 1. **输入处理**: - 读取站点数 `N`。 - 构建邻接表 `graph`,存储每条有向边(`a→b` 对应 `graph[a].push_back(b)`)。 2. **BFS遍历**: - 对每个节点 `start` (1 到 N) 进行 BFS: - 初始化 `visited` 数组记录访问状态。 - 使用队列 `q` 进行广度优先搜索,统计可达节点数 `count`。 - 若 `count == N`,说明从 `start` 可达所有节点,输出 `start` 并结束程序。 3. **无解处理**: - 若遍历完所有节点均未找到解,输出 `-1`。 ### 复杂度分析 - **时间复杂度**:O(),最坏情况下对每个节点进行 BFS(每个节点遍历所有边)。 - **空间复杂度**:O(N),用于存储邻接表和访问标记。 ### 相关问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值