前言
吴老师回家了,于是XY来了。突然从ACM调到OI的感觉真的一点都不好…‘
T1 士兵训练
题目描述
N
N
个士兵排成一队进行军事训练,每个士兵的等级用范围内的数来表示,长官每隔
1
1
小时就随便说出个等级
a1,a2...,am
a
1
,
a
2
.
.
.
,
a
m
(
1≤ai≤K
1
≤
a
i
≤
K
,
M
M
个等级中允许有重复),如果这个等级组成的序列是排成一队的
N
N
个士兵等级序列的子序列,那么训练继续;否则训练结束。长官想知道,至少为多少时,训练才有可能结束。
例:士兵等级序列为1 5 3 2 5 1 3 4 4 2 5 1 2 3,长官说出的等级序列为5 4,那么训练继续,如果长官说出的等级序列为4 4 4,那么训练结束。
数据范围
分析
第一眼看,马上冒出一个十分naive的想法:找出现次数最少的等级,答案就是最少出现次数+1。然后马上发现这个思路错了…
例如序列为1 1 1 2 2的时候,若
K=3
K
=
3
,按原来的方法求得是3,而事实上1就可以了(等级序列3)。我们原来是尽可能让序列里的等级相同,而事实上,我们应该尽可能让序列里的等级不同,这样才是最优。因此我们把原序列分块,每块遍历
1
1
~所有等级并且最小,这样我们就可以推出答案即块数+1。我们设块数为
M
M
,容易发现长度为的序列肯定都是可以的,但是
M+1
M
+
1
就不一定了。
参考程序
// XJOI 978 1
#include <cstdio>
const int MAXK = 10005;
int cnt[MAXK] = {0}, N, K;
int main() {
int i, ai, ans = 0, k = 0;
scanf("%d%d", &N, &K);
for (i = 0; i < N; i++) {
scanf("%d", &ai);
if (cnt[ai] == ans) {
++cnt[ai];
if (++k == K) ans++, k = 0;
}
}
printf("%d\n", ++ans);
return 0;
}
T2 三角形(TRINGLE)
题目描述
离圣诞节只有一个月了,家里要你准备一个很大的星星,然后把它粘在圣诞树的顶端。你已经准备好了一个三角形的银色包装纸来做星星,可忽然有一天你发现在这张大纸上被弄了好多的小洞,原来是你的弟弟妹妹已经从这张大纸上剪掉了许多小的三角形,做了很多小星星。现在,你只能将就了,看怎样才能从这残缺的大纸中,找出一块最大的完整的三角形。
如下图所示,假设给你一张大的三角形纸,里面黑的三角形是被你的弟弟妹妹剪掉了的,需要你在剩下的白色区域中找出一个最大的三角形区域。
分析
简单的DP,考虑两个不同的方向DP时,分别以某个点为三角形顶点的时候三角形最大的边长,方程很好列,详见程序。
参考程序
// XJOI 978 2
#include <cstdio>
#include <algorithm>
const int MAXN = 105;
int DP[MAXN][MAXN << 1], N;
char tgnl[MAXN][MAXN << 1];
void solve();
int main() {
int k = 0;
while (scanf("%d", &N) == 1 && N) {
printf("Triangle #%d\n", ++k);
solve();
}
return 0;
}
void solve() {
using std::max;
using std::min;
int i, j, lim;
for (i = N; i > 0; i--) scanf("%s", tgnl[i]);
int res = 0;
for (i = 0, lim = (N << 1) - 1; i < lim; i++)
if (!(i & 1)) res = max(res, DP[N][i] = tgnl[N][i] == 'O' ? 1 : 0);
for (i = N - 1; i > 0; i--)
for (j = 0, lim = (i << 1) - 1; j < lim; j++)
if (!(j & 1)) {
if (tgnl[i][j] == 'X') DP[i][j] = 0;
else res = max(res, DP[i][j] = tgnl[i + 1][j + 1] == 'O' ? min(DP[i + 1][j], DP[i + 1][j + 2]) + 1 : 1);
}
for (i = 2; i <= N; i++)
for (j = 3, lim = (i << 1) - 2, DP[i][1] = tgnl[i][1] == 'O' ? 1 : 0, DP[i][lim] = tgnl[i][lim] == 'O' ? 1 : 0; j < lim; j++) // 边界很搞
if (j & 1) {
if (tgnl[i][j] == 'X') DP[i][j] = 0;
else res = max(res, DP[i][j] = tgnl[i - 1][j - 1] == 'O' ? min(DP[i - 1][j], DP[i - 1][j - 2]) + 1 : 1); // 一开始这里一个i-1打成i+1了...呜呜呜...直接爆零...
}
printf("The largest triangle area is %d\n", res * res);
}
T3 双子树
题目描述
这一天,随身带来的干粮豆吃玩了,必须有人去找些食物来吃才行。可Z4的懒惰是出了名的,谁会愿意去呢?经过一番商讨,大家最后决定以老规矩“猜十次”的方式决定人选。可怜的立方由于猜拳技术不过关,屡战屡败,只好踏上觅食的征途。
立方后来找到了两棵奇怪的树,由于它们的形态十分相像,双子座的立方一时兴起便把它们命名为“双子树”。可双子树的果实能不能吃呢?“管它呢,先摘了再说。”于是身手敏捷的立方便爬上其中一棵,摘下一个大大的果实。可就在此时,另一棵却有几个果实坠地。
立方细心一看,发现这双子树上的某些果实有一些细丝相连,只要摘下其中一棵树的某一个果实,另一棵树将会有相应的一个或多个(也可能没有)果实坠地而摔坏,这些果实都和摘下的果实用细丝相连。摔坏的果实当然不能吃了。而且,立方发现,那些细丝是没办法弄断的,除非摘下它两端的果实的其中一个。由于这时只有立方一人(好没义气的Z4啊……),他只能眼睁睁地看着它们摔坏。也就是说,立方无法同时摘取任一条细丝两端上的两个果实。于是,不同的摘法最后会得到不同数量的果实,而立方将会用随身带的一个容量为
V
V
(表示能装个果实)的大袋子将它们装好。
馋嘴的立方当然希望摘得越多越好啦,那么,他最多可以得到多少个果实呢?
特别地,任两个果实间最多只会有一条细丝相连,同一棵树上的果实间不会有细丝相连,当袋子装满后,立方的口还可以塞进一个。
分析
第一眼看成树型DP了…然后很开心地打完发现好像连成的图不一定是树…再一看原来是个最大独立集,裸题裸题,注意立方嘴里还可以塞一个就好啦。
参考程序
// vjudge 978 3
#include <cstdio>
#include <cstring>
#include <algorithm>
const int MAXV = 505;
const int MAXE = 250005;
struct Edge {
int to, next;
} E[MAXE];
int tote = 0, V, N1, N2, M, last[MAXV] = {0}, match[MAXV] = {0};
bool vis[MAXV];
inline void add_edge(int u, int v) {
E[++tote].to = v, E[tote].next = last[u], last[u] = tote;
}
bool dfs(int u);
int main() {
scanf("%d%d%d%d", &V, &N1, &N2, &M);
int i, ai, bi;
for (i = 0; i < M; i++) {
scanf("%d%d", &ai, &bi);
add_edge(ai, bi); // 一开始这里打了个bi+N1,爆RE了,其实直接写bi就好
}
int res = N1 + N2;
for (i = 1; i <= N1; i++) {
memset(vis, 0, sizeof(vis));
if (dfs(i)) --res;
}
printf("%d\n", std::min(V + 1, res));
return 0;
}
bool dfs(int u) {
for (int i = last[u], v; i; i = E[i].next)
if (!vis[v = E[i].to]) {
vis[v] = true;
if (!match[v] || dfs(match[v])) {
match[v] = u;
return true;
}
}
return false;
}
T4 奶牛浴场
题目描述
在长为 P P ,宽为的长方形牧场中,有 N N 个产奶点,为了使奶牛产更多的奶,需要在牧场中建一个边与牧场的边平行的长方形浴场,使奶牛们生活得更愉快一些。当然浴场不能破坏产奶点,但产奶点在浴场边上是可以的。已知各产奶点的坐标,求建最大浴场的面积。
数据范围
分析
求最大子矩形有多种方法,放到一篇独立的文章中介绍。这题使用的是复杂度为 O(N2) O ( N 2 ) 的算法。本题需注意,产奶点是在点上,而需要求的是格子的面积。
参考程序
// XJOI 978 4
#include <cstdio>
#include <utility>
#include <algorithm>
#define fir first
#define sec second
const int MAXN = 5010;
typedef std::pair<int, int> P;
P A[MAXN];
int N, R, C;
int main() {
scanf("%d%d%d", &N, &R, &C);
int i, j;
for (i = 0; i < N; i++) scanf("%d%d", &A[i].fir, &A[i].sec);
A[N++] = P(0, 0), A[N++] = P(0, C), A[N++] = P(R, 0), A[N++] = P(R, C);
std::sort(A, A + N);
int res = 0;
for (i = 0; i < N; i++) {
int lb = 0, ub = C;
// printf("i=%d, (%d, %d)\n", i, A[i].fir, A[i].sec);
for (j = i + 1; j < N; j++)
if (A[i].fir != A[j].fir) {
// printf(" j=%d, (%d, %d), lb=%d, ub=%d\n", j, A[j].fir, A[j].sec, lb, ub);
res = std::max(res, (ub - lb) * (A[j].fir - A[i].fir));
if (A[j].sec <= A[i].sec) lb = std::max(lb, A[j].sec);
if (A[j].sec >= A[i].sec) ub = std::min(ub, A[j].sec);
}
std::swap(A[i].fir, A[i].sec);
}
std::sort(A, A + N);
for (i = 0; i < N; i++) {
int lb = 0, ub = R;
for (j = i + 1; j < N; j++)
if (A[i].fir != A[j].fir) {
res = std::max(res, (ub - lb) * (A[j].fir - A[i].fir));
if (A[j].sec <= A[i].sec) lb = std::max(lb, A[j].sec);
if (A[j].sec >= A[i].sec) ub = std::min(ub, A[j].sec);
}
std::swap(A[i].fir, A[i].sec);
}
printf("%d\n", res);
return 0;
}
T5 点和线
题目描述
平面上有一些点,你可以用线段将两点连接起来。那么有多少种方法可以把这些点连续地连起来,使得任何两个线段都不交叉。
显然,三个点只有一种方法。四个点最多只有3种方法。写一个程序计算方法总数。最多只有10个点。
分析
搜索即可。然后需要一点几何知识,反正我就直接把板子拉过来了…(逃…)
然后关于计数:注意到一个环上面会被统计多次,因此我们直接固定第一个点,但是又因为一个圈顺时针和逆时针会被重复计,最后答案还要除以2。
参考程序
#include <cstdio>
#include <cmath>
#include <algorithm>
const int MAXP = 12;
const double EPS = 1e-6;
struct Point {
double x, y;
} P[MAXP];
// typedef Point Vector;
int N, ans = 0, A[MAXP];
bool used[MAXP];
template <typename T>
inline bool equal_real(T a, T b) { return fabs(a - b) <= EPS; }
inline double cross(const Point & a, const Point & b, const Point & c) {
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
}
inline bool seg_cross(const Point & p1, const Point & p2, const Point & q1, const Point & q2) {
using std::min;
using std::max;
return min(p1.x, p2.x) <= max(q1.x, q2.x) &&
min(q1.x, q2.x) <= max(p1.x, p2.x) &&
min(p1.y, p2.y) <= max(q1.y, q2.y) &&
min(q1.y, q2.y) <= max(p1.y, p2.y) &&
cross(p1, q2, q1) * cross(p2, q2, q1) <= 0 &&
cross(q1, p2, p1) * cross(q2, p2, p1) <= 0;
// 一开始这里一个2打成1了,又一题爆零...
}
void dfs(int dep);
int main() {
for (ans = N = 0; scanf("%lf%lf", &P[N].x, &P[N].y) == 2; N++)
if (equal_real(P[N].x, 0.0) && equal_real(P[N].y, 0.0)) break;
N++;
A[0] = 0, used[0] = true;
dfs(1);
printf("%d\n", ans >> 1);
return 0;
}
void dfs(int dep) {
bool fail;
int i, j;
if (dep == N) {
// 注意判断最后首尾相连是否合法
for (fail = false, j = dep - 2; j > 1; j--)
if (seg_cross(P[A[0]], P[A[dep - 1]], P[A[j]], P[A[j - 1]])) { fail = true; break; }
if (fail) return;
++ans;
return;
}
for (i = 0; i < N; i++)
if (!used[i]) {
// 判断新线段是否与之前的线段相交
for (fail = false, j = dep - 2; j > 0; j--)
if (seg_cross(P[i], P[A[dep - 1]], P[A[j]], P[A[j - 1]])) { fail = true; break; }
if (fail) continue;
used[i] = true, A[dep] = i;
dfs(dep + 1);
used[i] = false;
}
}
总结
现在回到OI赛制,做题需要冷静谨慎下来了。今天的题,三道都是因为忽视了一个很小的错误而爆零,这是打ACM留下的极大病状。并且,本次测验也发现了一些知识点还掌握得不熟练,有:极大化子矩形、计算几何、二分图的知识点,应立即补上。