T1 COCI2019 Quiz
Description
Solution
考虑暴力思路:设 f[i][j]
表示取了 次,最后一次取到 的最大值。转移方程如下:
然后内层可以用斜率优化,并且有决策单调性,可以做到 。
接下来我们发现,进一步地优化必然涉及本质上的改变——因为状态数就是 了。
Kewth : 这种「拆分最优化」的题目基本上都是凸函数。
考虑带权二分。因为我们需要控制的是段数,所以我们每新增一段,就增加一个新的费用 delta 。由于是要求最大值,故当拆分数大于 时就调小 delta ,否则调大 delta。
剩下的就是些带权二分的细节了。最后的时候要 check(L)
,以及 check
内应该是 >
而非 >=
。
Code
#define ldb long double
const int N = 1e5 + 5;
using namespace std;
int n, k;
int q[N], cnt[N];
ldb f[N], X[N], Y[N];
inline bool cmp(ldb a, ldb b, ldb c, ldb d){
return a * d <= b * c;
}
bool check(ldb delta){
int l = 1, r = 1;
X[0] = n;
for(int j = 1; j <= n; ++j)
X[j] = n - j, cnt[j] = 0;
for(int j = 1; j <= n; ++j){
while(l < r and cmp(Y[q[l]] - Y[q[l + 1]], X[q[l]] - X[q[l + 1]], Y[q[l + 1]] + j, X[q[l + 1]])) // k1 < k2
++l;
f[j] = f[q[l]] + 1.0 * (j - q[l]) / (n - q[l]) + delta;
Y[j] = (n - j) * f[j] - j;
cnt[j] = cnt[q[l]] + 1;
while(l < r and cmp(Y[q[r]] - Y[j], X[q[r]] - X[j], Y[q[r - 1]] - Y[q[r]], X[q[r - 1]] - X[q[r]])) --r;
q[++r] = j;
}
return cnt[n] > k;
}
const ldb eps = 1e-15;
int main(){
cin >> n >> k;
ldb L = -1e3, R = 0, ans = 0.0;
while (R - L > eps){
ldb mid = (L + R) * 0.5;
if(check(mid)) R = mid;
else L = mid;
}
check(L); ans = f[n] - cnt[n] * L;
printf("%.9Lf\n", ans);
return 0;
}
T2 JSOI2011 柠檬
Description
Solution
一道题学两个算法 针不戳
首先考虑一个关键性质:每次转移一定是从相同颜色处的前一位转移过来。
可以这么想:这次划分当前颜色必然有贡献。可以根据「不限制划分次数」这一点感性理解。
设 f[i]
表示前 个贝壳的最大值, 是第 个贝壳的颜色, 是前 个贝壳中颜色为 的贝壳的个数,那么有
里面的式子打开就是斜率优化的形式(注意 所以可以任意换):
可以发现 都是递增的。而我们的要求是截距最大。
画画图可以发现,应该是用「栈」来维护上凸壳。
也可以使用二分决策栈来简化思维难度。假设栈顶为 ,弹掉栈顶后的栈顶为 ,那么 比 优秀一定是在某个时间以后。我们可以二分这个时间,并且与 什么时候比新增元素更优秀的时间判断 是否弹掉。
Code
const int N = 1e5 + 5;
using namespace std;
int n;
int s[N], buc[N], pre[N], cnt[N];
ll f[N], X[N], Y[N];
vector<int>st[N];
inline bool cmp(ll a, ll b, ll c, ll d){
return a * d <= b * c;
}
int main(){
r(n);
for(int i = 1; i <= n; ++i) r(s[i]);
for(int i = 1; i <= n; ++i)
pre[i] = buc[s[i]], buc[s[i]] = i, cnt[i] = cnt[pre[i]] + 1;
for(int i = 1; i <= n; ++i)
X[i] = 2ll * s[i] * cnt[i];
for(int i = 1; i <= n; ++i){
int c = s[i];
f[i] = f[i - 1] + s[i];
#define p st[c].back()
#define q st[c][st[c].size() - 2]
while(st[c].size() >= 2 and cmp(Y[p] - Y[q], X[p] - X[q], cnt[i], 1)) st[c].pop_back();
if(st[c].size()){
int k = p;
f[i] = max(f[i], f[k - 1] + 1ll * s[i] * (cnt[i] - cnt[k] + 1) * (cnt[i] - cnt[k] + 1));
}
Y[i] = 1ll * s[i] * (cnt[i] - 1) * (cnt[i] - 1) + f[i - 1];
while(st[c].size() >= 2 and cmp(Y[p] - Y[q], X[p] - X[q], Y[i] - Y[p], X[i] - X[p])) st[c].pop_back();
st[c].push_back(i);
}
w(f[n]);
return 0;
}
T3 CF1096G Lucky Tickets
Description
Solution
令 是数码的 OGF。d[i]
表示第 个数是否出现。
要求的最后背包的 OGF 就是 。直接多项式快速幂淦就完了
可以用差值的方式。因为 DFT 求出来的最后事实上就是在特定步长下的 ,然后 IDFT 回去就可以得到原多项式。本题中,我们 暴力求出来 的值,再插值回去即可。
最后的答案就是
Code
const int N = 2e6 + 5, mod = 998244353, G = 3;
using namespace std;
int n, k;
int h[10], f[N], rev[N];
int Q(long long b, int t){
long long ret = 1;
for(int i = 1; i <= t; i <<= 1, b = b * b % mod)
if(i & t) ret = ret * b % mod;
return ret;
}
void NTT(int *a, int len){
for(int i = 0; i < len; ++i)
if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int i = 1; i < len; i <<= 1){
int wn = Q(G, (mod - 1)/(i << 1));
for(int j = 0; j < len; j += (i << 1)){
int w = 1, t1, t2;
for(int k = 0; k < i; ++k, w = 1ll * w * wn % mod)
t1 = a[j + k], t2 = 1ll * w * a[i + j + k] % mod,
a[j + k] = (t1 + t2) % mod, a[i + j + k] = (t1 + mod - t2) % mod;
}
}
reverse(a + 1, a + len);
int inv = Q(len, mod - 2);
for(int i = 0; i < len; ++i)
a[i] = 1ll * a[i] * inv % mod;
}
int main(){
r(n, k);
for(int i = 1, x; i <= k; ++i)
r(x), h[x] = 1;
int len = 1, cnt = 0;
for(int l = 9 * n / 2; len <= l; len <<= 1, ++cnt);
for(int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (cnt - 1));
int v = 1, wn = Q(G, (mod - 1) / len); // 注意要在特定步长下
for(int i = 0; i < len; ++i){
for(int j = 0; j <= 9; ++j)
if(h[j]) (f[i] += Q(v, j)) %= mod;
f[i] = Q(f[i], n / 2);
v = 1ll * v * wn % mod;
}
NTT(f, len);
int ans = 0;
for(int i = 0; i < len; ++i)
(ans += 1ll * f[i] * f[i] % mod) %= mod;
w(ans);
return 0;
}
Analysis
技不如人 甘拜下风
学习了 带权二分 栈实现斜优 插值的灵活运用 海星😙