[USACO Training] Section 1.3

本文介绍了贪心算法和枚举方法在解决编程竞赛问题中的应用,包括畜栏修复、排序问题、零钱兑换、混合牛奶、组合锁等多个实例,探讨了如何利用贪心策略和不同类型的枚举方法找到最优解。

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

这一节的主题是贪心和枚举。个人认为不剪枝的叫枚举,剪枝的叫搜索。
适当的枚举是许多优秀的解法中必不可少的环节,通常,枚举的东西可以分为这样几类:选择、限制、答案。
1. 选择。基本就是根据题意模拟。
2. 限制。如果题目中有多种限制,我们可以枚举一种,检验是否满足其他限制。有时,答案也可以看作一种限制。
3. 答案。检查答案是否满足约束;如果答案具有单调性,可以二分。

TEXT Greedy Algorithm

Barn Repair [1999 USACO Spring Open]

有一些畜栏排成一列,有的需要覆盖木板。木板的尺寸不限,给定木板数量N,要求最小化覆盖的畜栏总量。

一种角度:从N到(N+1)

这是原文的角度。假设我们已经得到了(N-1)块木板的最优解,怎样扩展到N块木板?这个有点类似于动态规划。一种尝试是,我们要移走覆盖着最长非必要区间的那块板,换成两块,一个在那个区间的左边,一个在那个区间的右边。

我觉得原文的正确性证明不对。这种扩展方式是显然的,关键要证明最优子结构,即N的最优解包含(N-1)的最优解。我的水平还不够把这件事说清,但是,如果采取下面这种角度,一切都很显然。

另一种角度:反过来看

找到第一个必须被覆盖的畜栏和最后一个必须被覆盖的畜栏,问题等价于去掉(N-1)个非必须的区间,区间长度和尽可能大。因为每去掉一个区间,木板的总量增加1。这些区间是互不干扰的,于是取前(N-1)长的区间就好了。

Sorting a three-valued sequence [IOI 1996]

有一个序列,每个元素的取值范围是{1, 2, 3},求通过交换给它排序的最少步数。

算法:计算出1、2、3在有序情况下应该处于的区间。先还原一对对正好错位的元素,再还原所有还没归位的1,最后还原所有未归位的2。

同样不认可那个证明……但是自己也不会。

Friendly Coins - A Counter example [abridged]

探究什么条件下用贪心的策略换零钱是正确的是一个有趣的问题,不知道是否已经解决了。

Topological Sort

通过检查顶点的入/出度,更新,来拓扑排序。这是贪心吗?

PROB Mixing Milk

给定需要的牛奶量,每个农民可供应的牛奶量、每单位牛奶的价格,求满足需要的采购方案的最小代价。

排个序就好啦。ANALYSIS页面好多题解,其实就是两类:用快排的和用计数排序的。我用的是STL sort……

/*
ID: chrt2001
PROG: milk
LANG: C++
*/
#include <cstdio>
#include <algorithm>
using namespace std;
struct Farmer {
    int p, a;
    bool operator<(const Farmer& rhs) const
    {
        return p < rhs.p;
    }
} F[5000];
int main()
{
    freopen("milk.in", "r", stdin);
    freopen("milk.out", "w", stdout);
    int n, m, ans = 0;
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; ++i)
        scanf("%d %d", &F[i].p, &F[i].a);
    sort(F, F+m);
    for (int i = 0; n > 0; ++i) {
        int buy = min(n, F[i].a);
        ans += buy * F[i].p;
        n -= buy;
    }
    printf("%d\n", ans);
    return 0;
}

PROB Barn Repair

分析见前文。

这次我用了计数排序。

/*
ID: chrt2001
PROG: barn1
LANG: C++
*/
#include <cstdio>
#include <algorithm>
using namespace std;

bool O[201];
int S[199];

int main()
{
    freopen("barn1.in", "r", stdin);
    freopen("barn1.out", "w", stdout);  
    int m, s, c, mn = 1<<30, mx = 0;
    scanf("%d %d %d", &m, &s, &c);
    for (int i = 0; i < c; ++i) {
        int x;
        scanf("%d", &x);
        O[x] = true;
        mn = min(mn, x);
        mx = max(mx, x);
    }
    int b = mn, mx_i = 0;
    for (int i = mn+1; i <= mx; ++i)
        if (O[i]) {
            ++S[i-b-1];
            mx_i = max(mx_i, i-b-1);
            b = i;
        }
    int cnt = 0, ans = mx-mn+1;
    for (int i = mx_i; cnt < m-1 && i > 0; )
        if (S[i]) {
            ++cnt;
            --S[i];
            ans -= i;
        } else
            --i;
    printf("%d\n", ans);
    return 0;
}

