贪心构造题练习
P12041 [USTCPC 2025] 图上交互题 2 / Constructive Minimum Mex Path
显然 \(f(u,v)\) 只有 \(0\) 和 \(1\) 两种取值。
当 \(f(u,v)=1\) 时,显然有 \(w(u,v)=0\),且 \(u\to v\) 的任意路径上,都存在边权为 \(0\) 的边。记删掉所有边权为 \(0\) 的边后新图为 \(G\),则 \(G(u,v)\) 不连通。
当 \(f(u,v)=0\) 时, 即 \(u\to v\) 的任意路径上,存在至少一条路径满足不存在边权为 \(0\) 的边,此时 \(G(u,v)\) 连通,另 \(w(u,v)=1\) 不会使图 \(G\) 的连通性更优。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int kN = 1e5 + 7;
int n, m;
bool vis[kN];
struct node{
int u, v, f, w;
}e[kN];
struct DSU{
int fa[kN], siz[kN];
void init(){
for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
}
int find(int x){
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void merge(int x, int y){
x = find(x), y = find(y);
if(x ^ y){
if(siz[x] > siz[y]) swap(x, y);
fa[x] = y;
siz[y] += siz[x];
}
}
}f;
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
f.init();
bool fg = 1;
for(int i = 1; i <= m; i++){
cin >> e[i].u >> e[i].v >> e[i].f;
if(e[i].f > 1) fg = 0;
e[i].w = e[i].f ^ 1;
if(e[i].w) f.merge(e[i].u, e[i].v);
}
if(!fg){
cout << "NO\n"; return 0;
}
for(int i = 1; i <= m; i++){
if(e[i].f == 1){
if(f.find(e[i].u) == f.find(e[i].v)){
cout << "NO\n"; return 0;
}
}
}
cout << "YES\n";
for(int i = 1; i <= m; i++) cout << e[i].w << ' ';
cout << '\n';
return 0;
}
P12042 [USTCPC 2025] 图上交互题 3 / Constructive Maximum Mex Path
由于可以经过重复的边,所以同一个连通块中任意两点的 \(u,v\) 的函数值 \(f(u,v)\) 应该相等。
假设 \(f(u,v)=k\),那这个连通块中要出现边权为 \([0,k)\) 且不能出现边权为 \(k\) 的边。于是我们随便赋值即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int kN = 1e5 + 7;
int n, m, a[kN];
struct edge{
int u, v, f, w;
}e[kN];
vector<int> pev[kN];
struct DSU{
int fa[kN], siz[kN];
void init(){
for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
}
int find(int x){
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void merge(int x, int y){
x = find(x), y = find(y);
if(x ^ y){
if(siz[x] > siz[y]) swap(x, y);
fa[x] = y, siz[y] += siz[x];
}
}
}f;
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
f.init();
for(int i = 1; i <= m; i++){
cin >> e[i].u >> e[i].v >> e[i].f;
f.merge(e[i].u, e[i].v);
}
for(int i = 1; i <= n; i++) a[i] = -1;
for(int i = 1; i <= m; i++){
if(a[f.find(e[i].u)] == -1) a[f.find(e[i].u)] = e[i].f;
else{
if(a[f.find(e[i].u)] != e[i].f){
cout << "NO\n"; return 0;
}
}
pev[f.find(e[i].u)].push_back(i);
}
for(int i = 1; i <= n; i++){
if(f.find(i) != i) continue;
if(a[i] > (int)pev[i].size()){
cout << "NO\n"; return 0;
}
int v = 0;
for(int j : pev[i]){
if(a[i]) e[j].w = v, v = (v + 1) % a[i];
else e[j].w = 1;
}
}
cout << "Yes\n";
for(int i = 1; i <= m; i++) cout << e[i].w << ' ';
cout << '\n';
return 0;
}
P12043 [USTCPC 2025] 图上交互题4 / Constructive Shortest Path
结论:\(w(i,j)=f(i,j)\)
证明:考虑图 \(G'=(u_i,v_i,f(u_i,v_i))\)。
因为 \(G'_{x,y}=f(x,y)\),所以 \(dis_{G'}(x,y)\le G'(x,y)=f(x,y)\)。
由三角形不等式得:\(dis(x,z)+dis(z,y)\ge dis(x,y)\)。而 \(G'\) 中任意两点的距离都为原图中两点的最短距离,所以 \(G'\) 中任意一条路径 \(x\to \dots \to y\) 的长度大于等于原图中两点 \((x,y)\) 的最短距离,即 \(dis_{G'}(x,y)\ge dis_G(x,y)=f(x,y)\)
所以 \(dis_{G'}(x,y)=f(x,y)\)。
这是一个经典结论,可以记下来。
经典结论
对于无负环的有向图或无向图,令新图为原图将边的权值改为两点之间的最短距离(有向图注意方向),新图中任意两点的最短距离等于原图中同样两点的最短距离。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int kN = 510;
const int kM = 1e5 + 7;
int n, m, d[kN][kN], f[kN][kN];
struct edge{
int u, v, f;
}e[kM];
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) d[i][j] = f[i][j] = 1e9;
for(int i = 1; i <= n; i++) f[i][i] = 0;
for(int i = 1, u, v, w; i <= m; i++){
cin >> u >> v >> w;
if(u == v && w){
cout << "NO\n"; return 0;
}
e[i] = {u, v, w};
d[u][v] = d[v][u] = f[u][v] = f[v][u] = min(f[u][v], w);
}
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
if(i != k){
for(int j = 1; j <= n; j++){
if(i != j && k != j) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
for(int i = 1; i <= m; i++){
if(e[i].f != d[e[i].u][e[i].v]){
cout << "NO\n"; return 0;
}
}
cout << "YES\n";
for(int i = 1; i <= m; i++) cout << e[i].f << ' ';
cout << '\n';
return 0;
}
P10178 陌路寻诗礼
从部分分 \(k=10^9\) 入手,此时从原图中随便拉出一棵以 \(1\) 为根的外向树,树边边权为 \(1\),非树边边权为 \(k\)。
考虑将上述做法拓展到一般情况,我们依然要从原图中拉出一棵以 \(1\) 为根的外向树,树边边权为 \(1\),非树边边权为 \(k\)。但是我们能随便拉出一棵树吗?
显然是不能的。
Hack
.in
4 5 2
1 2
1 3
2 4
3 4
1 4
.out
Yes
1 1 2 2 1
发现错因在 \(k\) 不够大,于是我们可以拉出一棵 \(bfs\) 生成树,在跑上面的做法就可以了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kN = 3e5 + 7;
int n, m, k, w[kN], vis[kN], dis[kN];
struct edge{
int u, v, w;
}e[kN];
vector<pii> ve[kN];
void Pre(int x){
queue<int> que;
que.push(x);
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = 1;
for(auto j : ve[u]){
if(vis[j.first]) continue;
vis[j.first] = 1;
w[j.second] = 1;
que.push(j.first);
}
}
}
priority_queue<pii, vector<pii>, greater<pii> > que;
bool bfs(int x){
for(int i = 1; i <= n; i++) dis[i] = 2e9, vis[i] = 0;
while(!que.empty()) que.pop();
que.push(mp(0, x));
while(!que.empty()){
pii u = que.top();
que.pop();
if(vis[u.second]){
if(u.first == dis[u.second]) return false;
continue;
}
vis[u.second] = 1;
dis[u.second] = u.first;
for(auto j : ve[u.second]){
que.push(mp(dis[u.second] + w[j.second], j.first));
}
}
return true;
}
void solve(){
cin >> n >> m >> k;
for(int i = 1; i <= m; i++){
cin >> e[i].u >> e[i].v;
ve[e[i].u].push_back(mp(e[i].v, i));
w[i] = k;
}
Pre(1);
if(bfs(1)){
cout << "Yes\n";
for(int i = 1; i <= m; i++) cout << w[i] << ' ';
cout << '\n';
}
else cout << "No\n";
for(int i = 1; i <= n; i++) ve[i].clear(), vis[i] = 0;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1; cin >> T;
while(T--) solve();
return 0;
}
P12676 相等排列(equal)
当 \(n=1\) 时,若 \(m\ne 1\),无解;否则,简单。
当 \(2\mid n\) 时,则正序降序接替放。
当 \(2\nmid n\) 时,考虑每个元素在序列中的位置和为 \(\frac {(m+1)n}{2}\)。
-
当 \(2\mid m\) 时,\(\frac {(m+1)n}{2}\) 不为整数,肯定无解。
-
当 \(2\nmid m\) 时
-
当 \(n = 3\) 时,第一排正序放。
这时我们 注意 到第 \(i\) 个数与第 \(i+1\) 个数的位置和之差为 \(1\),第 \(i\) 个数与第 \(i+2\) 个数的位置和之差为 \(2\)。
如何弥补这个 \(1\) 和 \(2\) 呢?
关键来了,我们 注意 到 \(i\) 与 \(i+1\) 的奇偶性不同,\(i\) 与 \(i+2\) 的奇偶性相同。考虑将奇数看作一组,偶数看作一组。
为了弥补 \(2\),我们 注意 到可以采取的一种方法是剩余两排都将奇数组和偶数组从大到小排放在连续的位置。
我们 注意 到奇数组的数的个数比偶数组的数的个数多 \(1\),奇数组中最大的数为偶数组中最大的数加 \(1\)。
这时候,我们 又惊奇地注意到 剩余两排中一排奇数组放在前面,另一排偶数组放在前面可以完美符合要求。
-
当 \(n > 3\) 时,都可以通过正序降序接替放归纳到 \(n=3\) 的情况。
-
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
void solve(){
cin >> n >> m;
if(n == 1){
if(m == 1) cout << 1 << '\n';
else cout << -1 << '\n';
return ;
}
if(m == 1){
for(int i = 1; i <= n; i++) cout << 1 << '\n';
return ;
}
if(n % 2 == 0){
for(int i = 1; i <= n; i += 2){
for(int j = 1; j <= m; j++) cout << j << ' ';
cout << '\n';
for(int j = m; j >= 1; j--) cout << j << ' ';
cout << '\n';
}
return ;
}
if(m % 2 == 0){
cout << -1 << '\n';
return ;
}
while(n > 3){
for(int j = 1; j <= m; j++) cout << j << ' ';
cout << '\n';
for(int j = m; j >= 1; j--) cout << j << ' ';
cout << '\n';
n -= 2;
}
for(int i = 1; i <= m; i++) cout << i << ' ';
cout << '\n';
for(int i = m; i >= 1; i -= 2) cout << i << ' ';
for(int i = m - 1; i >= 2; i -= 2) cout << i << ' ';
cout << '\n';
for(int i = m - 1; i >= 2; i -= 2) cout << i << ' ';
for(int i = m; i >= 1; i -= 2) cout << i << ' ';
cout << '\n';
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1; cin >> T;
while(T--) solve();
return 0;
}
P8957 「CGOI-3」巫泡弹弹乐
P7864 「EVOI-RD1」摘叶子
首先观察这题,我们能够发现存在一个叶子节点 \(x\) 的父节点的子节点数 \(\ge 2\) 时,先手必胜。
证明:
假设当前局面去掉 \(x\) 这个节点后必胜,那先手可以在第一步操作时顺带把节点 \(x\) 删除。
否则,先手可以在第一步操作只删除节点 \(x\)。
当不存在上面的情况时,我们发现树大概长这样:

设 \(f_x\) 表示节点 \(x\) 至少经过 \(f_x\) 条边抵达有至少两个子节点的祖先。
我们发现刚才我们讨论的情况属于存在叶子节点 \(x\),满足 \(f_x=1\)。
那接下来,我们只需要考虑 \(f_x\ge 2\) 的情况。
思考一下,对于所有的叶子节点 \(x\),若 \(f_x=2\),删除节点 \(x\),\(fa_x\) 成为新的叶子节点且 \(f_{fa_x}=1\),所以先手必败。
若 \(f_x\ge 3\),将所有 \(f_x=3\) 的叶子节点删除,可以回到 \(f_x=2\) 的情况。
以此类推,若所有的 \(f_x\) 都为偶数,则先手必败,否则先手必胜。
代码方面有些细节。
可以考虑令 \(0\) 节点为 \(1\) 节点的父亲,且 \(0\) 节点有 INF 个子节点。
若 \(siz_x\ge 2\),令 \(f_x=0\),否则令 \(f_x=f_{fa_x}+1\)。
我们还可以发现我们最开始讨论的 \(f_x=1\) 可以用是否存在 \(f_x\) 为奇数来判断。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
const int kN = 1e6 + 7;
int n, Fa[kN], f[kN], siz[kN];
vector<int> ve[kN];
void dfs(int x){
for(int v : ve[x]){
siz[x]++;
dfs(v);
}
return ;
}
void dfs2(int x){
if(siz[x] >= 2) f[x] = 0;
else f[x] = f[Fa[x]] + 1;
for(int v : ve[x]) dfs2(v);
return ;
}
void solve(){
cin >> n;
for(int i = 1; i <= n; i++) Fa[i] = siz[i] = 0, ve[i].clear();
for(int i = 2; i <= n; i++) cin >> Fa[i], ve[Fa[i]].push_back(i);
dfs(1);
dfs2(1);
for(int i = 1; i <= n; i++){
if(!siz[i] && (f[i] & 1)){
cout << 1 << '\n';
return ;
}
}
cout << 0 << '\n';
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1; cin >> T;
while(T--) solve();
return 0;
}
P16394 [ECUSTPC 2026 Spring] 回响形态
写个爆搜或者观察一下可以发现 \(k\) 的最小值在升序或降序排列时取到,\(k\) 的最大值在形如 \(1,n,\cdots\) 或 \(n,1,\cdots\) 这样的排列取到。容易发现对于一个合法的序列 \(a\),令 \(b_i\gets n-a_i+1\) 后,\(b\) 序列依然是一个合法序列,所以现在我们只需要求出一个合法序列,然后将它执行上面的操作即可。
先求出 \(k\) 的范围为 \([(n-1)\times \frac n 2,(n-1)^2]\)。
我们令 \(1\) 放在开头,此时可以包含最小值到最大值之间的所有 \(k\)。
假设最初的排列为升序排列,考虑增量,令 \(k\gets k-(n-1)\times \frac n 2\)。
对于 \([2,n]\) 的前缀,我们将任意元素 \(x\) 扔到 \(n\) 后面,发现答案增加 \(n-x\),等价于 \(k\gets k-(n-x)\)。这一部分 \(k\) 至多减少 \((n-1)\times \frac {n-2} 2\),发现完全包好了 \(k\) 的取值。
于是做完了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("star.in", "r", stdin);
freopen("star.out", "w", stdout);
}
#define int long long
using ll = long long;
const int kN = 1e5 + 7;
int n, a[kN], b[kN];
ll k;
set<int> P, Q;
void workA(ll k){
k -= (1ll * n * (n + 1) / 2 - 1);
for(int x = 2; x < n; x++) P.insert(x);
for(int x = 2; x <= n - 1; x++){
if(k >= n - x){
Q.insert(x);
P.erase(x);
k -= (n - x);
}
}
int m = 1;
while(!P.empty()){
a[++m] = *P.begin();
P.erase(*P.begin());
}
a[++m] = n;
while(!Q.empty()){
a[++m] = *prev(Q.end(), 1);
Q.erase(*prev(Q.end(), 1));
}
return ;
}
void workB(ll k){
k = n * (n - 1) / 2 - k;
for(int x = 1; x < n; x++) P.insert(x);
for(int x = n - 1; x >= 2; x--){
if(k >= x - 1){
Q.insert(x);
P.erase(x);
k -= (x - 1);
}
}
int m = 1;
while(!P.empty()){
b[++m] = *prev(P.end(), 1);
P.erase(*prev(P.end(), 1));
}
while(!Q.empty()){
b[++m] = *Q.begin();
Q.erase(*Q.begin());
}
return ;
}
void solve(){
cin >> n >> k;
if(k < 1ll * (n - 1) * n / 2 || k > 1ll * (n - 1) * (n - 1)){
cout << -1 << '\n';
return ;
}
a[1] = 1, b[1] = n;
workA(n + k - 1);
workB(1ll * n * (n - 1) - k);
bool fg = 1;
for(int i = 1; i <= n; i++){
if(a[i] != b[i]) fg = 0;
}
if(fg){
cout << -1 << '\n';
return ;
}
for(int i = 1; i <= n; i++) cout << a[i] << ' ';
cout << '\n';
for(int i = 1; i <= n; i++) cout << b[i] << ' ';
cout << '\n';
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1; cin >> T;
while(T--) solve();
return 0;
}

浙公网安备 33010602011771号