CF 2130
C:
题目分析:
首先可以将每一个点对抽象为数轴上的一个覆盖线段
f(S'): 集合 \(S'\) 中所有边在数轴上覆盖的点数之和,注意覆盖区间左闭右开
g(S'): 集合 \(S'\) 中包含若干环,这些环上的总点数之和
解题思路:
经过观察可以发现,如果选了一个环,那么其中的边必定首尾相接,且总共覆盖了一段完整的区间
那么总存在着一些边删掉不会影响\(f(S')\)结果
然后你就发现最优情况下的\(g(S')\)一定为0
那么我们求最大的\(f(S')\)即可
如何求最大的\(f(S')\)呢?
我们发现,我们需要在已知点对抽象出来的线段中选择一些线段,以覆盖整个区间尽可能多的点
wow!你见过这么典的题吗?
首先,为了防止成环,我们要避免选一些没用的边
没用的边指的是一条边的起点和终点都被包含到另一条边里,形式化的说,就是对于边\(i\),\(j\),如果\(a_i <= a_j\),\(b_i >= b_j\),则称边\(j\)为无用的边。
那么我们可以对边的起点进行排序,选择合适的边
具体的,我们要记录当前覆盖到的区间,并据此选择某一条边选不选。
因为边已经按照起点排序完毕,所以我们只需判断当前遍历到的边的终点是否包含在已覆盖区间里,如果是的话不选
对于终点不包含在已覆盖区间里的边,有两种情况:
- 这条边的起点包含在已覆盖区间里
对于这种情况,我们只需要更新当前区间的右端点即可 - 这条边的起点不包含在已覆盖区间里
对于这种情况我们需要更新当前区间的左右端点为当前线段的左右端点
然后就做完辣。😉
其实我觉得区间左端点可以不记录,但是本蒟一开始以为他要求 \(f(S')\) - \(g(S')\) 的值
参考代码:
是本蒟的赛时代码呐!
#include<bits/stdc++.h>
using namespace std;
int t,n;
const int N=3e3+33;
struct edge
{
int a,b;//start,end
int id,vis;
bool operator<(const edge&wow) const
{
return (a==wow.a) ? b>wow.b : a<wow.a;
}
}e[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--)
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>e[i].a>>e[i].b;
e[i].id=i;e[i].vis=0;
}
sort(e+1,e+n+1);
int he,ta,sum;//写个贪心,别挂哦
he=e[1].a,ta=e[1].b,sum=0;//666你统计了个寂寞
int cnt=1;
e[1].vis=1;
for(int i=2;i<=n;i++)
{
if(e[i].a>ta)
{
sum+=ta-he;
he=e[i].a;
ta=e[i].b;
cnt++;
e[i].vis=1;
}
else
{
if(e[i].b<=ta)
{
continue;
}
else
{
ta=e[i].b;
cnt++;
e[i].vis=1;
}
}
}
sum+=ta-he;
//cout<<sum<<"\n";
cout<<cnt<<'\n';
for(int i=1;i<=n;i++)
{
if(e[i].vis==1)
{
cout<<e[i].id<<' ';
}
}
cout<<'\n';
}
return 0;
}
tag:贪心
D:
前言:因为\(Codeforces\) \(Better\) 插件对于这道题的翻译过分人性化,所以我们今天特邀隆重嘉宾 \(Mr.Deepseek\)来为我们做一个题目翻译
题目翻译:
题目描述
给定一个长度为 \(n\) 的排列 \(p_1, p_2, \ldots, p_n\)。
你需要构造一个数组 \(a_1, a_2, \ldots, a_n\),构造规则如下:
- 对于每个 \(1 \leq i \leq n\),选择 \(a_i = p_i\) 或 \(a_i = 2n - p_i\)。
求数组 \(a\) 中可能的最小逆序对数。
排列的定义:长度为 \(n\) 的排列是一个由 \(1\) 到 \(n\) 的 \(n\) 个不同整数按任意顺序组成的数组。例如,\([2, 3, 1, 5, 4]\) 是一个排列,但 \([1, 2, 2]\) 不是(数字 2 重复),\([1, 3, 4]\) 也不是(\(n=3\) 但出现 4)。
逆序对的定义:数组 \(a\) 中的逆序对是指一组下标 \((i, j)\) 满足 \(1 \leq i < j \leq n\) 且 \(a_i > a_j\)。
输入格式
每个测试点包含多组测试数据。第一行输入一个整数 \(t\)(\(1 \leq t \leq 10^3\)),表示测试数据的组数。
每组测试数据:
- 第一行:一个整数 \(n\)(\(2 \leq n \leq 5 \cdot 10^3\))。
- 第二行:\(n\) 个整数 \(p_1, p_2, \ldots, p_n\)(保证是排列)。
保证所有测试数据的 \(n\) 之和不超过 \(5 \cdot 10^3\)。
输出格式
对于每组测试数据,输出一个整数,表示数组 \(a\) 的最小逆序对数。
样例
输入样例:
2
3
1 2 3
4
4 3 2 1
输出样例:
0
6
提示
- 样例1:选择 \(a_i = p_i\) 时,\(a = [1, 2, 3]\),逆序对数为 0。
- 样例2:无论怎么选择,最小逆序对数均为 6。
题目分析
对于每一个值,你可以选择维持原样或者让它变大,而明显的,这里的变大存在性质:
- 若存在两个数 \(p_i\) 和 \(p_j\),\(p_i>p_j\),那么变大后的值一定有 \(a_i<a_j\)(这不废话吗)。
题目里的数据范围:\(n∈[1,5e3]\),我们发现这是 \(O(n^2)\) 可以接受的。联想一下最基本的求逆序对方法,你可以发现暴力求逆序对也是 \(O(n^2)\) 的,所以甚至不用什么优化。
解题思路
这里提供两种思维方法,大概就是由整体到局部或者局部到整体的区别。
方法一是题解以及我花了大把时间研究的方法,方法二是队里奶龙 FQR_ 的思路,十分容易理解(storz)。
方法一:
首先你可以发现,对于任一个 \(i\),你有两种决策,第一种为使他变大(下文称为翻转),第二种为保持不变
首先讨论对 \(i\) 进行第一种操作:
对于任意满足 \(j>i\),\(p_j>p_i\),无论 \(a_j\) 是否翻转,操作后都满足 \(a_i>a_j\),即构成逆序对。
那么同理对 \(p_i\) 进行第二种操作满足:
对于任意满足 \(j<i\),\(p_j>p_i\),无论 \(p_j\) 是否翻转,操作后都满足 \(a_j>a_i\),即构成逆序对。
总结,当 \(i\) 取翻转值的时候,我们记 \(i\) 的贡献为在 \(i\) 右侧的所有原值大于 \(p_i\) 的数的数量,当 \(i\) 取原值的时候,我们记 \(i\) 的贡献为在 \(i\) 左侧的所有原值大于 \(p_i\) 的数的数量。
欸?怎么都是大于?有意思。
那么我们的解题切入点便在于此。
我们已经发现了最容易求的那些逆序对的求法,那么很自然的,下一步要证明这两种逆序对是否能不重不漏的覆盖掉所有的逆序对。
你会发现不管怎么取,对于任意一对 \(i,j\) 逆序对,都只会被统计一次,因为当 \(p_i>p_j\) 时逆序对在 \(j\) 被统计,\(p_i<p_j\) 时逆序对在 \(i\) 被统计,不存在重复的情况,又因为原数组为排列,所以不存在上述两种之外的情况,不重不漏,因此我们可以证明这种做法的正确性。
方法二:
观察发现,对于每一对数 \(i\),\(j\),若 \(p_i>p_j\),那么 \(i,j\) 是否为逆序对仅取决于 \(j\) 是否翻转(这里略同方法一)。
同理可得,对于每一对数 \(i\),\(j\),若 \(p_i<p_j\),那么 \(i,j\) 是否为逆序对仅取决于 \(i\) 是否翻转。
那么我们在遍历每一对数的过程中,只要判断 \(i<j\) 时 \(p_i\) 与 \(p_j\) 的大小关系,并据此将翻转贡献或者不翻转贡献加到对应下标上就能得出正确答案。
具体的,如果 \(p_i>p_j\),那么 \(j\) 不翻转的贡献自增。如果 \(p_i<p_j\),那么 \(i\) 翻转的贡献自增。
证明不重不漏:因为不存在任意 \(i\) 的值的改变带来的结果变化未被计入翻转贡献中,所以不漏;因为对于每一对逆序对都进行了仅一次遍历来计算贡献,所以不重。
两种方法的异同:
第一种方法重点在于求对 \(i\) 不同决策带来的全部贡献,先从总体的逆序对入手,再证明包含每一种逆序对即正确性。
第二种方法从每一对逆序对出发,分别将贡献加到对应下标上。
参考代码
方法一:
#include <bits/stdc++.h>
using namespace std;
void solve(){
int n, ans = 0;
cin >> n;
vector<int> p(n);
for(int i=0; i<n; i++) cin >> p[i];
for(int i=0; i<n; i++){
int left = 0, right = 0;
for(int j=0; j<i; j++) if(p[j] > p[i]) left++;
for(int j=i+1; j<n; j++) if(p[j] > p[i]) right++;
ans += min(left, right);
}
cout << ans << '\n';
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while(t--) solve();
}
方法二:
#include<bits/stdc++.h>
using namespace std;
int t,n;
const int N=5e3+10;
int chs1[N],chs0[N];
int p[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--)
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>p[i];
}
memset(chs1,0,sizeof(chs1));
memset(chs0,0,sizeof(chs0));
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(p[i]<p[j])
{
chs1[i]++;
}
else
{
chs0[j]++;
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans+=min(chs1[i],chs0[i]);
}
cout<<ans<<'\n';
}
return 0;
}//stooooooooo FQR ooooooooorz