2021牛客暑期多校训练营6 部分题解
C.Delete Edges
-
题意
给定一个包含 n n n个顶点的无向完全图,你可以选择三个顶点 x , y , z x,y,z x,y,z,如果 ( x , y ) , ( y , z ) , ( x , z ) (x,y),(y,z),(x,z) (x,y),(y,z),(x,z)这三条边没有被删除,你可以删除这三条边。你需要找到一种删除方案使得最后剩余边数 ≤ n \leq n ≤n。 -
解题思路
可以确定的是,如果我们选择 ( x , y , z ) (x,y,z) (x,y,z)删除,那么之后删除就不能是 ( x , y , z ) (x,y,z) (x,y,z)中的任意两个,因为它们之间没有边直接相连。
所以,我们贪心的去想,怎么去避免没有重复出现的两两组合数,即可以保证 x < y < z x<y<z x<y<z,同时维护 x + y + z = 0 ( m o d n ) x+y+z =0(\mod n) x+y+z=0(modn),那么我们就可以枚举 x , y x,y x,y来确定 z z z并记录操作即可。 -
AC代码
/**
*@filename:C
*@author: pursuit
*@created: 2021-08-07 16:19
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 4e6 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int n,m;
struct node{
int x,y,z;
};
node ans[N];
void solve(){
int tot = 0;
for(int i = 1; i <= n; ++ i){
for(int j = i + 1; j <= n; ++ j){
int k = n - i - j;
if(k <= 0)k += n;
if(k > j){
ans[++ tot] = {i,j,k};
}
}
}
printf("%d\n", tot);
for(int i = 1; i <= tot; ++ i){
printf("%d %d %d\n", ans[i].x, ans[i].y, ans[i].z);
}
}
int main(){
scanf("%d", &n);
solve();
return 0;
}
F.Hamburger Steak
-
题意
有 n n n个汉堡排和 m m m个锅,给出每个汉堡排需要煎的时间 t i t_i ti。一个汉堡排可以在一口锅中煎好,也可以分成两次在两个锅中煎好。一个锅同时只能煎一个汉堡排,一个汉堡排同时只能放到一个锅中。求一个方案使煎好所有的汉堡排所需要的时间最少(尽早煎完)。 -
解题思路
首先,根据题意,我们可以确定煎汉堡排的最小时间 T = m a x ( ⌈ ∑ i = 1 n t i ) m ⌉ , m a x ( t i ) ) T=max(\lceil\frac{\sum_{i=1}^nt_i)}{m}\rceil, max(t_i)) T=max(⌈m∑i=1nti)⌉,max(ti))。所以我们可以按平均时间将汉堡排贪心的分配给锅。具体操作为利用优先队列存储每个汉堡排的编号和结束时间,然后遍历 m m m个锅,充分利用完平均时间即可。 -
AC代码
/**
*@filename:F
*@author: pursuit
*@created: 2021-08-06 21:27
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl;
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 1e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
//m个平底锅,n个汉堡牛排,第i个汉堡需要t_i时间。其可以在一个锅中煎t_i分钟或者在两个不同的锅中煎a_i和b_i分钟,其中a_i+b_i=t_i。
//其中一个平底锅最多同时煎一个,一个汉堡最多同时放在一个锅中。
//最小化煎的时间。
//输出n行,其中每行第一个整数k,代表在几个锅中煎。其中k个元祖为平底锅编号id,开始时间和结束时间,即[l,r]。
int n,m;
struct node{
int id;
ll l,r;
};
vector<node> plans[N];
priority_queue<pii> q;//存储结束时间和汉堡排的编号。
ll T;
void solve(){
T = max((T + m - 1) / m,1LL * q.top().first);//获取最小耗时T.
ll time = 0;//当前时刻。
int id = 1;//平底锅编号。
while(!q.empty()){
int t = q.top().first, i = q.top().second;
q.pop();
if(time + t <= T){
//说明时间足够。
plans[i].push_back({id,time,time + t});
time += t;
}
else{
//说明时间不够,超出了,我们需要用两个锅分配。
plans[i].push_back({id,time,T});
t -= (T - time);//获取还需要操作的时间。
id ++;
time = t;
plans[i].push_back({id,0,time});
}
if(time == T){
time = 0;
id ++;
}
}
for(int i = 1; i <= n; ++ i){
if(plans[i].size() == 1){
printf("1 %d %lld %lld\n", plans[i][0].id, plans[i][0].l,plans[i][0].r);
}
else{
printf("2 %d %lld %lld %d %lld %lld\n", plans[i][1].id, plans[i][1].l,plans[i][1].r, plans[i][0].id, plans[i][0].l,plans[i][0].r);
}
}
}
int main(){
scanf("%d%d", &n, &m);
int x;
for(int i = 1; i <= n; ++ i){
scanf("%lld", &x);
q.push({x,i});//煎所需的时间。
T += x;
}
solve();
return 0;
}
H.Hopping Rabbit
-
题意
平面上有 n n n 个矩形,给定 d d d,需要找到一个位置 ( x , y ) (x,y) (x,y),使得所有 ( x + k d , y + k d ) (x+kd,y+kd) (x+kd,y+kd) 均不落在矩形中。 -
解题思路
由于我们能到达的位置是周期重复的,所以实际上我们可以将所有的矩形移动到 ( 0 , 0 ) (0,0) (0,0)和 ( d , d ) (d,d) (d,d)的范围内,然后在里面找一个没有被矩形覆盖的点即可,这样找到的这个点一定是不落在矩形上的。
那么考虑矩形平移,矩形可能会被分成很多个部分,我们需要对其左上角和右上角坐标对 d d d取模然后得到应该在的位置,根据这个位置确定分成多少个矩形。
处理完这些矩形之后,就可以利用扫描线维护这些矩形求答案了。
/**
*@filename:H
*@author: pursuit
*@created: 2021-08-07 13:48
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl;
#define x first
#define y second
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 2e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
//用左下角(x_1,y_1)和右上角(x_2,y_2)来描述一个陷阱矩阵。
int n,d;//n个陷阱,d为跳跃的长度。
struct Line{
int yl,yr;//左右端点。
int d;//是入边还是出边。
Line(){};
Line(int yl,int yr,int d) : yl(yl),yr(yr),d(d){};
};
vector<Line> scan[N];//扫描线。scan[i]代表扫描线高度为i的集合。利用桶排序。
struct node{
int l,r,minn,lazy;
}tree[N << 2];
void buildTree(int rt,int l,int r){
//cout << rt << " " << l << " " << r << endl;
tree[rt] = {l,r,0,0};
if(l == r){
return;//说明到了叶子结点。
}
int mid = l + r >> 1;
buildTree(rt << 1,l,mid);
buildTree(rt << 1 | 1,mid + 1,r);
}
void pushup(int rt){
tree[rt].minn = min(tree[rt << 1].minn,tree[rt << 1 | 1].minn);
}
void pushdown(int rt){
if(tree[rt].lazy){
int &lz = tree[rt].lazy;
tree[rt << 1].minn += lz;
tree[rt << 1].lazy += lz;
tree[rt << 1 | 1].minn += lz;
tree[rt << 1 | 1].lazy += lz;
lz = 0;
}
}
void update(int rt,int l,int r,int d){
if(l <= tree[rt].l && r >= tree[rt].r){
//整个区间都被覆盖了。
tree[rt].minn += d;
tree[rt].lazy += d;
return;
}
pushdown(rt);
int mid = (tree[rt].l + tree[rt].r) >> 1;
if(l <= mid){
update(rt << 1,l,r,d);
}
if(r > mid){
update(rt << 1 | 1,l,r,d);
}
pushup(rt);
}
int query(int rt,int l,int r){
if(l <= tree[rt].l && r >= tree[rt].r){
return tree[rt].minn;
}
pushdown(rt);
int mid = tree[rt].l + tree[rt].r >> 1;
int res = INF;
if(l <= mid){
res = min(res,query(rt << 1,l,r));
}
if(r > mid){
res = min(res,query(rt << 1 | 1,l,r));
}
return res;
}
void mod(pii &a){
a.x = (a.x % d + d) % d, a.y = (a.y % d + d) % d;
}
void add(int x1,int x2,int y1,int y2){
//cout << x1 << " " << x2 << " " << y1 << " " << y2 << endl;
scan[x1].push_back(Line(y1,y2,1));//入边。
scan[x2 + 1].push_back(Line(y1,y2,-1));//出边。
}
void push(pii a,pii b){
if(a.x <= b.x){
if(a.y <= b.y){
//说明只能分成一个矩形。
add(a.x,b.x,a.y,b.y);
}
else{
add(a.x,b.x,a.y,d - 1),add(a.x,b.x,0,b.y);
}
}
else{
//a.x > b.x;
if(a.y <= b.y){
add(0,b.x,a.y,b.y),add(a.x,d - 1,a.y,b.y);
}
else{
//a.y > b.y;
add(0,b.x,a.y,d - 1),add(0,b.x,0,b.y);
add(a.x,d - 1,a.y,d - 1),add(a.x,d - 1,0,b.y);
}
}
}
void solve(){
pii ans = pii(-1,-1);
buildTree(1,0,d - 1);
for(int i = 0; i < d; ++ i){
if(ans.x != - 1)break;
for(auto &l : scan[i]){
update(1,l.yl,l.yr,l.d);
}
if(query(1,0,d - 1) == 0){
//判断是否存在没有被覆盖的位置。
for(int j = 0; j < d; ++ j){
if(query(1,j,j) == 0){
ans.x = i,ans.y = j;
break;
}
}
}
}
if(ans.x != - 1){
puts("YES");
printf("%d %d\n", ans.x, ans.y);
}
else{
puts("NO");
}
}
int main(){
scanf("%d%d", &n, &d);
pii a,b;
for(int i = 1; i <= n; ++ i){
scanf("%d%d%d%d", &a.x, &a.y, &b.x, &b.y);
b.x --,b.y --;
if(b.x - a.x + 1 >= d){
b.x = d - 1,a.x = 0;
}
if(b.y - a.y + 1 >= d){
b.y = d - 1,a.y = 0;
}
mod(a),mod(b);
push(a,b);
}
solve();
return 0;
}
I.Intervals on the Ring
-
题意
给出环上的一组区间,你需要构造环上的一组区间使得这些区间的交是给定的区间的并。 -
解题思路
由于给出的间隔都是不相交的。所以我们能保证相邻两个区间的左右端点合在一起能覆盖整个区间,所以 [ l 1 , r 2 ] , [ l 2 , r 1 ] [l_1,r_2],[l_2,r_1] [l1,r2],[l2,r1]这个构建的区间交即是这两个区间的并。例如 n = 5 , [ 1 , 2 ] , [ 3 , 4 ] n=5,[1,2],[3,4] n=5,[1,2],[3,4]这个区间,我们则会选取 [ 1 , 4 ] , [ 3 , 2 ] [1,4],[3,2] [1,4],[3,2]。 -
AC代码
/**
*@filename:I
*@author: pursuit
*@created: 2021-08-06 19:37
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl;
#define l first
#define r second
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 1e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;
//若l <= r,则区间表示为[l,r]
//若l > r,则区间表示为[l,l + 1...n,1...r];
int t,n,m;
int l,r;
void solve(){
vector<pii> interval(m);
for(int i = 0; i < m; ++ i){
scanf("%d%d", &l, &r);
interval[i] = {l,r};
}
printf("%d\n", m);
sort(interval.begin(), interval.end());
for(int i = 0; i < m; ++ i){
printf("%d %d\n", interval[i].l, interval[(i + m - 1) % m].r);
//cout << (i + m - 1) % m << endl;
}
}
int main(){
scanf("%d", &t);
while(t -- ){
scanf("%d%d", &n, &m);
solve();
}
return 0;
}
J.Defend Your Country
-
题意
A国城市的地图可以看成一个简单的无向连通图,由n个城市和m条道路组成。B国想摧毁一些道路,A国可以决定提前关闭一些道路。
每个城市都有一个重量级 a i a_i ai。如果连通块中城市数量为奇数,可以重要总水平和为总和的负数,否则为正数。你需要使得总重量级最大。 -
解题思路
根据题意,我们自然是想保证每个连通块的城市数量均为偶数,而一开始保证连通,所以如果 n n n是偶数,那么一定满足条件。我们考虑 n n n为奇数的时候,此时我们一定要删除点来使得剩余的连通块中城市数量为偶数,那么对于非割点,我们删除不会影响图的连通性,所以易得删除非割点只需要选取最小的即可,那么考虑割点,如果删除割点,那么一定要保证剩余的连通块城市数量为偶数,因为如果不保证就会导致还要继续删除以删除更多的点。
综上实际上我们都是只要删除一个点即可,我们用tarjan算法找割点,同时用siz数组记录子树的节点总数。 -
AC代码
/**
*@filename:J
*@author: pursuit
*@created: 2021-09-26 19:19
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const int N = 1e6 + 10;
const int P = 1e9 + 7;
const int INF = 2e9;
//删去非割点显然只需要删除一个,所以考虑删掉割点的情况取最优即可。
//我们删除割点的时候一定不会删除掉非割点。
int t, n, m, a[N];
struct node{
int to, next;
}edges[N << 1];
int head[N], tot;
void add(int u, int v){
edges[++ tot].next = head[u], edges[tot].to = v, head[u] = tot;
}
int idx, low[N], dfn[N];
int siz[N];//siz[i]表示以i为结点的子树结点数总和。
bool cut[N], tag[N];//cut[i]表示i结点是否为割点, tag[i]表示i这个割点割去之后剩下的连通块城市数量是否为偶数。
ll res;
void init(){
tot = idx = 0;
res = 0;
for(int i = 1; i <= n; ++ i)head[i] = low[i] = dfn[i] = siz[i] = 0, cut[i] = tag[i] = false;
}
void tarjan(int u, int fu){
dfn[u] = low[u] = ++ idx;
int v, son = 0;
siz[u] = 1;
for(int i = head[u]; i; i = edges[i].next){
v = edges[i].to;
if(v == fu)continue;
if(!dfn[v]){
++ son;
tarjan(v, u);
siz[u] += siz[v];
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){
if(fu)cut[u] = true;//为割点。
if(siz[v] & 1){
//说明子树结点为奇数。
tag[u] = true;
}
}
}
else{
low[u] = min(low[u], dfn[v]);
}
}
if(!fu && son > 1)cut[u] = true;
}
void solve(){
if(n % 2 == 0){
printf("%lld\n", res);
init();
return;
}
tarjan(1, 0);
int minn = INF;
for(int i = 1; i <= n; ++ i){
if(!cut[i] || !tag[i]){
//cout << a[i] << " ";
minn = min(minn, a[i]);
}
}
//cout << endl;
printf("%lld\n", res - 2 * minn);
init();
}
int main(){
scanf("%d", &t);
while(t -- ){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
res += a[i];
}
for(int i = 1, u, v; i <= m; ++ i){
scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
solve();
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)