前言
整体评价
内测的时候,做题做一半忍不住仔细看了遍标题,确实是小白月赛啊,这个难度跟平时的小白月赛比起来要高很多,属实是上强度了。
A.伊甸之花
贪心
根据题意,要找到一个相似的,最优的就是将原先的曲子往下平移一格,或者往上平移一格。而如果数组里有一个数顶着上界,即 那么无法上移,如果有 ,则无法下移。用两个变量记录一下,如果既无法上移又无法下移,那么就输出 "No"
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
const int mod = 998244353;
int arr[maxn];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
int flag1 = 1, flag2 = 1;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
if (arr[i] == 1)
flag1 = 0;
if (arr[i] == m)
flag2 = 0;
}
cout << ((flag1 | flag2) ? "Yes" : "No") << endl;
// system("pause");
return 0;
}
B.显生之宙
贪心
根据题意,设想一下要让结果最小,应该如何操作。很显然对于负数,把其他所有数都加上这个数,对于正数,只选择一个数增加。
然后为了让负数最大化利用,将数组从小到大排序,让最小的数放前面,就可以尽可能让更多的数加上这个负数。然后就是模拟前面说的这个操作的过程。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 5e5 + 10;
int arr[maxn];
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> arr[i];
sort(arr + 1, arr + 1 + n);
int sum = 0;
for (int i = 1; i <= n; i++) {
arr[i] += sum;
if (arr[i] < 0)
sum += arr[i];
else if (i != n)
arr[n] += arr[i];
}
cout << arr[n] << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--)
solve();
return 0;
}
C.太阳之华
猜结论题,首先感谢样例里面把三种情况全都涉及到了
观察样例,直接猜以下结论:开始全都是蓝,则直接后手赢,如果红能一步赢,则先手赢,否则肯定会僵持住,就是平局。(证明不来)
所以这题主要代码考察如何判断红色一步直接赢,我的想法是这样:
用并查集把所有红色的连通块都缩在一起,统计选择每个连通块,能够增加的蓝色方块个数,若某个连通块该值等于场上所有蓝色方块个数,则可以一步先手赢。
再具体一点,就是遍历所有蓝色方块,四个方向枚举相邻的红色连通块,加入对应连通块编号的 set 中(比如围成圈的红色连通块,内部的一个蓝色,可能导致该连通块个数加了四次,所以用set去重,记录对应的蓝色块编号)。最好遍历所有set,找是否有
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e3 + 10;
char ch[maxn][maxn];
int bel[maxn][maxn];
int pianx[4] = {1, 0, -1, 0}, piany[4] = {0, 1, 0, -1};
int n, m;
set<int> s[1000000];
void dfs(int x, int y, int id) {
ch[x][y] = '@';
bel[x][y] = id;
for (int i = 0; i < 4; i++) {
int xx = x + pianx[i], yy = y + piany[i];
if (xx < 1 || xx > n || yy < 1 || yy > m || ch[xx][yy] != '#')
continue;
dfs(xx, yy, id);
}
}
void solve() {
cin >> n >> m;
int cntb = 0, cnt = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> ch[i][j];
cntb += ch[i][j] == '.';
}
if (cntb == n * m) {
cout << "Blue" << endl;
return;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (ch[i][j] == '#')
dfs(i, j, ++cnt);
}
for (int i = 1; i <= cnt; i++)
s[i].clear();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (ch[i][j] == '.') {
for (int k = 0; k < 4; k++) {
int ii = i + pianx[k], jj = j + piany[k];
if (ii < 1 || ii > n || jj < 1 || jj > m ||
ch[ii][jj] != '@')
continue;
s[bel[ii][jj]].insert((i - 1) * m + j);
}
}
for (int i = 1; i <= cnt; i++)
if (s[i].size() == cntb) {
cout << "Red" << endl;
return;
}
cout << "Draw" << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--)
solve();
return 0;
}
D.冥古之潮
bfs+dp,这个dp是比较典的,但是之前没见过可能忘了,所以卡题了
题干还是有点阅读理解的,这里简述一下题意:
个点, 条边的无向图,有一个特殊结点 ,接下来多次询问,每次询问给定数量 ,回答 选择 个到 的距离不同的结点 的方案数。
先单独考虑,对于确定的 ,应该要怎么做。
一个显然的步骤是,以 为起点,bfs一遍图,处理出来数组 表示距离 为 的结点的数量。
然后得到 数组后,没什么想法,以为会是组合数什么的乱搞,就卡住了。补题才知道,这里有个很典的DP模型。
设二维DP数组 表示考虑到距离 为止,选择了 个点的方案数,那么转移方程就分为距离 选或者不选两种可能。不选,直接转移。选了的话,距离 的点共有 个,所以乘 , 即
注意取模问题。
会发现,按这种方式DP的话,选择点的数量是由小到大转移,所以直接预处理选点个数到最大为止,之后每次询问都直接 回答 即可。
代码:
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 1e6 + 10;
const int maxm = 5e3;
const int mod = 1e9 + 7;
vector<int> G[maxn];
int dep[maxn], d[maxn], vis[maxn];
int dp[maxm][maxm];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m, q, x;
cin >> n >> m >> q >> x;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
queue<int> que;
que.push(x);
vis[x] = 1;
int mxdep = 0;
while (!que.empty()) {
int u = que.front();
que.pop();
for (auto v : G[u]) {
if (vis[v])
continue;
vis[v] = 1;
dep[v] = dep[u] + 1;
d[dep[v]]++;
mxdep = max(mxdep, dep[v]);
que.push(v);
}
}
dp[0][0] = 1;
for (int i = 1; i <= mxdep; i++) {
dp[i][0] = 1;
for (int j = 1; j <= mxdep; j++) {
dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - 1] * d[i]) % mod;
}
}
while (q--) {
int k;
cin >> k;
cout << dp[mxdep][k] << endl;
}
// system("pause");
return 0;
}
E.神性之陨
通过看样例的图片,会发现,为了满足题目中 只存在一条路径 ,选择的竖条都是错落开的。定义一列中,选择的连续方格为一个竖条,则合法的路径选择的竖条,要么前面的上端点和后面的下端点相连,要么前面的下端点和后面的上端点相连,相连的地方就是唯一路径在这两列中的走法。
所以自然会想到,开一个三维 数组,表示在第 行, 第 列的方格,能否作为上/下端点。又因为能作为端点的地方,都可以作为唯一路径上的点,故去统计最后一列里,能作为端点的格子数量即为答案。
转移方程也很好写,直接 的枚举每个格子,左边能作为端点的地方,判断这一列的竖条能否不超界的对应放置。
还有注意一下下标访问会不会越界,也就是竖条放置会不会超出界限的问题,很容易就把程序写好,然后发现样例过不去
看到第二个样例里面,最后统计的时候少统计了,思考原因,发现是因为最后一列中的 不需要在前一列的端点处才能放。因为长度为1的竖条只要相邻,都能使路径在这一列上唯一,所以长度为 的竖条能放置的地方是前一列所有 能放置方块 的地方,不要求必须是端点处。
这个需求跟差分的特性很相近,所以会考虑使用差分数组来维护,如果某个格子 能当上端点,那么这一列里,能放置方块的行数范围就包括了 ,然后每一列去维护这个差分数组,碰到竖条长度为1的,就使用差分数组更新 DP 数组,而非,前一列的DP数组更新。
下面讲一下我出现的错误,可选择性跳过
核心思路就是这样,但我代码写挂了,喜提出题人嘲笑
我写挂的原因是,我处理差分数组是直接对差分数组进行前缀和,所以遇到连续的1时,就对差分数组错误处理,所以错了。思路很对,但是wa了的人可以试一下下面这组样例
1
50 50
1 24 20 18 20 9 16 8 12 4 13 20 1 22 15 22 11 16 9 20 8 9 4 18 11 11 13 17 17 13 25 24 21 4 23 21 16 2 13 9 7 20 6 28 14 19 14 2 1 1
答案是42
代码环节
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define pii pair<int, int>
using namespace std;
const int maxn = 5e3 + 10;
int dp[maxn][maxn][2]; // 第i行第j列能否成为 上/下 端点
int arr[maxn];
void solve() {
int n, m;
cin >> n >> m;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++)
dp[i][j][0] = dp[i][j][1] = 0;
for (int i = 1; i <= m; i++)
cin >> arr[i];
vector<int> sub(n + 2);
sub[1] = 1;
sub[n + 1] = -1;
for (int j = 1; j <= m; j++) {
if (arr[j] == 1) {
vector<int> tem(n + 2);
for (int i = 1; i <= n; i++) {
sub[i] += sub[i - 1];
if (sub[i] > 0) {
dp[i][j][0] = dp[i][j][1] = 1;
tem[i]++;
tem[i + 1]--;
}
}
swap(sub, tem);
continue;
}
for (int i = 0; i <= n + 1; i++)
sub[i] = 0;
for (int i = 1; i <= n; i++) {
if (dp[i][j - 1][0]) { // 前一列能当上端点
if (i - arr[j] + 1 >= 1) {
dp[i][j][1] = dp[i - arr[j] + 1][j][0] = 1;
sub[i - arr[j] + 1]++;
sub[i + 1]--;
}
}
if (dp[i][j - 1][1]) {
if (i + arr[j] - 1 <= n) {
dp[i][j][0] = dp[i + arr[j] - 1][j][1] = 1;
sub[i]++;
sub[i + arr[j]]--;
}
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
ans += dp[i][m][0] | dp[i][m][1];
cout << ans << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--)
solve();
return 0;
}
F.无垢之土
怎么说呢,这道题我现在依旧不太懂为什么树形dp后会自动考虑到越过顶点的情况,只是照着题解的思路,写一份仅供参考的代码放这里吧。
题解思路是:对于答案的两个顶点 和 ,在树上会有lca ,则他们相遇的时间两倍是
然后通过二分消除出生条件的限制,即二分答案时间,只去维护出生时间二倍小等于当前二分的时间的人。
我一开始想维护每个节点,它的子树中的人到达该节点的最短时间,然后取前2短的相加。不过这种思路就是答案思路去掉二分的情况,一个hack的样例就比如,2个节点,1为根,0s时,一号节点有人出生,2s时二号节点有人出生。然后不二分的话,在1号节点统计这两个人到达时间分别是0s和3s。但是正确答案显然是1号节点到2号节点等他出生,2s时二人相遇。
代码
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 1e5 + 10;
vector<int> G[maxn];
vector<int> t[maxn];
vector<int> mn[maxn];
int ans = inf;
int n, m;
void dfs(int u, int f, int mid) {
vector<int> tem;
mn[u].clear();
for (auto it : t[u]) {
if (it * 2 <= mid)
tem.push_back(it);
}
for (auto v : G[u]) {
if (v == f)
continue;
dfs(v, u, mid);
for (auto it : mn[v])
tem.push_back(it + 1);
}
sort(tem.begin(), tem.end());
for (int i = 0; i < min(2, (int)tem.size()); i++)
mn[u].push_back(tem[i]);
if (tem.size() >= 2)
ans = min(ans, tem[0] + tem[1]);
}
bool check(int mid) {
ans = inf;
dfs(1, 1, mid);
if (ans <= mid)
return 1;
return 0;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 1; i <= m; i++) {
int a, tt;
cin >> a >> tt;
t[a].push_back(tt);
}
int l = 0, r = 1e9, ans;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid))
ans = mid, r = mid - 1;
else
l = mid + 1;
}
cout << ans << endl;
// system("pause");
return 0;
}
说在最后
第一次参加牛牛的内测,激动之余,模仿珂朵莉的风格放一些漂亮的图片来写题解。