OI随笔
题目编号皆为洛谷编号。
P10378
2025-4-15
题面
有 \(n\) 个元素 \(m\) 次关联信息,每次提供哪两个元素关联信息,有关联信息的两个元素绝对不是同一个集合的,最多只有两个集合,问其中的一个集合最多有多少人,最少有多少人?
分析
可以明确的是,有关联的两个元素绝对不属于一个集合,那么我们就可以做出如下推理:
有三个元素 \(a,b,c\) 。
假设 \(a\) 与 \(b\) 有关联, \(b\) 与 \(c\) 有关联。
∵a∈A,b∈B
又∵c∉B
∴c∈A
就相当于一种传递性,那么我们就找到一类数之间的关联性。
就拿洛谷题面上的输入输出样例2来说吧。
由样例可知:
这个里面,1: 1 3 4
2: 2
3: 5 7
4: 6
各属于一个集合,我们还需要考虑:1 和 2 3 和 4 是绑定的,所以不能合并为一个大集合,其他的可以互相组合,那么就可以得到这几个结果:2 3 4 5
但是,我们只需要取最大或者最小的就可以了,答案就是2 5
。
经过这个推理,相信你也大概知道思路了:
- 先拼凑出一个对立集合
- 找出所有对立两个集合
- 找出最大和最小的答案
(其中,在执行 3 得过程时,我们可以用一些小贪心)
代码
下面就是呈现代码,其实这道题说到底就是 二分图 实在不懂可以看 OI-wiki。
主要难点在于对这种东西进行分类,确定出关系,也就是那个简单的推理,很简单,但是很关键。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> g[N];
bool vis[N];
int a, b, mn, mx;
void dfs(int u, int f) {
if (vis[u])
return;
vis[u] = 1;
f ? ++b : ++a;
for (size_t i = 0; i < g[u].size(); ++i)
dfs(g[u][i], !f);
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
g[x].push_back(y);
g[y].push_back(x);
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
a = b = 0;
dfs(i, 0);
mn += min(a, b);
mx += max(a, b);
}
}
printf("%d %d", mn, mx);
return 0;
}
总结
实际上这道题可以用二分图得连通块染色思想去做,但是笔者认为这个思路更好思考,有时候比一定可以去用模板思想,主要还是有一个开放性思维。(bushi
P10378
2025-4-20
题面
小杨要学习m种算法,每种算法需要达到至少k的掌握程度。现在有n道练习题,每道题对应一个算法a_i,学习这道题可以增加对应算法b_i的掌握度。题目有以下限制:
每道题只能学习一次
不能连续学习两道相同算法的题目(即不能连续选a_i相同的题)
要求找出最少需要学习多少道题才能让所有m种算法都达到k的掌握程度。如果无法实现这个目标,就输出-1。
分析
看到最值立刻想到贪心。
算法的题目按提升值从高到低排序,优先选择提升值大的题目,直到该算法的总提升值达到 k。这样一定是用了最少的题目数。
计算所有算法所需题数的总和 sum,找到一种排列顺序使得同一算法的题目不连续。这取决于最多题数的算法(设为 \(max_c\))与其他算法的题数之和(\(sum - max_c\))的关系:
若 \(max_c > sum - max_c + 1\),则无法避免连续,返回 -1。
否则,可以交替排列,返回 sum。
将题目按算法分组。
对每组题目按提升值降序排序,确保优先选择高提升值的题目。
每个算法,累加题目提升值直至达到 k就可以记录所需题数。若无法达到 k就直接返回 -1。
最后需要检测一下是否符合题目要求。
若 \(max_c > sum - max_c + 1\),说明无法避免连续,返回 -1;否则返回 sum。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int m, n, k, sum;
struct node {
int a, b;
} a[N], b[N];
bool cmp(node a1, node a2) {
return a1.a == a2.a ? a1.b > a2.b : a1.a < a2.a;
}
bool p;
int main() {
scanf("%d%d%d", &m, &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].a);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].b);
sort(a + 1, a + n + 1, cmp);
for (int i = 1; i <= n; i++) {
if (a[i].a != a[i - 1].a)
p = 0;
if (!p)
b[a[i].a].a += a[i].b, b[a[i].a].b++;
if (b[a[i].a].a >= k)
p = 1;
}
for (int i = 1; i <= m; i++) {
if (b[i].a < k) {
printf("-1");
return 0;
}
sum += b[i].b;
}
for (int i = 1; i <= m; i++)
if (b[i].b > sum - b[i].b + 1) {
printf("-1");
return 0;
}
printf("%d", sum);
return 0;
}
总结
辨别出贪心的策略是贪心的核心,如果一道题根本不是贪心我们还试图用这种方法去做那么只会前功尽弃,就例如动态规划。
P10378
2025-4-20
题面
有一棵树,有三种情况(看网址),计算N次之后的节点位置。
分析
这个题的关键是要把路径表示成栈的形式,这样不管怎么移动都不用担心数值过大。比如说初始节点s很大,但是可以拆解成二进制路径的形式。比如s是2的话,对应的路径是左,用0表示。这样每次操作U就是回退一步,L或R就是往左或右走一步。
那具体怎么拆呢,比如s等于5的时候,5的二进制是101,拆的时候每次除以2,得到余数1和0,然后反过顺序,得到路径是左然后右,对应的栈是0和1。这样处理操作的时候,比如L操作就压入0,R压入1,U弹出。
处理完所有操作之后,再根据栈里的路径重新计算节点编号。比如栈是0和1的话,根节点1先左到2,再右到5。这样计算的时候,每一步都是当前值乘2加上余数,这样就能得到最终的节点编号。
比如操作URR的话,初始栈是0,U操作之后栈空了,然后R两次压入1和1,得到栈是1和1。计算的时候1*2+1=3,再乘2加1得到7。这样就正确了。
要注意的是拆解初始节点的时候要反转余数列表,这样才能得到正确的路径顺序。比如5拆解得到余数是1和0,反转之后是0和1,对应左和右。这样栈的顺序才是正确的。
所以整个思路就是通过栈来维护当前路径,避免处理大数,从而高效解决问题。
代码
#include<bits/stdc++.h>
using namespace std;
long long n,t,n1;
char a;
char s[1000005];
signed main(){
scanf("%lld%lld",&n,&t);
for(int i=1;i<=n;i++)
{
cin>>a;
if(a=='U'){
if(s[n1]!='U'&&n1!=0){
s[n1]='l',n1--;
}
else s[++n1]=a;
}
else s[++n1]=a;
}
for(int i=1;i<=n1;i++)
{
if(s[i]=='U')if(t>1) t=(long long)t/2;
if(s[i]=='L') t=(long long)t*2;
if(s[i]=='R') t=(long long)t*2+1;
}
cout<<t;
return 0;
}
总结
大模拟主要靠的是细心,还是细心。