Atcoder Beginner Contest 267
C. Index × A(Continuous ver.)
题目大意
给定一个序列\(A\),找一长度为 \(m\)的连续子序列\(B\),其 \(\sum\limits_{i=1}^{m} i \times B_i\)值最大,求该最大值。
解题思路
枚举该子序列即可,注意到下一个子序列的值可以由上一个子序列的值\(O(1)\)转移过来,它们就相差了一个区间和\(sum[i-1] - sum[i - 1 - m]\)以及 \(m\times A[i]\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<LL> a(n + 1, 0);
LL ans = -1e18;
LL sum = 0;
LL tmp = 0;
for(int i = 1; i <= n; ++ i){
cin >> a[i];
if (i <= m){
sum += a[i];
tmp += a[i] * i;
}else{
ans = max(ans, tmp);
tmp -= sum;
sum -= a[i - m];
sum += a[i];
tmp += a[i] * m;
}
}
ans = max(ans, tmp);
cout << ans << endl;
return 0;
}
D. Index × A(Not Continuous ver.)
题目大意
给定一个序列\(A\),找一长度为 \(m\)的(不连续)子序列\(B\),其 \(\sum\limits_{i=1}^{m} i \times B_i\)值最大,求该最大值。
解题思路
设\(dp[i][j]\)表示前 \(i\)个数选了 \(j\)个数的最大值,对于第\(i+1\)个数考虑选或不选转移即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<vector<LL>> dp(n + 1, vector<LL>(m + 1, -1e18));
dp[0][0] = 0;
for(int i = 1; i <= n; ++ i){
LL a;
cin >> a;
dp[i][0] = 0;
for(int j = 1, up = min(i, m); j <= up; ++ j){
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + a * j);
}
}
cout << dp[n][m] << '\n';
return 0;
}
E. Erasing Vertices 2
题目大意
给定一张\(n\)个点 \(m\)条边的无向图,点有点权。需要进行\(n\)次操作,每次操作,选择一个点\(a\),并移除该点以及与该点相连的所有边,其代价是与点\(a\)直接相连的所有点权和。问所有操作的代价的最大值的最小值是多少。
解题思路
我们每次移除当前代价最小的那个,那么最终一定是最大值最小的情况。于是我们用优先队列维护这个代价最小的那个点,移除后暴力修改周围点的代价,并把新的代价加入到优先队列里。
优先队列里出队时先看其代价是不是当前点的代价,就能判断这个是不是最新的代价了。
暴力修改的复杂度其实就是边的数量,因此总的时间复杂度是\(O((n+m)\log (n+m) + m)\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<LL> a(n);
for(auto &i : a)
cin >> i;
vector<vector<int>> edge(n, vector<int>());
vector<LL> sum(n, 0);
for(int i = 1; i <= m; ++ i){
int x, y;
cin >> x >> y;
-- x;
-- y;
edge[x].push_back(y);
edge[y].push_back(x);
sum[x] += a[y];
sum[y] += a[x];
}
priority_queue<pair<LL, int>> qwq;
for(int i = 0; i < n; ++ i){
qwq.push({- sum[i], i});
}
LL ans = 0;
int cnt = n;
set<int> ff;
while(cnt){
auto [val, u] = qwq.top();
val = -val;
qwq.pop();
if (val != sum[u])
continue;
ff.insert(u);
ans = max(ans, val);
cnt --;
for(auto &v : edge[u]){
if (ff.find(v) != ff.end())
continue;
sum[v] -= a[u];
qwq.push({-sum[v], v});
}
}
cout << ans << '\n';
return 0;
}
F. Exactly K Steps
题目大意
给定一棵树,有\(q\)个询问,第 \(i\)个询问求任意一个与点 \(u_i\) 距离\(k_i\)的点的下标。
解题思路
考虑到树上往父亲跑容易,但往儿子跑不太容易,因此我们都尽量往父亲跑。
注意到,任意一个点与树上的点距离最远的,一定是这棵树的直径上的点,这个从直径的证明里可以得出。
因此先跑两次\(dfs\)求得树的直径,然后再分别以直径两点为根,预处理往上跳的倍增数组。对于询问中的点\(u_i\),就在距离根节点最远的那棵树往上跳\(k_i\)步就能得到答案。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
const int N = 2e5 + 8;
const int SP = 25;
vector<int> G[N];
int n, q;
struct tu{
int up[N][SP];
int dep[N];
void dfs(int u, int fa) {
up[u][0] = fa; dep[u] = dep[fa] + 1;
FOR (i, 1, SP) up[u][i] = up[up[u][i - 1]][i - 1];
for (int& v: G[u]) {
if (v == fa) continue;
dfs(v, u);
}
}
int lca(int u, int t) {
FOR (i, 0, SP) if (t & (1 << i)) u = up[u][i];
return u;
}
}l1, l2;
int dep[N];
int solve(int u, int fa) {
dep[u] = dep[fa] + 1;
int maxDeep = u;
for (int& v: G[u]) {
if (v == fa) continue;
int maxd = solve(v, u);
if (dep[maxDeep] < dep[maxd])
maxDeep = maxd;
}
return maxDeep;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i < n; ++ i){
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
int l = solve(1, 1);
dep[l] = 0;
int r = solve(l, l);
l1.dfs(l, l);
l2.dfs(r, r);
cin >> q;
tu* L;
for(int i = 1; i <= q; ++ i){
int u, k;
cin >> u >> k;
if (l1.dep[u] < l2.dep[u]){
L = &l2;
}else {
L = &l1;
}
if (L->dep[u] <= k)
cout << -1 << '\n';
else
cout << L->lca(u, k) << '\n';
}
return 0;
}
G. Increasing K Times
题目大意
给定一个序列\(A\),可打乱其排列顺序,问有多少个排列情况满足,恰好有\(k\)个数对,前者小于后者。
解题思路
神奇的代码
Ex. Odd Sum
题目大意
给定一个序列\(A\),其中 \(1 \leq A_i \leq 10\),问有多少个长度为奇数的子序列其和为\(M\)
解题思路
朴素的\(DP\)很容易可以想到设 \(dp[i][0/1][j]\)表示前 \(i\)个数,选了偶数/奇数个,和为 \(j\)的方案数。虽然第一维可以再压缩一下,将 \(1-10\)这 \(10\)个数依次考虑,考虑选多少个数出来,最后再 \(C\)一下,但因为 \(j\)状态里面,\(O(NM)\)的状态始终降不下来。
这里考虑下转移策略,我们的转移方程始终是从\(i-1\)转移过来的,即 \((1,i-1)\)的状态和 \((i,i)\) 的状态结合,得到了\((1,i)\)的状态。每次转移长度都增加 \(1\)。
那么考虑倍增地方式结合。一开始我们有 \(n\)个状态,分别是 \((1,1),(2,2),...,(n,n)\)。然后像线段树合并或者分治合并的方式,先让 \((1,1),(2,2)\)合并成 \((1,2)\), \((3,3),(4,4)\)合并成 \((3,4)\),然后 \((1,2),(3,4)\)合并成 \((1,4)\)这样,这样我们每次合并时的长度都会翻倍,因此我们只需要合并 \(\log n\)次就可以得到\((1,n)\)的状态,也即 \(dp[n]\)。
而对于合并,观察\(dp\)转移方程\(dp[i][0][j] = dp[i - 1][0][k] + dp[i - 1][0][j - k]......\) ,第三维是个卷积形式,因此我们将\(dp[i][0]\)和 \(dp[i][1]\)分别看成一个多项式,其中 \(x_j\)的系数为 \(dp[i][0][j]\)和 \(dp[i][1][j]\),转移的时候就是两个多项式相乘,用 \(NTT\)加速计算。
合并两个多项式也即:
理论时间复杂度是\(O(M\log M\log n)\),但由于中间实现会产生较多次的临时数组创建开销,不知该怎么优雅的实现榜一3分钟A的代码还确实用这种做法能过,但其贴了多项式全家桶长达800行不知怎么做到的
有个一个小小的优化,就是由于只有\(10\)个数,我们就预处理每一个数的选择方案,也即 \(dp[i][0/1]\)表示一个由选择了偶数个/奇数个\(i\)的多项式, 其中\(x_j\)的系数表示和为 \(j=i\times cnt\)的方案数,其实就是个组合数\(C^{cnt}_{num_i}\),其中\(num_i\)表示序列\(A\)中 \(i\)个个数。然后我们只需要做 \(10\)次 \(NTT\)就可以了。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
const int N = 1e5 + 8;
const LL MOD = 998244353;
const int G = 3;
int n, m;
int cnt[11];
vector<LL> dp[11][2];
vector<LL> f[2], g[2];
LL bin(LL x, LL n, LL MOD) {
LL ret = MOD != 1;
for (x %= MOD; n; n >>= 1, x = x * x % MOD)
if (n & 1) ret = ret * x % MOD;
return ret;
}
inline LL get_inv(LL x, LL p) { return bin(x, p - 2, p); }
LL wn[(N * 10) << 2], rev[(N * 10) << 2];
int NTT_init(int n_) {
int step = 0; int n = 1;
for ( ; n < n_; n <<= 1) ++step;
FOR (i, 1, n)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (step - 1));
int g = bin(G, (MOD - 1) / n, MOD);
wn[0] = 1;
for (int i = 1; i <= n; ++i)
wn[i] = wn[i - 1] * g % MOD;
return n;
}
void NTT(vector<LL>& a, int n, int f) {
FOR (i, 0, n) if (i < rev[i])
std::swap(a[i], a[rev[i]]);
for (int k = 1; k < n; k <<= 1) {
for (int i = 0; i < n; i += (k << 1)) {
int t = n / (k << 1);
FOR (j, 0, k) {
LL w = f == 1 ? wn[t * j] : wn[n - t * j];
LL x = a[i + j];
LL y = a[i + j + k] * w % MOD;
a[i + j] = (x + y) % MOD;
a[i + j + k] = (x - y + MOD) % MOD;
}
}
}
if (f == -1) {
LL ninv = get_inv(n, MOD);
FOR (i, 0, n)
a[i] = a[i] * ninv % MOD;
}
}
vector<LL> operator+(vector<LL> a, const vector<LL>& b){
a.resize(max(a.size(), b.size()));
for(int i = 0; i < b.size(); ++ i)
a[i] = (a[i] + b[i]) % MOD;
return a;
}
vector<LL> conv(vector<LL> a, vector<LL> b) {
int len = a.size() + b.size() - 1;
int n = NTT_init(len);
a.resize(n);
b.resize(n);
NTT(a, n, 1);
NTT(b, n, 1);
FOR (i, 0, n)
a[i] = a[i] * b[i] % MOD;
NTT(a, n, -1);
a.resize(len);
return a;
}
LL invf[N], fac[N] = {1};
void fac_inv_init(LL n, LL p) {
FOR (i, 1, n)
fac[i] = i * fac[i - 1] % p;
invf[n - 1] = bin(fac[n - 1], p - 2, p);
FORD (i, n - 2, -1)
invf[i] = invf[i + 1] * (i + 1) % p;
}
LL C(int n, int m){
if (n < m)
return 0;
return fac[n] * invf[m] % MOD * invf[n - m] % MOD;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
fac_inv_init(n + 1, MOD);
for(int i = 1; i <= n; ++ i){
int a;
cin >> a;
cnt[a] ++;
}
for(int i = 1; i <= 10; ++ i){
dp[i][0].resize(cnt[i] * i + 1);
dp[i][1].resize(cnt[i] * i + 1);
for(int j = 0; j <= cnt[i]; ++ j){
dp[i][j & 1][i * j] = C(cnt[i], j);
}
}
f[0].resize(1);
f[0][0] = 1;
for(int i = 1; i <= 10; ++ i){
g[0] = conv(f[0], dp[i][0]) + conv(f[1], dp[i][1]);
g[1] = conv(f[1], dp[i][0]) + conv(f[0], dp[i][1]);
f[0].swap(g[0]);
f[1].swap(g[1]);
}
if (f[1].size() <= m)
cout << 0 << '\n';
else
cout << f[1][m] << '\n';
return 0;
}