目录
原题链接:Codeforces Round #787 (Div. 3)
A. Food for Animals(签到)
题目大意:
有 x x x 只狗,和 y y y 只猫。你可以购买 a a a 包狗粮, b b b 包猫粮, c c c 包通用粮(猫和狗都能吃)。问你在购买之后,是否能正好分完粮,或者不够分。
解题思路:
判断还有剩下几只没分到的,看看通用粮够不够分即可。
AC代码:
using i64 = long long;
void solve()
{
i64 a, b, c, x, y;
cin >> a >> b >> c >> x >> y;
if (a - x < 0) c += a - x;
if (b - y < 0) c += b - y;
if (c >= 0) YES;
else NO;
}
B. Make It Increasing(模拟)
题目大意:
给出一个数列 a a a ,你可以任意选择一个 a i a_i ai 替换成 ⌊ a i 2 ⌋ \lfloor \frac{a_i}{2} \rfloor ⌊2ai⌋ 问最少操作多少次,才能让 a a a 变成一个严格递减的数列,如果不能达到,输出 − 1 -1 −1。
解题思路:
看一眼范围,
1
≤
n
≤
30
1\leq n \leq 30
1≤n≤30,直接暴力模拟改即可。
要判断是不是
−
1
-1
−1 ,则只需要看
a
a
a 数组最前面两个数一直向下取整后是否都是
0
0
0 即可(易知如果存在两个
0
0
0 则已经不是严格递减了)。
AC代码:
using i64 = long long;
void solve()
{
int n; cin >> n;
vector<int> arr(n);
for (auto& x : arr) cin >> x;
int ans = 0, cnt = 0;
bool ok = true;
while (true)
{
ok = true;//判断当前是否修改了
for (int i = 0; i + 1 < n; ++i)
if (arr[i + 1] <= arr[i]) //后一个小于自己 就除二
arr[i] >>= 1, ok = false, ++ans;
//不修改 或者无解就break
if (ok || (arr[0] == 0 && arr[1] == 0)) break;
}
cout << ((arr[0] == 0 && arr[1] == 0) ? -1 : ans) << '\n';
}
C. Detective Task(思维)
题目大意:
房里有一幅画,有 n n n 个人按顺序进入房间,最后你发现房间里的画不见了。
现在你按顺序审问这 n n n 个人。按原来的顺序来回答你的问题: “ 1 1 1” 表示画还在,“ 0 0 0” 表示画不在了,“ ? ? ?” 表示自己没注意。
只有小偷会撒谎,其他人都是诚实的。需要你求出有几个人可能是小偷。
解题思路:
分析,题目给出的原序列一定是 111111000000 111111000000 111111000000 这样的序列。然后再参杂进 “ ? ? ?” 。
只有小偷撒谎了,这一段序列中,其他人都一定诚实,那么可以得到结论:
- 对最后一个说 1 1 1 的人来说,前面的人一定都是诚实的,自己不一定诚实。
- 对第一个说 0 0 0 的人来说,后面的人一定都是诚实的,自己不一定诚实。
- 如果区间中间存在 ? ? ? ,我们并不能判断他是 1 1 1 还是 0 0 0 ,因此我们也要纳入怀疑名单中。
- 特别地,只存在 1 1 1 ,说明最后一个人是小偷,只存在 0 0 0, 说明第一个人是小偷。
所以序列是
1
?
?
1
?
1
?
1
?
?
?
0
?
000
?
?
0
1??1?1?1???0?000??0
1??1?1?1???0?000??0 时候。
答案区间为
1
?
?
1
?
1
?
(
1
?
?
?
0
)
?
000
?
?
0
1??1?1? (~~1???0~~) ?000??0
1??1?1?( 1???0 )?000??0 。
那么只要找到这两个
1
1
1 和
0
0
0 的下标,相减一下就可以了。
(不得不说,这题思维难度还是很大的 )
AC代码:
void solve()
{
string str;
cin >> str;
int n = str.size();
int l = 0, r = n - 1;
for (int i = 0; i < n; ++i)//找最后一个1
if (str[i] == '1') l = i;
for (int i = l; i < n; ++i)//找第一个0
if (str[i] == '0')
{
r = i;
break;
}
cout << r - l + 1 << '\n';
}
D. Vertical Paths(bfs)
题目大意:
给定一颗含有 n n n 个节点, n − 1 n - 1 n−1 条边的树,问最少分成多少个从上到下的链,并输出这些链。
解题思路:
分成最少链看似困难,但是只要保证每条链包含节点最多,也就是每一条都能够尽量长,就可以满足最少的链了。
我们只要从根开始,每条边都建立从自己到父亲的单向边,从根进行 b f s bfs bfs ,得到每个节点的前驱,就可以得到回溯时的路径了。
考虑如何回溯,根据我们的建图,易知入度为 0 0 0 的点就是叶子节点,把所有入度为 0 0 0 的节点都进行一次回溯。
从叶子节点向上回溯,如果遇到之前走过的链就结束,记录这一条链所有遇到过的节点,把经过的点都标记一下。下一次回溯遇到被标记过的点就直接 break 就行了。
(似乎很绕?看看代码配合解释就明白了 )
AC代码:
const int N = 2e5 + 10;
vector<int> g[N];//vector存图
vector<int> res;
bool vis[N];
void dfs(int u)
{
vis[u] = true;
for (auto& x : g[u])
if (!vis[x]) dfs(x);
res.emplace_back(u);//把路径上的点都存起来
}
void solve()
{
fill(vis + 1, vis + n + 1, false);//多测清空
for (int i = 1; i <= n; ++i) g[i].clear();
int n, root; cin >> n;
vector<int> in(n + 1);
for (int i = 1, x; i <= n; ++i)
{
cin >> x;
if (x == i) root = x;//判断根
else g[i].emplace_back(x), ++in[x];//建单向边 记录入度
}
vector<vector<int>> ans;//ans保存每一条路径
for (int i = 1; i <= n; ++i)
{
if (in[i]) continue;//如果不是是叶子节点 continue
dfs(i), ans.emplace_back(res);
res.clear();
}
cout << ans.size() << '\n';
for (auto& rec : ans)//输出所有路径
{
cout << rec.size() << '\n';
for (auto& x : rec) cout << x << ' ';
cout << '\n';
}
cout << '\n';
}
E. Replace With the Previous, Minimize(贪心,并查集)
题目大意:
给出一个长度为 n n n 的字符串 s s s ,每次可以选择 s s s 中的一个字母,将所有该字母变成其之前的字母(例如: d → c d \rightarrow c d→c, c → b c \rightarrow b c→b, z → y z \rightarrow y z→y 特殊地 a → z a \rightarrow z a→z )。
你只能操作不超过 k k k 次,使得这个字符串字典序最小。输出改变后的字符串 s s s。
解题思路:
字典序最小的题一般都直接想到贪心。要字典序最小,那么前面的字母能向 a a a 靠近,就向 a a a 靠近。而且注意到,操作是对当前所有字母都有效的。
对于: s = f e d c b a s =fedcba s=fedcba, k = 3 k = 3 k=3 来说,我们操作的序列就是这样的:
- 第一次: s = e e d c b a s = eedcba s=eedcba
- 第二次: s = d d d c b a s = dddcba s=dddcba
- 第三次: s = c c c c b a s = ccccba s=ccccba
那么我们维护一下每个字母都改变成了哪一个字母就好了,这里可以用并查集。
字母每向前移动一次,就把他的父亲变成前一个节点,并且易知( a → z a \rightarrow z a→z)这个操作肯定不优,因此我们只需要考虑变到 a a a 这个字母停止就行了。
贪心地从 s s s 的第一个字母开始变,如果变到了 a a a 就从第二个字母开始变,以此类推。
AC代码:
int fa[26];//存父亲
int find(int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void solve()
{
int n, k; cin >> n >> k;
string str; cin >> str;
//初始化并查集 0 为 a,1 为 b,以此类推
iota(fa, fa + 26, 0);
for (int i = 0; i < n; ++i)
{
int x = find(str[i] - 'a');//找到现在这个字母改变成了什么
if (fa[x] != 0)//当前这个字母没有变到字母 a 则变换
{
while (k && fa[find(x)] != 0)
k--, x = fa[x] = fa[x - 1];//每次变换 操作 k 都减少 1
}
}
for (auto& x : str)
cout << char(find(x - 'a') + 'a');//将每个字母转换后输出
cout << '\n';
}
F. Vlad and Unfinished Business(bfs)
题目大意:
给出一棵有 n n n 个节点, n − 1 n - 1 n−1 条边的树,给出你根 x x x 和终点 y y y 。
你要去到 a 1 , a 2 , . . . , a k a_1,a_2,...,a_k a1,a2,...,ak 点之后,才能在终点 y y y 停下。每一个点可以访问多次,问你要达到上述条件,你要走的路程最小是多少。
解题思路:
通过手摸样例可以知道,我们不管先到达哪一个点 a i a_i ai,然后再折返去到另一个点 a j a_j aj,在最后返回到 y y y的时候,我们的最短路程都是固定的(前提是你走的是两点间的最短路,而且没重复经过 x → y x \rightarrow y x→y 的最短路径)。
给出一个例子,比如我们现在的图是这样的:
那么我们要保证不重复经过
x
→
y
x \rightarrow y
x→y 的最短路径,来达到目的的话,我们的路径应该是这样的:
观察得到,除了
x
→
y
x \rightarrow y
x→y 的最短路径,其他所有的边经过的次数都是
2
2
2 次,因此我们只需要算出别的边一共有多少条,乘上
2
2
2 再加上
x
→
y
x \rightarrow y
x→y 的最短路径的长度,即可得正确答案(如果某个点在
x
→
y
x \rightarrow y
x→y 的最短路径上,那就不纳入考虑)。
我们执行一次 b f s bfs bfs,跑出每个点的前驱是谁,再回溯一下路径,如果当前点已经被回溯过就 break 掉。
(具体实现看代码就行了)
AC代码:
using i64 = long long;
const int N = 2e5 + 10;
vector<int> g[N];//邻接表存图
int pre[N], n, k, st, ed;//pre存前驱
bool vis[N];
inline void bfs()
{
fill(vis, vis + 1 + n, false);//初始化访问数组
//bfs板子
queue<int> que;
que.push(st);
vis[st] = true;
while (que.size())
{
auto t = que.front();
que.pop();
for (auto& x : g[t])
{
if (vis[x]) continue;
pre[x] = t;
vis[x] = true;
que.push(x);
}
}
}
i64 mov(i64 u, i64 sum)//回溯路径
{
while (vis[u])//如果当前点没被回溯到过
vis[u] = false, u = pre[u], ++sum;
//每次把vis置成false 防止重复经过一条边
return sum;
}
void solve()
{
cin >> n >> k;
cin >> st >> ed;//st是起点 ed是终点
for (int i = 1; i <= n; ++i) g[i].clear();//多测清空
vector<int> ned(k);
for (auto& x : ned) cin >> x;
for (int i = 0, x, y; i < n - 1; ++i)
cin >> x >> y,
g[x].emplace_back(y),
g[y].emplace_back(x);//建双向边
i64 ans = 0;
bfs();
vis[st] = false;//起点已经去过了
ans += mov(ed, 0);//加上x -> y的路径长度
for (auto& x : ned)
ans += mov(x, 0) << 1;//其他点的路径长度 * 2
cout << ans << '\n';
}
G太难辣!!以后补了再补一下题解!!
谢谢观看!!