UPDATE 21/08/26
更改题面、转移链接等。
2021.8.12 新初一暑假测试六
1 / 18,260 / 400。
总结
整体还可以吧,该拿的分都尽量拿了,也证明最近的训练有效 (我近来的效率是真的低),除了 T4。
排名我是真没想到 ,考试的时候看见教练对着我笑还来问候我,自己还是做了心理斗争的,最后毅然决然选择比赛划水。
小视野主页的文章不够看啊,催更。
T1 一遍 ac;
T2 纯暴力 60 分;
T3 知道是强连通分量,推的时候按桥来推,这个不应该错;
T4 做过,栽在了排列组合上,忘记答案除以二了。
检查很重要!还是要沉下心来比较好,当时就是信心满满检查 T4,直接忽略了错误代码。
T1
题目概述
太水了,懒得说了。
这是一道大水题,签到题,所以为什么刚开始有一半人没 ac?
话说这两次考试第一题都是送给你的??
T2
题目概述
初始 n 个点互相独立,之间没有连边(则初始图为一个 DAG)。输入 t 条边,若加上这条边之后图不再是一个 DAG,ans 加一,最后输出 ans。
也就是求那些能让图成环的边的数量。
求解
这个。。记忆化搜索。
教练说出了搜索,我就直接暴力了。。
当 m p u , v ← t r u e mp_u,_v \gets true mpu,v←true 时,表示能从 u 走到 v。反之, m p u , v ← f a l s e mp_u,_v \gets false mpu,v←false 表示还不能从 u 走到 v。
-
我们初始化一下数组—— m p i , i ← t r u e mp_i,_i \gets true mpi,i←true;
-
没当输入一条边 ( u , v ) (u,v) (u,v) 时,我们判断 i f ( m p v , u = = 1 ) if(mp_v,_u==1) if(mpv,u==1)——能够从 v 出发走到 u,如果可以,就统计这条边。
因为我们发现,如果要让 u 和 v 处于一个环内,那么必须要满足 m p u , v = = m p v , u = = 1 mp_u,_v==mp_v,_u==1 mpu,v==mpv,u==1——能从 v 出发走到 u,反之,也要可以。(此题灵魂)
-
如果不可以,我们就可以让 m p u , v ← t r u e mp_u,_v \gets true mpu,v←true。
这时候记忆化搜索就体现出来了。如果我们已经赋值过 m p u , v mp_u,_v mpu,v 了,那就可以直接跳过下面的赋值操作,处理下一条边。如果没有,我们就给他赋值。
-
赋值的时候,我们可以连其他的边一起赋值, 这样就可以在后面遍历到赋值过的边时大大减小处理它的时间,优化代码以及时间复杂度。
我们发现,如果连接了 ( u , v ) (u,v) (u,v) 这条边,仅仅会对两部分点造成影响,而我们一起赋值的,也是他们。
假设能从点 i 走到点 u,点 j 走到点 v,那么当我们将 ( u , v ) (u,v) (u,v) 连上之后,i 就也能和 j 互达了。
于是,我们找到所有的 i 和 j,让他们都连上, m p i , j ← t r u e mp_i,_j \gets true mpi,j←true。
还是要学会灵活运用记搜啊,能打暴力或写 dp 的时候想想记搜吧。
#include<bits/stdc++.h>
using namespace std;
int n, t;
const int maxn = 300;
int mp[maxn][maxn], ut[maxn], vt[maxn];
int ans;
int main ()
{
cin >> n >> t;
for (int i = 1; i <= n; i++) mp[i][i] = 1;
while (t--)
{
int u, v;
cin >> u >> v;
int tot = 0, tot2 = 0;
if (mp[v][u]) {ans++;continue;}
if (mp[u][v]) continue;
for (int i = 1; i <= n; i++)
{
if (mp[i][u]) ut[++tot] = i;
if (mp[v][i]) vt[++tot2] = i;
}
for (int i = 1; i <= tot; i++)
for (int j = 1; j <= tot2; j++)mp[ut[i]][vt[j]] = 1;
}
cout << ans << endl;
return 0;
}
T3 稳定婚姻
智推 yyds!推给我了,我点开了,题读了,但就!是!没!做!
题目概述
有 n 对夫妻,以及 m 对前尘恋情,
若一对夫妇离婚,女的会去找前 npy,男的也会去找前 npy,然后各个 npy 的另一半就也会和他们离婚去找属于自己的前 npy…
要是最后每个人都成功地找到了新的伴侣,那么最初离婚的那对夫妇的夫妻关系就被称作是不安全的(什么鬼),反之就是安全的(???)。
求解
我一开始真的觉得不就是判桥吗?
但是实际上并不是桥,准确来说,我们是要在夫妻之间没有连边的情况下,判断图是否连通、成环, 如果连通,那么就是不安全的,反之就是安全的。
同时在保证代码、时间复杂度尽量低的前提下,最佳也是最简单的算法当然是短小而精悍的强连通分量。
建边
阅览了无数题解,翻遍了最优解代码, 我发现关于建边实际上就两种方法:
-
夫妻之间不建边(这里我们把夫妻看为一体,一个单独的节点),对于
前尘恋情b o y ← g i r l boy \gets girl boy←girl 或者 g i r l ← b o y girl \gets boy girl←boy; -
夫妻之间 b o y ← g i r l boy \gets girl boy←girl(夫妻各为一个独立的节点),
前尘恋情g i r l ← b o y girl \gets boy girl←boy(反过来也可以,只要两个不同即可)。
无论是以上哪一种,只要夫妻最后在同一个强连通分量中,那么这对关系就是不安全的。
反之就是安全的咯~
map
这道题里涉及到了 STL 中的 map。
wtcl,才注意到还有这么强大的 1 v 1 对应关系存储。
我比赛没用 map (没意识用这) : 输入前尘恋情的时候循环一个个找,找到再建边,但这很明显会一发 TLE。
而 map,作为 STL 大家庭中的一员,拥有自动建立 key - value 的对应。 key 和 value 可以是任意你需要的类型,包括自定义类型。
map<string, int> mp1, mp2;
这是这道题我们需要的,定义了之后,我们就可以用一个字符串 string
去指向一个(一对夫妻或一个人的)编号 int
。
char c1[10], c2[10];
scanf ("%s %s", c1, c2);
add (mp1[c1], mp2[c2]);
具体可以像这样直接当成数组来使用 (好强大,Orz)。
#include<bits/stdc++.h>
using namespace std;
int n, m;
const int maxn = 25005;
int cnt, hd[maxn];
struct node{
int to, nxt;
}e[maxn * 2];
struct inpt{
string namv;
string nama;
}in[maxn];
int nt, root, tmp, top, col;
int dfn[maxn], low[maxn], vis[maxn], co[maxn];
int cut[maxn], st[maxn], siz[maxn];
map<string, int> mp1, mp2;
int read ()
{
int x = 1, s = 0;
char ch = getchar ();
while (ch < '0' or ch > '9') {if (ch == '-') x = -1; ch = getchar ();}
while (ch >= '0' and ch <= '9') {s = s * 10 + ch - '0'; ch = getchar ();}
return x * s;
}
void add (int u, int v)
{
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan (int u)
{
dfn[u] = low[u] = ++tmp;
st[++top] = u;
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan (v);
low[u] = min (low[u], low[v]);
}
else if (!co[v]) low[u] = min (low[u], dfn[v]);
}
if (dfn[u] == low[u])
{
siz[++col]=1;
co[u] = col;
while (st[top] != u)
{
co[st[top]] = col;
siz[col]++;
top--;
}
top--;
}
}
int main ()
{
n = read ();
for (int i = 1; i <= n; i++)
{
int u, v;
char c1[10], c2[10];
scanf ("%s %s", c1, c2);
mp1[c1] = i;
mp2[c2] = i;
}
m = read ();
for (int i = 1; i <= m; i++)
{
char c1[10], c2[10];
scanf ("%s %s", c1, c2);
add (mp1[c1], mp2[c2]);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i]) tarjan (i);
if (siz[co[i]] <= 1) printf ("Safe\n");
else printf ("Unsafe\n");
}
return 0;
}
T4 矿场搭建
Emmm…做过的一道题,多亏了他我才能位居榜首(膜拜第二名大佬 szx,唯一 ac T3)。
题目概述
把每一个开采矿的地方看成一个点 (有一说一钱真多),问至少要在几个点上建逃生出口以及有几种建立出口的方式,
使建了出口之后无论哪一个节点炸了(出口也有可能炸,且只会炸掉一个节点)之后其他所有点的工人都能从某一个逃生出口逃出来。
求解
很明显,一道点双联通分量。
首先,如果原图就是一个点双,也就是没有割点,任意两点互通,我们就只需要建两个出口——一个炸了走另一个。
如果原图不是点双,那么我们就把原图分成若干个点双,逐一求解。
先 Tarjan,求出每一个割点,然后 dfs,把原图分成若干点双。
我们每找一个点双,就边找边统计该点双中的割点数及非割点数。
-
割点数为 0,建两个出口(和整体为一个点双原因相同)。
-
割点数为 1,建一个出口(割点没炸走割点,割点炸了走点双里面的出口)。
-
割点数为 2,不用建了(一个割点炸了走另一个割点)。
总数量直接加数量,总方案数乘非割点数的组合情况(每次初始化为 1):
-
割点数为 0: a n s s × s i z u anss \times siz_u anss×sizu;
-
割点数为 1: a n s s × s i z u × ( s i z u − 1 ) / 2 anss \times siz_u \times (siz_u-1) /2 anss×sizu×(sizu−1)/2。
记得除以 2!!!排列组合!!!
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n, m;
const int maxn = 1005;
int cnt, hd[maxn];
struct node{
int to, nxt;
}e[maxn * 2];
int dfn[maxn], low[maxn], cut[maxn], co[maxn];
int ans, anss;
int tmp, root, col;
int ct, siz;
void init ()
{
n = cnt = ans = tmp = ct = siz = col = 0;
anss = 1;
memset (hd, 0, sizeof hd);
memset (e, 0, sizeof e);
memset (dfn, 0, sizeof dfn);
memset (low, 0, sizeof low);
memset (cut, 0, sizeof cut);
memset (co, 0, sizeof co);
}
int read ()
{
int x = 1, s = 0;
char ch = getchar ();
while (ch < '0' or ch > '9') {if (ch == '-') x = -1; ch = getchar ();}
while (ch >= '0' and ch <= '9') {s = s * 10 + ch - '0'; ch = getchar ();}
return x * s;
}
void add (int u, int v)
{
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan (int u, int fa)
{
dfn[u] = low[u] = ++tmp;
int chil = 0;
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
chil++;
tarjan (v, u);
low[u] = min (low[u], low[v]);
if ((dfn[u] <= low[v] and u != root) or (u == root and chil > 1))
{
cut[u] = 1;
}
}
else if (v != fa) low[u] = min (low[u], dfn[v]);
}
}
void dfs (int x)
{
co[x] = col;
siz++;
for (int i = hd[x]; i; i = e[i].nxt)
{
int v = e[i].to;
if (cut[v] and co[v] != col)
{
co[v] = col;
ct++;
}
if (!co[v])
{
dfs (v);
}
}
}
signed main ()
{
int T = 0;
m = read ();
while (m != 0)
{
init ();
T++;
for (int i = 1; i <= m; i++)
{
int u, v;
u = read (), v = read ();
add (u, v), add (v, u);
n = max (n, max (u, v));
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
root = i;
tarjan (i, -1);
}
}
for (int i = 1; i <= n; i++)
{
if (cut[i] or co[i]) continue;
col++;
siz = ct = 0;
dfs (i);
if (ct == 0)
{
ans += 2;
anss *= (siz * (siz - 1)) / 2;
}
if (ct == 1)
{
ans += 1;
anss *= siz;
}
}
printf ("Case %lld: %lld %lld\n", T, ans, anss);
m = read ();
}
return 0;
}
—— E n d End End——
阳 和 启 蛰 , 枯 木 逢 春 。 阳和启蛰,枯木逢春。 阳和启蛰,枯木逢春。