21-22寒假一期训练3
传送门:https://codeforces.com/group/uVAsoW2Jkj/contest/364973
原题链接:https://codeforces.com/gym/102916
J. Lost Island
题目大意
一个岛上有 \(n\) 种不同眼睛颜色的人,每种颜色有 \(a_i\) 个人
他们不知道自己的眼睛颜色,只知道别人的。一旦他们知道自己的眼睛颜色,就会在下一天里自杀
在第0天,一位旅行者告诉他们每种颜色 \(i\) 至少有 \(b_i\) 个人
现在告诉你所有的 \(a_i\) 和 \(b_i\) ,求自杀发生的最后一天和自杀的总人数
分析规律
当 \(a_i\, = 1\),\(b_i = 1\),那个人没发现别人有第 \(i\) 种颜色,所以知道自己是第 \(i\) 种,第 \(1\) 天自杀
当 \(a_i\, = 2\),\(b_i = 1\),颜色 \(i\) 的人看到有一个人是颜色 \(i\) ,等着他在第一天自杀,但那个人没有自杀,所以他知道自己是第 \(i\) 种,第 \(2\) 天自杀
当 \(a_i\, = 2\),\(b_i = 2\),颜色 \(i\) 的人只看到有一个人是颜色 \(i\),所以知道自己是,第 \(1\) 天自杀
用数学归纳法分析得,当 \(b_i > 0\) 时,颜色 \(i\) 的人全在 \(a - b + 1\) 天自杀
当只有一种 \(b_i = 0\) 时,他们发现其他颜色的人都挂掉后,就知道自己的颜色,遂在下一天自杀
当只有两种或以上 \(b_i = 0\) 时,他们可以活下来
特殊情况
当没有 \(b_i = 0\) 的颜色时,最后一批自杀的人是 \(b_i > 0\) ,他们看到其他颜色全挂掉后,就知道自己的颜色,所以在下一天就马上自杀了
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
typedef pair<int, int> pii;
const int N = 100010;
signed main()
{
int n; cin >> n;
int deadTot = 0, zeroCnt = 0, zeroTot = 0, lastDay = 0, second = 0;
for(int i = 1; i <= n; i++)
{
int a, b;
cin >> a >> b;
if(b == 0) zeroCnt++, zeroTot += a;
else
{
if(a - b + 1 > lastDay)
{
second = lastDay; // 记录倒数第二久的天数
lastDay = a - b + 1;
}
else second = max(second, a - b + 1);
deadTot += a;
}
}
if(zeroCnt == 1) deadTot += zeroTot, lastDay++;
if(zeroCnt == 0) lastDay = min(lastDay, second + 1);
cout << lastDay << " " << deadTot << endl;
return 0;
}
K. Bloodseeker
题目大意
有 \(n\) 个敌人,每个敌人需要打 \(t_i\) 下,打完会回复玩家 \(h_i\) 血
玩家一开始有 \(m\) 滴血,而且最多只能有 \(m\) 滴血
每打敌人一下就会消耗 \(1\) 滴血,如果玩家血量少于 \(0\) 就会结束游戏
在任意时刻,玩家可以选择打任意一个敌人
问玩家是否能打完所有敌人
分析规律
因为最多只有 \(m\) 滴血,所以一定要对 \(h_i\) 取 \(min\,(\,h_i,\,m\,)\) 再进行后续操作
有两种敌人,一种是 \(h_i\,>=\,t_i\),打完补血;一种是 \(h_i\,<\,t_i\) 打完扣血
转换思路(思维题)
对于打完补血的敌人,先把它们都打到剩 \(1\) 滴血,再去打别人。当打别人打到自己只剩 \(1\) 滴血时,再打补血怪,这样就能一滴也不浪费,补完所有的 \(h_i\)。所以在这种策略下,\(hp\,=\,m\, + \, \sum h_i\)。因为 \(h_i\,<=\,m\) 所以补血怪一定有 \(t_i\, <= m\),所以补血怪都能打
对于打完扣血的敌人,一个一个打,至于先打哪一个,列出方程贪心找规律(相似题目:AcWing 125. 耍杂技的牛)
打两个敌人时:\(hp\, - t_i\, + \, h_i \, - \, t_{i + 1}\, + \, h_{i + 1}\)
两个可能会死的地方:
- \(hp\, - t_i\)
- \(hp\, - t_i\, + \, h_i \, - \, t_{i + 1}\)
对于第一个地方,因为打的都是扣血怪,所以无论先打 \(i\) 还是先打 \(i+1\),只要有一个 \(t>hp\) 就会去世
对于第二个地方,先打 \(h\) 更大的回的血多一点不容易去世,所以先打 \(h\) 更大
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
#define iofast ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
typedef pair<int, int> pii;
const int N = 200010;
pii ary[N];
signed main()
{
int time; cin >> time;
while(time--)
{
int n, m; cin >> n >> m;
bool flag = true;
int cnt = 1, hp = m;
for(int i = 1; i <= n; i++)
{
int t, h; cin >> t >> h;
h = min(h, m); // 实际能补的血
if(h - t >= 0) hp += h - t;
else ary[cnt++] = {h, t};
}
// 一个一个打那些打完扣血的
// 根据 h 排序,先打 h 大的
sort(ary + 1, ary + 1 + cnt - 1);
for(int i = cnt - 1; i >= 1; i--)
{
hp -= ary[i].second;
if(hp < 0)
{
flag = false;
break;
}
hp += ary[i].first;
}
if(flag) cout << "YES" << endl;
else cout << "NO" << endl;
}
}
L. Not the Longest Increasing Subsequence
题目描述
给定长度 \(n\) 的数组,数组元素大小在 \(1\) 到 \(k\) 之间 \((1≤n≤10^6,1≤k≤n)\)
问最少删除多少个元素,使得数组中的 \(LIS\) 长度都小于 \(k\)
输出最少删除的个数和要删除元素的下标
分析规律
- 因为元素大小在 \(1\) 到 \(k\) 之间,所以长度为 \(k\) 的 \(LIS\) 一定是 \(1, 2, ..., k\)
- 要删除长度为 \(k\) 的 \(LIS\) 有两种选择:
- 删除第 \(k\) 个数
- 删除前面的数,使得不存在长度为 \(k - 1\) 的 \(LIS\)
分析得,该题为需要记录状态的线性DP
状态表示:\(f\,[\,i\,]\) 为删除长度为 \(i\) 的 \(LIS\) 需要的最小次数
状态转移方程:\(f\,[\,i\,] = min(\,f\,[\,i\,] + 1, \,f\,[\,i\,])\)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
typedef pair<int, int> pii;
const int N = 1000100;
int ary[N];
int f[N], g[N];
signed main()
{
int n, k; cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> ary[i];
for(int i = 1; i <= n; i++)
{
int v = ary[i];
g[i] = f[v]; // 记录状态转移过程
if(v == 1) f[v]++; // 边界条件
// 可能是删除当前数,或者删除LIS前面的数
else f[v] = min(f[v] + 1, f[v - 1]);
}
cout << f[k] << endl;
// 从尾开始扫描,逆着查询状态转移的过程
int cur = k; // 当前要消除的LIS的长度
for(int i = n; i >= 1; i--)
{
int v = ary[i];
if(v == cur)
{
if(f[v] == g[i] + 1) // 当前数被删除(删龙头)
cout << i << " ";
else // 消除更短的LIS(删龙身)
cur--;
}
f[v] = g[i]; // 往前恢复状态
}
}
M. Binary Search Tree
题目大意
给定一颗由 \(n\) 个点组成的无根树 \((1≤n≤500000)\)
请你求出能作为二叉搜索树根节点的点,若没有这样的点则输出 -1
分析规律
若有拥有 4 条边或以上的点则谁都不能作为BST根节点,拥有3条边的点一定不是根节点。
若一个点可以作为BST根节点,则它的子树肯定是BST,而且它本身包括合法子树的数值范围一定是 \([1, \,n]\)
要得到每个点作为根节点可以表示的数值范围,就必须得到它每一条边上的子树的数值范围
时间不能是 \(O(n^2)\) ,所以必须整棵树一起遍历,不能对于某个节点遍历整棵树
解题
选定一个边小于3的点作为初节点,使用两次 dfs 。
第一次 dfs 求出从上到下的方向上,每个点及其子树作为BST的数值范围(若不能构成BST,则无表示范围)
第二次 dfs 依旧从上到下遍历,求两个值
- 每个点的BST子树为遍历过程的父节点,求出逆方向的数值范围
- 求每个点作为根节点表示的数值范围(此时每个点以各个方向为子树的数值范围已经求出)
(注意:输入较大开 iosfast)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define double long double
#define iosfast ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
typedef pair<int, int> pii;
const int N = 1000010;
int h[N], e[N], ne[N], idx, dg[N];
// dp[i][j]表示以 j 为父节点的方向上,i节点包含数的范围
unordered_map<int, pii> dp[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
pii merge(int x, const vector<int>& child)
{
if(child.empty()) return make_pair(x, x);
if(child.size() == 1)
{
auto v = dp[child[0]][x];
if(x <= v.first) return make_pair(x, v.second);
else if(x >= v.second) return make_pair(v.first, x);
else return make_pair(-INT32_MAX, INT32_MAX);
}
else
{
auto lc = dp[child[0]][x], rc = dp[child[1]][x];
if(lc.first > rc.first) swap(lc, rc);
if(lc.second <= x && x <= rc.first)
return make_pair(lc.first, rc.second);
else
return make_pair(-INT32_MAX, INT32_MAX);
}
}
void dfs1(int x, int fa)
{
vector<int> child;
for(int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if(j == fa) continue;
child.push_back(j);
dfs1(j, x); // 后序遍历,先访问完子节点,再操作当前节点
}
// 以 x 节点为父节点(fa方向),可以表示的范围
// 即合并 x 的值和(x为父节点时)子节点可表示的范围
dp[x][fa] = merge(x, child);
}
void dfs2(int x, int fa)
{
vector<int> child;
for(int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if(j == fa) continue;
child.push_back(j);
}
/*
* 对于每个节点做两件事
* 1. 计算逆方向时,节点 x 的子节点能表示的范围,dp[child][x]
* 2. 计算以 x 为根节点能表示的范围,dp[x][x]
*/
if(child.empty())
dp[x][x] = fa == x ? make_pair(x, x) : merge(x, {fa});
else if(child.size() == 1)
{
if(fa == x)
dp[x][child[0]] = make_pair(x, x);
else
{
dp[x][child[0]] = merge(x, {fa});
dp[x][x] = merge(x, {child[0], fa});
}
}
else if(child.size() == 2)
{
if(fa == x)
{
dp[x][child[0]] = merge(x, {child[1]});
dp[x][child[1]] = merge(x, {child[0]});
}
else
{
dp[x][child[0]] = merge(x, {child[1], fa});
dp[x][child[1]] = merge(x, {child[0], fa});
}
}
// 前序遍历
for(auto v : child)
dfs2(v, x);
}
signed main()
{
iosfast;
memset(h, -1, sizeof h);
int n; cin >> n;
for(int i = 1; i <= n - 1; i++)
{
int a, b; cin >> a >> b;
add(a, b), add(b, a);
dg[a]++, dg[b]++;
}
int rt = -1;
for(int i = 1; i <= n; i++)
{
if(dg[i] >= 4)
{
cout << -1 << endl;
return 0;
}
if(dg[i] <= 2 && rt == -1) rt = i;
}
dfs1(rt, rt); // 计算一个方向下每个点的表示范围
dfs2(rt, rt); // 记录反方向每个点的表示范围
vector<int> ans;
for(int i = 1; i <= n; i++)
if(dp[i][i] == make_pair((int)1, n))
ans.push_back(i);
if(ans.empty()) cout << -1 << endl;
else
{
for(auto v : ans)
cout << v << " ";
cout << endl;
}
return 0;
}

浙公网安备 33010602011771号