Codeforces Round 892 (Div. 2)

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++一秒钟大约能跑4108次,本题数据范围到500,O(n3logn)理论上是能过的(当然会被卡常),
  • 为了防止痛苦,我们可以学习上面代码用并查集来维护当前情况排列的连通性,把复杂度降到O(n3)
    首先值得学习的是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(n3logn)

算法描述:依然是枚举最大值,这次枚举从选哪个数和放在哪个位置入手,确定好后维护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(n2)

  • 根据给的样例猜想最佳排列是后段数进行反转,利用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; }

__EOF__

本文作者爱飞鱼
本文链接https://www.cnblogs.com/mathiter/p/17627632.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   potential-star  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示