题目
https://codeforces.com/contest/1661/problem/F
给你n个线段
问最少切多少次,让切割后所有线段长度平方和不大于m
范围
n<=1e5
线段长度和 <= 1e9
m <= 1e18
7s
512MB
题解
尝试
对于最外层的答案, 显然二分答案
那么问题变成了 如果指定切割k次, 能否满足条件
贪心: 从小到大, 判断剩余期望与已切割, 显然如果 当前 乘上段数 不大于剩余值, 那么不需要切割, 否则任意不合法必定存在比当前段更大的值, 要切也是切更大的
一定不切割的状态 能证明了, 但是不是这种状态时,可能切割也可能不切割, 即使切割, 怎么计算次数也不知道
https://codeforces.com/contest/1661/submission/153691360
例如两个线段 3,4
, 要结果小于17, 最好的办法是均分4, 而这种没有对应的贪心规则, 上述方法不能判断
另一个正确但是会超时的思路是
我们如果知道一个具体的段,要切割成多少份, 那么显然可以数学O(1)就算出这一段切割的最小值,(切割出来的尽量相等)
那么一个段 从 k次变成k+1次 它带来的变化量也是 上述计算相减,也是O(1)的
那么 我们直接维护优先队列 (多切割一次代价,线段编号,当前切割次数)
, 这样每次取最大的切一下,放回去
复杂度就是 O(线段长度和), 也能直接计算出k次最优
问题是O(线段长度和)的计算代价, 甚至说这就是枚举答案了,外层的二分都没意义了
题解
注意到 一个线段 随着切割次数变多, 每次贡献的代价也是单调递减的!!!!!!
再结合上面的 优先队列思路, 其实就是选取了k次最大值, 那么也就是 被选的 >=x, 未被选的 <= x
也就变成了找x, 满足如果被选的都是 大于x 则不满足题意,且如果被选的都是 大于等于 x 则满足题意
那么个数也就自然 是 大于x的个数,加上 与目标差距 除以 x 向上取整了
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| #include <bits/stdc++.h> using namespace std;
typedef long long ll; #define MOD 1000000007 #define rep(i,a,n) for (ll i=a;i<n;i++) #define per(i,a,n) for (ll i=n;i-->a;) #define pb push_back const double pi = acos(-1.0);
ll a[200010]; vector<ll> segs ; int n; ll m;
ll f(ll len, ll part){ if(part > len)return len; ll minv = len/part; ll maxcnt = len%part; return minv*minv*(part - maxcnt) + (minv+1)*(minv+1) * maxcnt; }
pair<ll,ll> calc(ll x){ assert(x > 0); ll cnt = 0; ll sum = 0; rep(i,0,n){ if(x <= 2){ sum += f(segs[i],1) - f(segs[i],segs[i]); cnt += segs[i] - 1; continue; } if(f(segs[i],1) - f(segs[i],2) < x){ continue; } int l = 1, r = segs[i]; while(l+1<r){ int mid = (l+r)/2; if(f(segs[i],mid) - f(segs[i],mid+1)>= x){ l = mid; }else{ r = mid; } } sum += f(segs[i],1) - f(segs[i],r); cnt += l; } return {cnt,sum}; }
int main(){ ll cost = 0; scanf("%d",&n); rep(i,1,n+1){ scanf("%lld",a+i); segs.push_back(a[i]-a[i-1]); cost += (a[i] - a[i-1])*(a[i] - a[i-1]); } scanf("%lld",&m); if(cost <= m){ printf("0\n"); return 0; } ll l = 0, r = 1'000'000'000'000'000'000; while(l+1<r){ ll mid = (l+r)/2; if(calc(mid).second >= cost - m){ l = mid; }else{ r = mid; } } assert(l != 0); auto [c,s] = calc(r); printf("%lld\n",c + (cost - m - s)/l + !!((cost - m - s)%l)); return 0; }
|
总结
里面这个 二分好像很有用, 感觉之前做PE应该遇到过类似的,但是没想到二分,唉 我好蠢啊
数学证明
其实这里有一个东西 没有严格证明
就是 f(x,k) - f(x,k+1) 随着k增大而减小
难就难在它是整数划分, 如果是实数的话, 直接分析导数即可
dreamoon
jiangly
简单的说, 把 (x,k-1)的方案 和 (x,k+1)的方案拼在一起, 那么它一定是 2x 分割2k块的一个方案
那么 显然 (x,k)的方案的两倍 恰好是(2x,2k)的最优解
因此 2f(x,k) <= f(x,k-1) + f(x,k+1) 即
f(x,k-1) - f(x,k) >= f(x,k) - f(x,k+1) 得证
参考
https://blog.csdn.net/Code92007/article/details/124089868