TEXT Crafting Winning Solutions

很好的文章。以下对文本进行总结。

赛场行动指南

  1. 看题面。
  2. 首先brainstorm,判断时空可行性,然后选能出解的算法中最蠢的那一个。
  3. Hack!
  4. 给问题排序,从易到难。
  5. 对于每个问题,确定最终算法。
  6. 针对棘手的情况编数据。
  7. 写数据结构。
  8. 写输出
  9. 一步步细化:注释程序的逻辑。
  10. 完善代码,一个部分一个部分边写边测试。
  11. 让程序工作,检查正确性。
  12. Hack!
  13. 逐步优化,不做不必要的优化,保留历史版本。

Checklist

读了文章后,我决定拟一份自己的checklist。
1. 输入的格式正确吗?行和列的顺序是正确的吗?
2. 多组数据,是否进行了必要的初始、收尾工作?
3. return 0?
4. 常量打对了吗?
5. 变量初始化了吗?(-Wall) 初始化正确吗?
6. 数组的大小正确吗?
7. 指针有越界现象吗?
8. 多余的输出删除了吗?输出格式正确吗?开了文件输出吗?

PROB Prime Cryptarithm

从给定的数字集中取数,填充乘法计算的竖式,求解的数目。

我从数字集中取数组合成三、四位数,ANALYSIS用循环枚举(四位数, 三位数),验证各个位数是不是在数字集中。

/*
ID: chrt2001
PROG: crypt1
LANG: C++
*/
#include <cstdio>
#include <algorithm>
#include <cassert>
using namespace std;

int n, ans, s[9], d[2][3];
bool f[10];

bool valid(int x)
{
    while (x > 0) {
        if (!f[x % 10])
            return false;
        x /= 10;
    }
    return true;
}

bool check()
{
    int x = d[0][0] + d[0][1]*10 + d[0][2]*100, y = d[1][0] + d[1][1]*10, z = x*d[1][0], t = x*d[1][1], w = x*y;
    if (z >= 1000 || t >= 1000 || w >= 10000)
        return false;
    return valid(z) && valid(t) && valid(w);
}

void solve(int k, int i)
{
    if (k == 2) {
        if (check())
            ++ans;
        return;
    }
    for (int j = 0; j < n; ++j) {
        d[k][i] = s[j];
        if (k == 0 && i == 2)
            solve(1, 0);
        else if (k == 1 && i == 1)
            solve(2, 0);
        else
            solve(k, i+1);
    }
}

int main()
{
    freopen("crypt1.in", "r", stdin);
    freopen("crypt1.out", "w", stdout);
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", &s[i]);
        f[s[i]] = true;
    }
    solve(0, 0);
    printf("%d\n", ans);
    return 0;
}

PROB Combination Lock

给两个三元组和N,求在模N的意义下,对应元素和其中一个三元组作差,各个差的绝对值不超过2的三元组数量。

我直接枚举这样的三元组,用set去重。ANALYSIS枚举了所有三元组,检查。发了封电子邮件分享我的解答,会不会有回应呢?

Youtube看不了。

/*
ID: chrt2001
PROG: combo
LANG: C++
*/
#include <cstdio>
#include <set>
#define inc(i, fr, to) for (int i = fr; i < to; ++i)
using namespace std;
int n, lock[3];
set<int> S;

inline int mod(int x)
{
    return (x % n + n) % n;
}

void solve()
{
    inc (i, lock[0]-2, lock[0]+3)
        inc (j, lock[1]-2, lock[1]+3)
            inc (k, lock[2]-2, lock[2]+3)
                S.insert(mod(i) + mod(j)*100 + mod(k)*10000);
}

int main()
{
    freopen("combo.in", "r", stdin);
    freopen("combo.out", "w", stdout);
    scanf("%d", &n);
    inc (i, 0, 2) {
        inc (j, 0, 3)
            scanf("%d", &lock[j]);
        solve();
    }
    printf("%lu\n", S.size());
    return 0;
}

PROB Wormholes

