【2019】0711 的 C 组模拟赛总结 + 感想
Cefola_Kiroxs · · 个人记录
比赛状况
比赛地址
分数(实际/估分):91.7 / 120
排名:并列 29$
这次的比赛是之前一次模拟赛的原题,一模一样。考虑到过去我的博客很丑,所以我不推荐各位点进去看。(但我也不会重新详细地写题解)然而,即便是 “原题” “印象深刻” 的一次比赛,我居然也打爆了 …… 话说说来,今天也是大凶。
总结
-
优化暴力往往是推导正解的有效方法之一。在条件允许下,一定要首先思考 “最直接、最省事的暴力” ,再去思考 “能不能从别的途径思考” 或者 “能不能改进暴力的某个地方” ,但通常选择后者。
-
优化暴力的角度往往有:从耗时瓶颈处优化、缩小问题规模(即做等价变换,使问题更小)等。
-
遇到 “某个范围内累加某个数” 的题,即使用了离散化,也通常考虑用前缀和转化为 “某几个点上累加某个数” ,但一定要掌握二维前缀和的推导方式。
(简单概括:预处理时 “原点” 加,越界一个的点减,越界两个的点加;累计答案时往前偏移一个的点加,往前偏移两个的点减)
-
遇到 “统计矩阵但输入点,所以可能出现边重叠但不计入答案” 的题,通常的做法有两个 —— “手动去重” 或者 “将放大一点的矩阵和缩小一点的矩阵一起离散化” 。
-
double的读入用的是%lf,输出是%f -
不要乱设二分的边界。并且,数组的越界很有可能造成 “不会报错” 的答案错误。
T1 GCD 与 LCM
本题很容易想到朴素做法 —— 显然,
可以发现,上面的枚举如果同时除以
#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
const int INF = 1000000000 + 5;
int getGCD(int x, int y) { return (y == 0) ? x : getGCD(y, x % y); }
int main()
{
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
int GCD, LCM;
scanf("%d%d", &GCD, &LCM);
LCM /= GCD;
int ans = INF;
for (int i = 1; i*i <= LCM; ++i)
if (LCM % i == 0)
{
int j = LCM / i;
if (getGCD(i, j) == 1) ans = min(ans, abs(i * GCD - j * GCD));
//这里我采用的是 “在更新ans过程中就乘上GCD”的方式,换成输出再乘未免不可
}
printf("%d", ans);
return 0;
}
T2 幻灯片
这题同样也要从暴力开始思考。显然,将每一个幻灯片的信息原样存储下来,再对所有格子枚举一次,就能得到答案。但是,这样做的时间复杂度高达
通过
不过,关于填充颜色,如果我们离散化后,对每一个幻灯片都填充一次颜色,显然效率会大大下降。此时,我们就能使用二维前缀和 —— 这是一个较难的内容,希望各位看代码自行体会。就这样,这个程序完成了基本内容。
但还存在一个问题 —— 题目要求我们统计 “矩阵”, 但给我们输入的是 “点” 。这样,当出现 “两个矩阵有重叠边” 的情况时,直接用上面的做法会将这个 “重叠边” 的颜色累加,但实际上 “重叠边” 根本没有颜色!怎么解决这个问题呢?比较普适的做法是,将每一个幻灯片 “放大一点点” 的样子和 “缩小一点点” 的样子一起离散化,并以 “缩小一点点” 的样子进行操作。这样,即使有重叠边,也不会被重复累加颜色了。
#include<cstdio>
#include<algorithm>
#define rr register
using namespace std;
const int MAXN = 100 + 5;
const int MAXC = 10000 + 5;
const int MAXP = MAXN << 3;
struct range
{
double x1, y1, x2, y2; //(x1,y1)是左下角,(x2,y2)是右上角
int c;
}
a[MAXN];
int len, sum[MAXP][MAXP], cnt[MAXC];
double ind[MAXP];
int main()
{
freopen("b.in", "r", stdin);
freopen("b.out", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
scanf("%lf%lf%lf%lf%d", &a[i].x1, &a[i].y1, &a[i].x2, &a[i].y2, &a[i].c);
ind[++len] = a[i].x1 - 0.5; //“扩大一点”的区间
ind[++len] = a[i].y1 - 0.5;
ind[++len] = a[i].x2 + 0.5;
ind[++len] = a[i].y2 + 0.5;
ind[++len] = (a[i].x1 += 0.5); //“缩小一点”的区间,拿它来操作
ind[++len] = (a[i].y1 += 0.5);
ind[++len] = (a[i].x2 -= 0.5);
ind[++len] = (a[i].y2 -= 0.5);
}
sort(ind + 1, ind + len + 1);
len = unique(ind + 1, ind + len + 1) - (ind + 1);
for (int i = 1; i <= n; ++i)
{
a[i].x1 = lower_bound(ind + 1, ind + len + 1, a[i].x1) - (ind + 1);
a[i].y1 = lower_bound(ind + 1, ind + len + 1, a[i].y1) - (ind + 1);
a[i].x2 = lower_bound(ind + 1, ind + len + 1, a[i].x2) - (ind + 1);
a[i].y2 = lower_bound(ind + 1, ind + len + 1, a[i].y2) - (ind + 1);
sum[ (int)a[i].x1 ][ (int)a[i].y1 ] += a[i].c;
sum[ (int)a[i].x1 ][ (int)a[i].y2 + 1 ] -= a[i].c;
sum[ (int)a[i].x2 + 1 ][ (int)a[i].y1 ] -= a[i].c;
sum[ (int)a[i].x2 + 1 ][ (int)a[i].y2 + 1 ] += a[i].c;
}
int ans = 0;
for (rr int i = 1; i <= len; ++i)
for (rr int j = 1; j <= len; ++j)
{
sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
if (sum[i][j] && !cnt[sum[i][j]]) { ans++; cnt[sum[i][j]]++; }
}
printf("%d\n", ans);
return 0;
}
T3 导弹
首先,我们可以分析题目,提炼出题目的模型 —— 给定一个无向图,并将所有属于
在使用 Floyd 求出最短路以后,我们就要解决 “选定的边的最大权值最小” 这个问题。看到 “最大值最小” ,你想到了什么?二分查找。二分查找就是常见解决 “双最值” 问题的算法,通常用二分先确定一个答案,再检验这个答案是否合法。在本题中,我们以用时为
我们每次构成的图叫做 “二分图”。检验每次构成的二分图是否合法,我们需要使用 “匈牙利算法” 。匈牙利算法虽然对于初学者不是特别友好,但如果认真寻找资料,反复对比,相信各位一定可以理解,在此不多做阐述。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXK = 100 + 5;
int n, m, a[MAXK], b[MAXK], f[MAXK][MAXK];
const int MAXE = 10000 + 5; //邻接表存图
int head[MAXK], to[MAXE], last[MAXE], tot;
void addEdge(int u, int v)
{
to[++tot] = v;
last[tot] = head[u];
head[u] = tot;
}
int now, vis[MAXK], belong[MAXK];
//now是当前匹配的点,虽然匹配过程中我们会递归,但终究是为了让now匹配
//vis是“访问时间”数组,用now来标记访问时间,如果重复标记同一个now就代表匹配失败
bool check(int u)
{
if (vis[u] == now) return 0;
vis[u] = now;
for (int i = head[u]; i; i = last[i])
{
int v = to[i];
if (!belong[v] || check(belong[v]))
{
belong[v] = u;
return 1;
}
}
return 0;
}
bool hungary()
{
bool OK = true;
for (now = 1; now <= m; ++now)
if (!check(b[now])) { OK = false; break; }
return OK;
}
int main()
{
freopen("c.in", "r", stdin);
freopen("c.out", "w", stdout);
int k;
scanf("%d", &k);
for (int i = 1; i <= k; ++i)
for (int j = 1; j <= k; ++j)
scanf("%d", &f[i][j]);
for (int p = 1; p <= k; ++p)
for (int i = 1; i <= k; ++i)
for (int j = 1; j <= k; ++j)
f[i][j] = min(f[i][j], f[i][p] + f[p][j]);
scanf("%d", &n); for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
scanf("%d", &m); for (int i = 1; i <= m; ++i) scanf("%d", &b[i]);
int L = 0, R = 0;
for (int i = 1; i <= k; ++i)
for (int j = 1; j <= k; ++j)
R = max(R, f[i][j]);
while (L < R)
{
int mid = (L + R) >> 1;
tot = 0;
memset(head, 0, sizeof(head));
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
if (f[b[i]][a[j]] <= mid) addEdge(b[i], a[j]);
memset(vis, 0, sizeof(vis));
memset(belong, 0, sizeof(belong));
if (hungary()) R = mid; else L = mid + 1; //二分的写法很重要哦
}
printf("%d\n", L);
return 0;
}
T4 医院
由题意可以知道,不满意度肯定为 0(毕竟没有限定建医院的数量)。而且,这题还是 Special Judge ,所以输出方案也不必担心,只要能输出合理的方案就行了。那么,有什么简单的方法,能够保证一个方案的不满意度一定为 0 呢?答案是 “不能放的点就不放,可以放的点全都放” 。
首先,因为题目说这是一张有向图,所以显然,入度为 0 的点必须要放医院(我们称之为 “第一层” )。之后,根据题目要求,与这些点相邻的点就不能放了(我们称之为 “第二层” )。接下来,因为题目中 “Special Judge ,不用考虑数量的多少,那么为什么要记录它现在与 “第一层” 的点是隔一条边,还是隔两条边呢?所以,与 “第二层” 相邻的点仍然要放。
通过上述的分析,我们发现 —— 这就是一个贪心嘛!事实上,由于本题是 Special Judge ,所以这个贪心策略是完全正确的。依照这种贪心策略,虽然实现起来有些复杂,但总归是能得到正确的结果的。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100 + 5;
int n;
int g[MAXN][MAXN], into[MAXN];
bool cantUse[MAXN], vis[MAXN];
void dfs(int i)
{
if (!cantUse[i])
{
vis[i] = 1;
for (int j = 1; j <= n; ++j)
if (g[i][j]) cantUse[j] = 1;
for (int j = 1; j <= n; ++j)
if (g[i][j]) { vis[j] = 1; dfs(j); }
}
else
{
for (int j = 1; j <= n; ++j)
if (g[i][j] && !vis[j]) { vis[j] = 1; dfs(j); }
}
}
int main()
{
freopen("d.in", "r", stdin);
freopen("d.out", "w", stdout);
printf("0\n");
int m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
{
int x, y;
scanf("%d%d", &x, &y);
g[x][y] = 1;
into[y] = 1; //我们只关心入度是否为0,所以直接标1可以避免“重边”造成的错误
}
int u;
scanf("%d", &u);
for (int i = 1; i <= n; ++i)
if (!into[i])
{
cantUse[i] = 0; vis[i] = 1;
for (int j = 1; j <= n; ++j)
if (g[i][j]) cantUse[j] = 1;
}
bool flag = 1;
for (int i = 1; i <= n; ++i)
if (!into[i]) {
flag = 0;
dfs(i);
}
if (flag) //如果都是环,不存在入度为0的点,所有的点都要跑一遍
for (int i = 1; i <= n; ++i)
dfs(i);
int numHosp = 0;
for (int i = 1; i <= n; ++i)
if (!cantUse[i]) numHosp++;
printf("%d\n", numHosp);
for (int i = 1; i <= n; ++i)
if (!cantUse[i]) printf("%d%c", i, (i==n) ? '\n' : ' ');
return 0;
}