2026CSP前集訓
9月6日
AT_agc005_c [AGC005C] Tree Restoring
思路
对于任意一个点,离他最远的点一定是它直径两端点中的一个,我们容易因此想到找直径。
直径的两端点一定是 \(a\) 值最大的,从两端到中间到两端的 \(a\) 值从 \(a\) 下降到 \(a/2\) 再上升到 \(a\)。
所以我们开个桶,第 \(i\) 位记录 \(a\) 值为 \(i\) 的有几个,然后减去构造直径所需的点。
-
如果发现构造不出来直径。(减去之后为剩余点数为负)
-
如果发现有比直径上最小值更小的点。(由最前面的结论知,其他点的 \(a\) 值不可能小于直径上的最小值)
就无法构造出这棵树。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int n, a[N], cnt[10005], maxx, minn;
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
maxx = max(maxx, a[i]);
minn = min(minn, a[i]);
cnt[a[i]] ++;
}
sort(a + 1, a + n + 1);
for(int i = 2; i <= n; i ++)
{
if(a[i] != a[i - 1] && a[i] != a[i - 1] + 1)
{
cout << "Impossible" << endl;
return 0;
}
if(a[i] > a[1] * 2 + 1)
{
cout << "Impossible" << endl;
return 0;
}
}
if(a[2] == a[1] && a[1] == 1 && n != 2)
{
cout << "Impossible" << endl;
return 0;
}
if(maxx & 1)
{
if(cnt[(maxx + 1) / 2] != 2)
{
cout << "Impossible" << endl;
return 0;
}
}
else if(cnt[maxx / 2] != 1)
{
cout << "Impossible" << endl;
return 0;
}
cout << "Possible" << endl;
return 0;
}
AT_agc033_c [AGC033C] Removing Coins
思路
考虑操作的本质,即选定一个 \(x\) 删除树上的所有叶子(除了 \(x\))。
自然的想到了直径(使这棵树可以被操作更多次),如果我们选取的 \(x\) 是直径的端点,那么直径的长度会减少 \(1\),否则减少 \(2\)。
为什么呢?因为直径的两个端点一定都是叶子,而如果我们选取了某一个端点,那么这个端点不会被删除。
然后就像是个 dp 了,当某一个直径长度 \(y\) 不能使我们赢时,那么 \(y + 1\) 和 \(y + 2\) 的长度我们是一定能赢的。
设 \(dp_i\) 为当前直径长度 \(i\) 先手是否能赢(是:1,否:0),则有 \(dp_i = (!dp[i - 1]) || (!dp[i - 2])\)。
初始 \(dp_1 = 1, dp_2 = 0\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, maxx, pre, s, t, dp[N];
vector<int> g[N];
void dfs(int u, int sum, int fa)
{
if(sum > maxx) maxx = sum, pre = u;
for(int v : g[u])
{
if(v == fa) continue;
dfs(v, sum + 1, u);
}
}
signed main()
{
cin >> n;
for(int i = 1; i < n; i ++)
{
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0, 0);
s = pre;
maxx = 0;
dfs(s, 1, 0);
dp[1] = 1, dp[2] = 0;
for(int i = 3; i <= maxx; i ++)
{
dp[i] = (!dp[i - 1]) || (!dp[i - 2]);
}
cout << (dp[maxx] == 0 ? "Second\n" : "First\n");
}
9月13日
P1776 宝物筛选
思路
二进制拆分优化多重背包模板题。(其实也可以用单调队列优化做,但是我现在只会二进制拆分)
考虑将个 \(x\) 拆成 \(2\) 的幂次,根据我们的直觉,这些数一定能表示出所有 \(x\) 以内的数的。
缺点:费空间
(单调队列做法以后会了再补)
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, c, v[N], w[N], cnt, dp[N];
signed main()
{
cin >> n >> c;
for(int i = 1; i <= n; i ++)
{
int x, y, m; cin >> y >> x >> m;
int now = 1;
while(m)
{
int t = min(now, m);
v[++ cnt] = x * t;
w[cnt] = y * t;
m -= t;
now <<= 1;
}
}
for(int i = 1; i <= cnt; i ++)
for(int j = c; j >= v[i]; j --)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[c] << endl;
}
P6005 [USACO20JAN] Time is Mooney G
思路
考虑 dp。
出差的花费很烦我们先不管它。
设 \(dp[i][j]\) 为第 \(j\) 天到达第 \(i\) 座城市能够得到的最大收益。
则可以列出状态转移方程:\(dp[i][j] = max(dp[v][j - 1]) + m[i])\),其中 \(v\) 是可以通过 \(v\) 直接到达 \(i\) 的节点。
那么答案就是 \(dp[1][j] - (c * j ^ 2)\) 的最大值。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1005;
int n, m, c, dp[N][N], val[N], ans;
const int inf = 1e9;
vector<int> g[N];
signed main()
{
cin >> n >> m >> c;
for(int i = 1; i <= n; i ++)
{
cin >> val[i];
}
for(int i = 1; i <= m; i ++)
{
int u, v; cin >> u >> v;
g[v].push_back(u);
}
for(int i = 0; i <= n; i ++)
{
if(i == 1) continue;
for(int j = 0; j <= 1000; j ++)
{
dp[i][j] = -inf;
}
}
for(int i = 1; i <= 1000; i ++)
{
for(int j = 1; j <= n; j ++)
{
for(int v : g[j])
{
dp[j][i] = max(dp[j][i], dp[v][i - 1] + val[j]);
}
}
ans = max(ans, dp[1][i] - c * i * i);
}
cout << ans << endl;
}
9月26日
P1541 [NOIP 2010 提高组] 乌龟棋
思路
很暴力的一道題。
設 \(dp[a][b][c][d]\) 為當前使用了 \(a\) 張前進 \(1\) 格的卡,\(b\) 張前進兩格的...依此類推。
則直接暴力轉移即可。
#include<bits/stdc++.h>
using namespace std;
int dp[41][41][41][41], cnt[5], x[405], n, m;
signed main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> x[i];
for(int i = 1; i <= m; i ++)
{
int x; cin >> x;
cnt[x] ++;
}
dp[0][0][0][0] = x[1];
for(int a = 0; a <= cnt[1]; a ++)
{
for(int b = 0; b <= cnt[2]; b ++)
{
for(int c = 0; c <= cnt[3]; c ++)
{
for(int d = 0; d <= cnt[4]; d ++)
{
int val = a + b * 2 + c * 3 + d * 4 + 1, maxx = 0;;
if(a) maxx = max(maxx, dp[a - 1][b][c][d]);
if(b) maxx = max(maxx, dp[a][b - 1][c][d]);
if(c) maxx = max(maxx, dp[a][b][c - 1][d]);
if(d) maxx = max(maxx, dp[a][b][c][d - 1]);
dp[a][b][c][d] = maxx + x[val];
}
}
}
}
cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]] << endl;
}
P1868 飢餓的奶牛
思路
設 \(dp[i]\) 為從 \(0\) 到 \(i\) 中最多有多少草,欽定 \(l\), \(r\) 為一個區間的左右端點。
則有 \(dp[r] = dp[l - 1] + r - l + 1\)。
把所有區間按照左端點排個序,然後按照順序依次轉移就做完了。
#include<bits/stdc++.h>
using namespace std;
int dp[3000005], n, maxx, cur = 1, ans;
struct seq
{
int l, r;
}a[150005];
bool cmp(seq a, seq b)
{
if(a.l == b.l) return a.r < b.r;
return a.l < b.l;
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> a[i].l >> a[i].r;
maxx = max(maxx, a[i].r);
}
sort(a + 1, a + n + 1, cmp);
for(int i = 0; i <= maxx; i ++)
{
if(i) dp[i] = max(dp[i], dp[i - 1]);
while(a[cur].l == i && cur <= n)
{
dp[a[cur].r] = max(dp[a[cur].r], dp[a[cur].l - 1] + a[cur].r - a[cur].l + 1);
cur ++;
}
ans = max(ans, dp[i]);
}
cout << ans << endl;
}
P2245 星际导航
思路
先跑一遍最小生成树。
考虑在求 LCA 时就把最大危险度给求出来,于是查询时直接查两点到他们的 LCA 上的最大危险度就可以了。
询问时直接输出即可,注意最小生成树跑出来可能时森林,需要用并查集判断两点在不在同一个连通块内。
/*
做法:
考慮求出最小生成樹。
然後最大危險值就是兩個節點到他們最近公共祖先中的路徑的最大值。
在 LCA 中預處理一下最大值就可以了。
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
struct edge2
{
int u, v, w;
}ve[N];
struct edge
{
int v, nxt, w;
}e[N];
int head[N], num;
int n, m, numve, fa[N], f[N][20], sum[N][20], dep[N];
bool cmp(edge2 a, edge2 b)
{
return a.w < b.w;
}
int find(int x)
{
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void dfs(int u)
{
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].v;
if(!dep[v])
{
dep[v] = dep[u] + 1;
f[v][0] = u;
sum[v][0] = e[i].w;
dfs(v);
}
}
}
int lca(int x, int y)
{
int res = 0;
if(dep[y] > dep[x]) swap(x, y);
for(int i = 17; i >= 0; i --)
{
if(dep[f[x][i]] >= dep[y])
{
res = max(res, sum[x][i]);
x = f[x][i];
}
}
for(int i = 17; i >= 0; i --)
{
if(f[x][i] != f[y][i])
{
res = max(res, max(sum[x][i], sum[y][i]));
x = f[x][i], y = f[y][i];
}
}
if(x == y) return res;
return max(res, max(sum[x][0], sum[y][0]));
}
signed main()
{
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
int x, y, z; cin >> x >> y >> z;
ve[++ numve] = {x, y, z};
}
for(int i = 1; i <= n; i ++) fa[i] = i;
sort(ve + 1, ve + numve + 1, cmp);
for(int i = 1; i <= numve; i ++)
{
int v = ve[i].v, u = ve[i].u, w = ve[i].w;
int fu = find(u), fv = find(v);
if(fu == fv) continue;
fa[fu] = fv;
e[++ num] = {v, head[u], w}, head[u] = num;
e[++ num] = {u, head[v], w}, head[v] = num;
}
for(int i = 1; i <= n; i ++)
{
if(!dep[i])
{
dep[i] = 1;
dfs(i);
f[i][0] = i, sum[i][0] = 0;
}
}
for(int i = 1; i < 18; i ++)
{
for(int j = 1; j <= n; j ++)
{
f[j][i] = f[f[j][i - 1]][i - 1];
sum[j][i] = max(sum[j][i - 1], sum[f[j][i - 1]][i - 1]);
}
}
int q; cin >> q;
while(q --)
{
int x, y; cin >> x >> y;
if(find(x) != find(y)) cout << "impossible" << endl;
else cout << lca(x, y) << endl;
}
}
P3065 [USACO12DEC] First! G
思路
9月27日
模拟赛
T1
考场思路
看着很像字典树+计数dp的东西,推了推似乎没有思路。
dfs 预计得分:10pts
T2
考场思路
暴力模拟。
对于操作 1,直接搜索 \(x\) 的子树,更新答案。
对于操作 2,求出 lca,然后搜索两点到 lca 的路径。
期望得分:40pts
T3
以前做过原题,但是忘记怎么写树上背包了qwq
思路
设 \(dp[i][j][0/1][0/1]\) 为以 \(i\) 为根的子树中放了 \(j\) 个监视器,\(i\) 上 无/有 放监视器,\(i\) 无/有 被它的子结点监视的方案数。
开一个 \(f[i][j]\) 数组,表示 \(i\) 为根的子树中放 \(j\) 个监视器的方案数,然后树上背包(我居然忘记怎么写树上背包了)
T4
唉。有正解的思路但没正解的代码实现能力,一直没调出来。
思路
考虑分块。
设 \(step[i]\) 为从 \(i\) 出发被弹出 \(i\) 所在的块所需要的步数,\(to[i]\) 为从 \(i\) 出发会到达的第一个不在块内的地点。
这两个数组都是可以预处理出来的,从后往前算,算过的可以直接用,这样就不用重复算了。
查询就是按照 \(to[i]\) 往后推,途中累加 \(step[i]\),直到 \(to[i]\) 大于 \(n\) 为止。
修改由于 \(to\) 和 \(step\) 只和块内的值有关,所以就暴力把这个块内的值全部重新算一遍。
由于我的代码实现能力太差了,所以我考场上的的方法是从前往后暴力推,极限数据下修改的复杂度是 \(O(n)\) 的。
考场期望得分:70pts

浙公网安备 33010602011771号