2D平面上有一些虫洞,共有偶数个,两两成对,从其中一个进去,会从另一个出来,方向不变。问有多少种配对方案,会使得一头从某处(未指定)出发、沿+x方向走的牛陷入死循环。

离散化,枚举配对方式,再判断。由于牛只沿+x走,穿过虫洞不改变走的方向,只有横向的相对位置才是有意义的。

我的实现不如ANALYSIS。我先把虫洞的位置读入struct Point,排序,再计算出M——离散化之后的地图。这样是想保留行与行之间的相对顺序,其实是不必要的。计算出一个next_on_right数组就好了。检查死循环的时候我犯了两个错误:vis数组在直接return的情况下没清零,一个点在当前起点下访问了两次就算作死循环。把这张地图转成“标准”的有向图,发现一个点关联着这样几条边:左边的点、右边的点、配对的虫洞。如果从左边的点进,就要走虫洞;如果从虫洞进,就要到右边的点。只有访问一个点3次才能保证出现了相同的情形。ANALYSIS的做法很简洁,虫洞+向右,走N次没出界就表明存在死循环。

/*
ID: chrt2001
PROG: wormhole
LANG: C++
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

int n, ans, M[12][13], vis[13];

struct Point {
    int x, y, cp;
    bool operator<(const Point& rhs) const
    {
        return x < rhs.x || x == rhs.x && y < rhs.y;
    }
} P[13];

bool check()
{
    for (int i = 1; i <= n; ++i) {
        int u = i, x = P[u].x, y = P[u].y;
        memset(vis, 0, sizeof(vis));
        while (u) {
            if (vis[u] == 2 || M[x][y+1] && vis[M[x][y+1]] == 2)
                return true;
            ++vis[M[x][y+1]];
            ++vis[u];
            u = P[M[x][y+1]].cp;
            x = P[u].x;
            y = P[u].y;
        }   
    }
    return false;
}

void solve(int k)
{
    if (k == n+1) {
        if (check())
            ++ans;
        return;
    }

    if (P[k].cp)
        solve(k+1);
    else {
        for (int i = k+1; i <= n; ++i)
            if (!P[i].cp) {
                P[i].cp = k;
                P[k].cp = i;
                solve(k+1);
                P[i].cp = 0;
            }
        P[k].cp = 0;
    }
}

int main()
{
    freopen("wormhole.in", "r", stdin);
    freopen("wormhole.out", "w", stdout);

    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d %d", &P[i].y, &P[i].x);
    sort(P+1, P+1+n);
    int x = 0, y = 0;
    M[0][0] = 1;
    for (int i = 2; i <= n; ++i)
        if (P[i].x == P[i-1].x)
            M[x][++y] = i;
        else
            M[++x][y=0] = i;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j) {
            P[M[i][j]].x = i;
            P[M[i][j]].y = j;
        }
    solve(1);
    printf("%d\n", ans);
    return 0;
}

PROB Ski Course Design

有N(N<=1000)座山,每座的高度是[0, 100]内的整数。把一座山的高度改变一个单位,代价是改变量的平方。每座山的高度至多改变一次,问使最高的山与最矮的山高度之差不超过17的最小代价。

枚举所有长度为17的区间,把超标的高度削到区间的右端点,把不足的高度补至区间的左端点,求代价,取最小值。和今年NOI Day2 T1有异曲同工之妙!

/*
ID: chrt2001
PROG: skidesign
LANG: C++
*/
#include <cstdio>
#include <algorithm>
using namespace std;
const int D = 17;
int H[1000];
inline int sq(int x)
{
    return x*x;
}

int main()
{
    freopen("skidesign.in", "r", stdin);
    freopen("skidesign.out", "w", stdout);

    int n, ans = 1<<30, mn = 100, mx = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", &H[i]);
        mn = min(mn, H[i]);
        mx = max(mx, H[i]);
    }

    if (mx-mn <= D) {
        puts("0");
        return 0;
    }

    for (int l = mn; l <= mx-D; ++l) {
        int r = l+D, c = 0;
        for (int i = 0; i < n; ++i)
            if (H[i] < l)
                c += sq(H[i]-l);
            else if (H[i] > r)
                c += sq(r-H[i]);
        ans = min(ans, c);
    }
    printf("%d\n", ans);

    return 0;
}

暑假作业还没怎么写TAT

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值