这一节的主题是贪心和枚举。个人认为不剪枝的叫枚举,剪枝的叫搜索。
适当的枚举是许多优秀的解法中必不可少的环节,通常,枚举的东西可以分为这样几类:选择、限制、答案。
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
很好的文章。以下对文本进行总结。
赛场行动指南
- 看题面。
- 首先brainstorm,判断时空可行性,然后选能出解的算法中最蠢的那一个。
- Hack!
- 给问题排序,从易到难。
- 对于每个问题,确定最终算法。
- 针对棘手的情况编数据。
- 写数据结构。
- 写
- 写输出
- 一步步细化:注释程序的逻辑。
- 完善代码,一个部分一个部分边写边测试。
- 让程序工作,检查正确性。
- Hack!
- 逐步优化,不做不必要的优化,保留历史版本。
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