Codeforces Round 892 (Div. 2)
title: Codeforces Round 892 (Div. 2)
categories:
- ICPC
tags:
- null
abbrlink: a38ff4fe
date: 2024-08-09 00:00:00
c题jls的代码,拿过来仔细研究了一番,终于弄明白了。
https://codeforces.com/contest/1859/problem/C
jls代码
#include <bits/stdc++.h>
using i64 = long long;
struct DSU {
std::vector<int> f, siz;
DSU() {}
DSU(int n) {
init(n);
}
void init(int n) {
f.resize(n);
std::iota(f.begin(), f.end(), 0);
siz.assign(n, 1);
}
int find(int x) {
while (x != f[x]) {
x = f[x] = f[f[x]];
}
return x;
}
bool same(int x, int y) {
return find(x) == find(y);
}
bool merge(int x, int y) {
x = find(x);
y = find(y);
if (x == y) {
return false;
}
siz[x] += siz[y];
f[y] = x;
return true;
}
int size(int x) {
return siz[find(x)];
}
};
void solve() {
int n;
std::cin >> n;
int ans = 0;
for (int l = n * n; ; l--) {
DSU dsu(n + 1);
int sum = 0;
bool ok = true;
for (int i = n; i >= 1; i--) {
int x = dsu.find(std::min(n, l / i));
// std::cerr<<l<<" "<<i<<" "<<x<<'\n';
// std::cerr<<dsu.f[x]<<"\n";
if (x == 0) {
ok = false;
break;
}
sum += i * x;
dsu.merge(x - 1, x);
//std::cerr<<dsu.f[x]<<"\n";
}
if (!ok) {
break;
}
ans = std::max(ans, sum - l);
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
- c++一秒钟大约能跑$4 * 10 ^ 8$次,本题数据范围到500,$O(n^3logn)$理论上是能过的(
当然会被卡常),
- 为了防止痛苦,我们可以学习上面代码用并查集来维护当前情况排列的连通性,把复杂度降到$O(n^3)$。
首先值得学习的是DSU的板子,其次是这道题是如何用dsu来维护的,为什么能用dsu维护,这可以使我们对dsu的理解加深,
#算法综述:本题常规解法是枚举可能的最大成本值,然后枚举每个位置的取值,根据cf题解的证明,每个位置的成本都应该尽可能接近最大值,但这就产生了竞争,但可以通过反证法证明越靠后的位置应该优先选数,因为优先选才能选到大数,这样可以使成本最高,所以我们倒序遍历每个位置。
- 先对上面这段话举个例子,比如现在到第3个数选了,由于其位置靠前,本来可以选很多大数,但是由于它在后面选,所以它只能选剩下的数中较大的数。
继续算法分析:在遍历每个位置时侯,我们先看如果没有任何限制这个位置最大能取什么数,这个数记作x,当然肯定要min(n,x),如果这个数已经被用过了,那么就去找次大的数,以此类推,直到发现这个位置能取的数都已经被抢完了,那说明当前这个最大值无法形成最优排列,我们直接break。如果为这个位置找到了当前视角下的最优数,我们将其加入排列,维护sum和排列。
- 上述算法描述如何实现呢?这个实现方式是关键,
容易想到的用set维护,维护当前哪些数能用,并且set是有序容器,且可以二分,这些将在下一份代码中作进一步展开。
完全想不到用并查集来维护,维护方式如下;
首先是dsu初始化已经完成,直接x=find(min(n, mx/ i)),只要x!=0,就说明能为当前这个位置找到数,然后merge合并操作将x的父节点指向x-1。
为什么将x的父节点指向x-1?
- 我们需要让后序的位置找到他们的最优数,比如后面也有一个位置想要x,但我们知道x已经用过了,但通过find我们可以找到x-1,x-1是次优数,后续同理,可以不断向前找。
为什么x==0就这个位置说明找不到了呢?
- 由于是逆序枚举位置,find函数传入的参数肯定是越来越大,而x一定是不断减1,所以之前的父节点又成为别人的子节点,而由于find函数是经过路径压缩的,所以一个连续用过的序列所有点都一定是指向其中的最小值的,当1也被用完,merge成为0的子节点时,比如此时1,2,3已经用过,而目前位置下一个数只能取<=3的数,一次find操作后会让1,2,3都指向0,所以当发现x==0的时候就说明当前这个排列无法构建了!
补问:为什么一旦x==0后续所有排列都不需要在考虑?
#代码2 用set实现
#时间复杂度:,$O(n^3logn)$
#算法描述:依然是枚举最大值,这次枚举从选哪个数和放在哪个位置入手,确定好后维护set,学习set的erase的接受多种参数
https://www.cainiaojc.com/cpp/cpp-set-erase-function.html(菜鸟教程)
这个代码算法也是逆序枚举位置,对于当前位置 int x =min(n,mx/j) ,对于x进行二分upper_bound,找到第一个比x大的数后,用prev函数返回上一个位置的迭代器(无关补充:next是返回下一个位置的迭代器)。prev指向的数就是这个位置的最优数,找到后就维护sum和集合s。如果二分找不到答案,比如s的所有数都比x大,那这个排列就不能要了,直接break。另一方面,如果s的所有数都比x小,则upper_bound返回s.end(),prev后得到当前集合的最大数(set是有序容器),把这个最大数安排给这个位置。
卡常现状:
开longlong会直接TLe,胡乱宏定义的弊端!
#include <bits/stdc++.h>
using namespace std;
#define ll long long
//# define int long long
#define ull unsigned long long
#define pii pair<int,int>
#define double long double
#define baoliu(x, y) cout << fixed << setprecision(y) << x
#define endl "\n"
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const double eps = 1e-8;
int n, m;
int a[N];
void solve() {
int n;
cin >> n;
int ans = 0;
for (int i = n; i >= 1; i --) {
for (int pos = 1; pos <= n; pos ++) {
int mx = pos * i;
set<int> s;
for (int j = 1; j <= n; j ++) s.insert(j);
s.erase(i);
int res = 0;
bool ok = true;
for (int j = n; j >= 1; j --) {
if (j == pos) continue;
int x =min(n,mx/j) ;
auto it = s.upper_bound(x);
if (it == s.begin()) {
ok = false;
break;
}
it = std::prev(it);
res += *it * j;
s.erase(it);
}
if (ok) ans = max(ans, res);
}
}
cout << ans << "\n";
}
signed main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int t;
cin>>t;
while (t--) {
solve();
}
return 0;
}
代码3:打表
时间复杂度:$O(n^2)$
- 根据给的样例猜想最佳排列是后段数进行反转,利用reverse函数实现
#include <bits/stdc++.h>
using namespace std;
//#define ll long long;
# define int long long
#define ull unsigned long long
#define pii pair<int,int>
#define double long double
#define baoliu(x, y) cout << fixed << setprecision(y) << x
#define endl "\n"
const int N =510;
const int M = 1e6 + 10;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const double eps = 1e-8;
int n, m;
int a[N];
vector<int>v;
void solve(){
v.clear();
cin>>n;
v.push_back(0);
for(int i=1;i<=n;i++){
v.push_back(i);
}
int ans=0;
for(int j=1;j<=n;j++){
int s=0;int res=0;
vector<int>b(v);
reverse(b.begin()+j,b.end());
for(int i=1;i<=n;i++){
res=max(res,i*b[i]);
s+=b[i]*i;
}
s-=res;
ans=max(ans,s);
}
cout<<ans<<endl;
}
signed main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int t;
cin>>t;
while (t--) {
solve();
}
return 0;
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 爱飞鱼的blog!