当前位置: 首页 > news >正文

4.16 AT好题选做

文章目录

  • 前言
  • [ARC103D] Distance Sums(确定树的形态,trick)
  • [AGC062B] Split and Insert(区间 d p dp dp)
  • [AGC012E] Camel and Oases(状压,可行性dp转最优性dp)
  • [ARC094D] Normalization(trick,转化)
  • [ARC125F] Tree Degree Subset Sum(结论,adhoc)
  • [AGC049E] Increment Decrement(slope trick, 计数技巧,神仙题)

前言

学长给我们挑了几道AT的思维题,码量都很小,但是思维难度较高。
马拉松的时候过了前三道,后三道的确是人类智慧。记录一下做法。

[ARC103D] Distance Sums(确定树的形态,trick)

题意:
给你一个长度为 N N N 的正整数数组 D i D_i Di保证 D i D_i Di 互不相同。你需要构造一棵 N N N 个点的树,树的边权都为 1 1 1, 满足 i i i 号点到其它所有点的距离和恰好为 D i D_i Di。如果不存在输出 − 1 -1 1,否则输出所有的边 ( u i , v i ) (u_i, v_i) (ui,vi)

2 ≤ N ≤ 1 0 5 , 1 ≤ D i ≤ 1 0 12 2 \leq N \leq 10^5, 1\leq D_i \leq 10^{12} 2N105,1Di1012保证 D i D_i Di 互不相同

分析:
关键点在于怎样运用 D i D_i Di 互不相同。
首先容易看出,如果能构造满足条件的树,那么这棵树一定只有一个 重心,并且重心的编号就是 D i D_i Di 最小的 i i i
考虑以这个重心为根确定树的形态:
从根往下确定是比较难做的,我们考虑对于每个点确定它的父亲,这在许多交互题里是常见的套路。设任意一个点 x x x 的父亲为 y y y,一定满足 D y < D x D_y < D_x Dy<Dx,这是因为在以重心为根的情况下向子树里递归距离的增量总是大于减量的。那么我们将 D i D_i Di 从小到大排序后倒着确定每个点 i i i 的父亲,此时一定确定 i i i 子树里所有的点。
那么同时知道 i i i子树大小 D i D_i Di,是很容易确定 i i i 父亲 j j j D j D_j Dj 的。考虑从 j j j 递归到 i i i 时,增量为 n − s z i n - sz_i nszi,减量为 s z i sz_i szi,因此有 D j + n − 2 × s z i = D i D_j + n - 2 \times sz_i = D_i Dj+n2×szi=Di D j = D i + 2 × s z i − n D_j = D_i + 2 \times sz_i - n Dj=Di+2×szin,那么开一个 m a p map map 就能知道 D j D_j Dj 对应的编号了。
坑点在于还原出树的形态也未必能对应上 D i D_i Di 数组,我们还需要验证一下重心 x x x 到其它点的距离和是否等于 D x D_x Dx
复杂度 O ( n ) O(n) O(n)
CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
unordered_map< LL, int > idx;
int n, odr[N], fat[N], sz[N];
LL d[N];
inline bool cmp(int x, int y) {return d[x] < d[y];}
int main() {scanf("%d", &n);for(int i = 1; i <= n; i ++ ) {scanf("%lld", &d[i]);odr[i] = i;idx[d[i]] = i;}sort(odr + 1, odr + n + 1, cmp);for(int i = 1; i <= n; i ++ ) sz[i] = 1;LL res = 0;for(int i = n; i > 1; i -- ) { // 倒着考虑 int o = odr[i];LL v = d[o] - (n - 2 * sz[o]);if(!idx[v] || v >= d[o]) {puts("-1"); return 0;}sz[idx[v]] += sz[o], fat[o] = idx[v]; res += 1LL * sz[o];}if(res != d[odr[1]]) {puts("-1"); return 0;}for(int i = 1; i <= n; i ++ ) {if(fat[i]) printf("%d %d\n", fat[i], i);}return 0;
}

[AGC062B] Split and Insert(区间 d p dp dp)

题意:
给你一个长度为 N N N 的排列 A A A,初始时 A i = i A_i = i Ai=i。你可以进行 K K K 轮操作,每一轮你可以选择排列末尾的 k i k_i ki 个数字,需要满足 k i ∈ [ 0 , n − 1 ] k_i \in [0, n - 1] ki[0,n1]。 然后将排列变成 A ′ A' A, 需要满足 A 1 … n − k i A_{1 \dots n - k_i} A1nki A ′ A' A A ′ A' A 的一个 子序列 A n − k i + 1 … n A_{n - k_i + 1 \dots n} Anki+1n A ′ A' A 的一个子序列。
你的目标是 K K K 轮操作后将排列变成 P P P,你的花费为 ∑ i = 1 K C i × k i \sum\limits_{i = 1}^{K}C_i \times k_i i=1KCi×ki。其中 P P P 为给定排列, C C C 为给定的数组。问你能否完成目标,如果不能输出 − 1 -1 1,否则输出最小花费。

2 ≤ N ≤ 100 , 1 ≤ K ≤ 100 , 1 ≤ C i ≤ 1 0 9 2 \leq N \leq 100, 1 \leq K \leq 100, 1 \leq C_i \leq 10^9 2N100,1K100,1Ci109

分析:
首先令 p o s P i = i pos_{P_i} = i posPi=i,然后将 A i ← p o s i A_i \gets pos_i Aiposi,那么目标变成了将 A A A 排序。
来考虑操作和排序有什么关系:发现如果有两个极长有序段,那么操作后面的连续段可以把它与前面的合并为一个极长有序段

这启发我们先将原排列划分成若干个 极长有序段 考虑:
首先有一个容易想到的事情是,每次操作一定是末尾的若干完整段,不会在某一个段内断开。
感性证明:如果第 i i i 次在某一段内断开,那么下次操作剩下这一部分时的代价系数 C j C_j Cj 如果小于 C i C_i Ci,显然可以将原来那一部分放到这次操作,否则可以将剩下这一部分在上次操作,答案会变得更优。
那么目标就变成了最优化合并过程,使得最后合并成一段。

每次操作的末尾几段应该怎么与前面合并?
首先合并后这几段不可能存在两段同时出现在一个有序段里,因为相对顺序不能改变。所以它们只有分别找前面的一段合并才有用。
但是你会发现将这几段合并到最靠前的段里好像未必最优,因为可能存在后面某轮 C i C_i Ci 比较小,我想把此时最后一段合并到第一段,那么此时最后一段应该越多越优。
但是能够发现 这些段的第一段一定是合并到开头段 的。证明感性理解吧。
那么由于 C i C_i Ci 的系数是 数量,会发现此时选中段与前面段可以 独立考虑 了!!可以认为 两部分在后面的轮次中都需要将所有段合并到这一部分的开头段。并且容易证明不存在更优秀的策略了。

因此直接区间 d p dp dp 即可:设 d p l , r , k dp_{l, r, k} dpl,r,k 表示从第 k k k 轮开始往后的轮次中,将 l ∼ r l \sim r lr 的极长连续段合并成一段的最小花费。转移枚举第 k k k 次选中的后缀段即可。复杂度 O ( n 3 K ) O(n^3K) O(n3K)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
typedef long long LL;
int n, K, p[N], pos[N], a[N], tot;
LL c[N], dp[N][N][N], sum[N];
int main() {scanf("%d%d", &n, &K);for(int i = 1; i <= K; i ++ ) scanf("%lld", &c[i]);for(int i = 1; i <= n; i ++ ) {scanf("%d", &p[i]); pos[p[i]] = i;}for(int i = 1; i <= n; i ++ ) p[i] = pos[i]; // 现在要把 p[i] 排序for(int i = 1; i <= n; ) {int j = i + 1;while(j <= n && p[j] > p[j - 1]) j ++;a[++ tot] = j - i;i = j;}for(int i = 1; i <= tot; i ++ ) sum[i] = sum[i - 1] + a[i];memset(dp, 0x3f, sizeof dp);for(int i = 0; i <= K + 1; i ++ ) {for(int j = 1; j <= tot; j ++ ) {dp[j][j][i] = 0;}}for(int len = 2; len <= tot; len ++ ) {for(int L = 1; L + len - 1 <= tot; L ++ ) {int R = L + len - 1;for(int k = K; k >= 1; k -- ) {			dp[L][R][k] = min(dp[L][R][k], dp[L][R][k + 1]); // 不动for(int mid = L + 1; mid <= R; mid ++ ) { // [mid, R] 移动  dp[L][R][k] = min(dp[L][R][k], dp[L][mid - 1][k + 1] + dp[mid][R][k + 1] + (sum[R] - sum[mid - 1]) * c[k]);}}}}if(dp[1][tot][1] > 1e12) puts("-1");else printf("%lld\n", dp[1][tot][1]);return 0;
}

[AGC012E] Camel and Oases(状压,可行性dp转最优性dp)

题意:
给你数轴上的 N N N 个点 x 1 … x N x_1 \dots x_N x1xN,保证 − 1 0 9 ≤ x 1 < x 2 < ⋯ < x N ≤ 1 0 9 -10^9 \leq x_1 < x_2 < \dots < x_N \leq 10^9 109x1<x2<<xN109。你需要对某个点 i i i 判断以 x i x_i xi 为起点能否按照以下方式遍历所有点:

  • 初始时有一个油量上限 V V V V V V 开始时给定。开始时有 V V V 升油。
  • 你可以从当前点 a a a 走路到一个点 b b b,满足 ∣ x a − x b ∣ ≤ V ′ |x_a - x_b| \leq V' xaxbV V ′ V' V 为此时剩下的油量,走路会消耗你 ∣ x a − x b ∣ |x_a - x_b| xaxb升油。
  • 当你位于 N N N 个点的任意一个点时,可以将油量加至上限。
  • V ′ > 0 V' > 0 V>0 时,你可以 跳跃 到这 N N N 个点的任意一个,并将油量上限改为 ⌊ V ′ 2 ⌋ \left \lfloor \frac{V'}{2} \right \rfloor 2V

2 ≤ N , V ≤ 2 × 1 0 5 2 \leq N, V \leq 2 \times 10^5 2N,V2×105 − 1 0 9 ≤ x 1 < x 2 < ⋯ < x N ≤ 1 0 9 -10^9 \leq x_1 < x_2 < \dots < x_N \leq 10^9 109x1<x2<<xN109,保证 V V V x i x_i xi 都是整数。

分析:
首先不难发现,每次只会在油量满的情况下 跳跃,因此油量上限只会取到 T = { ⌊ V 2 i ⌋ } T = \{\left \lfloor \frac{V}{2^i} \right \rfloor\} T={2iV} 中的数。并且能观察出 ∣ T ∣ = log ⁡ 2 V |T| = \log_2 V T=log2V,将集合中的数从大到小排序,设第 i i i 个数为 T i T_i Ti
考虑 走路跳跃 交替使用的过程:假设当前上限为 v ∈ T v \in T vT,将相邻且距离不超过 v v v 的点连一条边,那么会形成若干条链。如果当前在 x x x 号点,一定是把 x x x 所在链上的点用 走路 遍历完后,跳跃到别的点上,然后令 v ← ⌊ v 2 ⌋ v \gets \left \lfloor \frac{v}{2} \right \rfloor v2v。并且不难发现,随着 v v v 的减小,一定是在原先的基础上删掉一些边得到新的图。
可以认为,遍历所有点的过程等价于 将所有点都分到一条链里,并且满足所有链是按照 S S S 中一个数为限制形成的,并且没有用到重复的数。进一步的,如果我们规定一个点为起点,那么就表明这个点一定在以 V V V 为限制形成的链里。
由此我们枚举起点 s s s,可以得到一个可行性 d p dp dp c h e c k check check
先将 s s s 所在的以 V V V 为限制形成的链上的所有点删去,然后对剩下的点 d p dp dp
d p i , S , j dp_{i, S, j} dpi,S,j 表示考虑了前 i i i 个点并且把它们都划分到了一条链中,已经用掉的数的集合为 S S S,第 i i i 个点所在链用的第 j j j 个数为限制是否可行。
i i i i + 1 i + 1 i+1 转移:首先看一下 x i + 1 ′ − x i x'_{i + 1} - x_{i} xi+1xi 是否小于等于 T j T_j Tj,如果小于等于可以转移给 d p i + 1 , S , j dp_{i + 1, S, j} dpi+1,S,j。否则可以枚举 S S S 中没用到的一个数将 i + 1 i +1 i+1 划分到一条新链中。
复杂度显然是不对的,考虑优化:首先能发现, j j j 时没有用的,因为我们每次新开一段一定直接延伸到最右边不能延伸的位置是最优的。接着你能发现,对于 S S S 相同的合法状态 d p i , S dp_{i, S} dpi,S,一定只有 i i i 最大的那个有用,因此直接改成最优化 d p dp dp
d p S dp_{S} dpS 表示用掉了 S S S 集合中的数,当前最靠右能划分到哪。转移枚举一个还没用的数 k k k ,假设当前位置在相邻距离不超过 T k T_k Tk 的情况下最远能走到 p p p,这个可以 O ( n log ⁡ V ) O(n \log V) O(nlogV) 预处理。那么可以用 p + 1 p + 1 p+1 更新 d p S ∪ k dp_{S \cup k} dpSk + 1 +1 +1 是因为新开段前可以跳跃,但是需要注意使用 S S S最后一个 还未被使用 的数时不能 + 1 +1 +1
这样单次 c h e c k check check 的复杂度就是 O ( V log ⁡ V ) O(V \log V) O(VlogV) 的。发现以 V V V 为限制形成的每条链中的点是等价的,并且如果此时链的数量大于 ∣ T ∣ |T| T 那么一定每个点都不能遍历其余点,而 ∣ T ∣ = log ⁡ V |T| = \log V T=logV,因此暴力枚举每一条链 c h e c k check check 即可。
总复杂度 O ( ( n + V ) log ⁡ 2 V ) O((n + V) \log^2 V) O((n+V)log2V)

CODE:

// 相当于把 n 个位置划分到 logV 段里,每段步长不同 
// 首先想出一个可行性dp,然后变成最优化问题即可。 
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, V, s[20], tot; // s[i] 表示 v / 2^i    取值为 [0, tot] 
int dp[1 << 19], x[N], y[N], len, lst[1 << 19], o[1 << 19]; // dp[S] 表示使用了 S 里的步长, 最靠后能延伸到哪 
int l[N], r[N], ct, R[N][19];
inline int jump(int s, int S, int l) { // 从 s 开始使用 l 步长最靠后能跳到哪 if(s > len) return s;else return R[s][l] - ((S ^ (1 << l)) == (1 << tot + 1) - 1);
}
inline void pre() { // 预处理 for(int i = 1; i <= tot; i ++ ) {for(int j = 1; j <= len; ) {int k = j + 1;while(k <= len && y[k] - y[k - 1] <= s[i]) k ++;for(int l = j; l < k; l ++ ) R[l][i] = k;j = k;}}
}
inline void DP(int l) {memset(dp, -1, sizeof dp);dp[1 << 0] = 1; // 能跳到 1 for(int s = 0; s < (1 << tot + 1); s ++ ) {if(dp[s] < 0) continue;else {for(int l = 0; l <= tot; l ++ ) {if(!(s >> l & 1)) { // 考虑使用 l 步长 dp[s ^ (1 << l)] = max(dp[s ^ (1 << l)], jump(dp[s], s, l));}}}}
}
int main() {scanf("%d%d", &n, &V);int nv = V;for(int i = 0; i <= 20; i ++ ) {s[i] = nv; if(nv == 0) {tot = i; break;}nv /= 2;}for(int i = 1; i <= n; i ++ ) scanf("%d", &x[i]);for(int i = 1; i <= n; ) {int j = i + 1;while(j <= n && x[j] - x[j - 1] <= V) j ++;l[++ ct] = i, r[ct] = j - 1;i = j;}if(ct > tot + 1) {for(int i = 1; i <= n; i ++ ) puts("Impossible"); return 0;}else {for(int i = 1; i <= ct; i ++ ) {len = 0;for(int j = 1; j <= ct; j ++ ) {if(j == i) continue;for(int k = l[j]; k <= r[j]; k ++ ) y[++ len] = x[k];}pre();DP(len);if(dp[(1 << tot + 1) - 1] >= len) {for(int k = l[i]; k <= r[i]; k ++ ) puts("Possible");}else {for(int k = l[i]; k <= r[i]; k ++ ) puts("Impossible");}}}return 0;
}

[ARC094D] Normalization(trick,转化)

题意:
给你一个仅包含 a , b , c a,b,c a,b,c 的字符串 S S S,你可以进行以下操作任意次:

  • 选择两个相邻的不相同字符,把它们替换成两个没有出现的那种字符。

例如你可以操作 a b → c c ab \to cc abcc
你需要求出一共能得到多少种 本质不同 的字符串。答案对 998244353 998244353 998244353 取模。

2 ≤ ∣ S ∣ ≤ 2 × 1 0 5 2 \leq |S| \leq 2 \times 10^5 2S2×105

分析:
很有 a t c atc atc 风格的一道题?反正我是没想到。
考虑将 a , b , c a,b,c a,b,c 分别别替换为 0 , 1 , 2 0,1,2 0,1,2。那么会发现 操作前后两个数的和在模 3 3 3 下相同
也就是说一个字符串 S S S 每个字符替换成数字后的和在任意操作后是不变的。定义 f ( S ) f(S) f(S) S S S 中的字符用数字替换后模 3 3 3 下的数字和。
那么很容易猜到 S S S 能变换得到的字符串与 f ( T ) = f ( S ) f(T) = f(S) f(T)=f(S) 的所有字符串 T T T 有很大关系。
经过打表,发现重要结论:

  • n > 3 n > 3 n>3 时, S S S 可以变换得到 所有 f ( T ) = f ( S ) f(T) = f(S) f(T)=f(S),且 T T T 中存在相邻相同字符的字符串 T T T

存在相邻相同字符也是好理解的,因为每次操作后必定存在相邻的相同字符。
那么就很容易做了,我们考虑拿 f ( T ) = f ( S ) f(T) = f(S) f(T)=f(S) T T T 的数量减去 f ( T ) = f ( S ) f(T) = f(S) f(T)=f(S) T T T 中不存在相邻相同字符的 T T T 的数量。
对于前者,显然答案是 3 n − 1 3^{n - 1} 3n1
对于后者,考虑一个 d p dp dp d p i , j , k dp_{i, j, k} dpi,j,k 表示考虑前 i i i 个字符,当前和模 3 3 3 j j j,第 i i i 个填了 k k k 的方案数,转移是平凡的。

需要特判 n ≤ 3 n \leq 3 n3 以及 S S S 中所有字符都相等的情况。
时间复杂度 O ( n ) O(n) O(n)

CODE:

// 无敌妙妙题 
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
typedef long long LL;
const LL mod = 998244353;
int n;
char s[N];
LL f[N][3][3], mi[N]; // f[i][j][k] 表示考虑到前 i 个, 和为 j, 第i个填了 k 
int main() {scanf("%s", s + 1); n = strlen(s + 1); if(n <= 3) {if(n == 1) puts("1");else if(n == 2) {if(s[1] == s[2]) puts("1");else puts("2");}else {int cnt = (s[1] != s[2]) + (s[2] != s[3]);if(cnt == 0) puts("1");else if(cnt == 1) puts("6");else {if(s[1] != s[3]) puts("3");else puts("7");}}}else {mi[0] = 1; for(int i = 1; i <= n; i ++ ) mi[i] = mi[i - 1] * 3 % mod;bool flag = 0; int sum = 0;for(int i = 2; i <= n; i ++ ) flag |= (s[i] != s[i - 1]);for(int i = 1; i <= n; i ++ ) sum += (s[i] - 'a'); sum %= 3;if(!flag) puts("1");else { // 能得到所有和相同, 存在相邻相同字符的字符串 int c = 1;for(int i = 2; i <= n; i ++ ) if(s[i] == s[i - 1]) c = 0;for(int i = 0; i <= 2; i ++ ) f[1][i][i] = 1;for(int i = 2; i <= n; i ++ ) for(int j = 0; j <= 2; j ++ ) for(int k = 0; k <= 2; k ++ ) for(int l = 0; l <= 2; l ++ )if(k != l) f[i][(j + l) % 3][l] = (f[i][(j + l) % 3][l] + f[i - 1][j][k]) % mod; LL res = 0;for(int i = 0; i <= 2; i ++ ) res = (res + f[n][sum][i]) % mod;printf("%lld\n", (mi[n - 1] - res + mod + c) % mod); }}return 0;
}

[ARC125F] Tree Degree Subset Sum(结论,adhoc)

题意:
给你一棵 N N N 个点的树,你需要求出所有满足以下条件的二元组 ( x , y ) (x, y) (x,y) 的数量:

  • 0 ≤ x ≤ N 0 \leq x \leq N 0xN
  • 存在一种恰好选出 x x x 个点的方案,使得这 x x x 个点的度数和为 y y y

2 ≤ N ≤ 2 × 1 0 5 2 \leq N \leq 2 \times 10^5 2N2×105

分析:
首先树的形态没有任何作用,我们只需要求出度数数组就可以把树扔掉。
然后是非常超模的一步:
将所有 d e g i − 1 deg_i - 1 degi1,不难发现此时求得的所有二元组 ( x , y ′ ) (x, y') (x,y) 与原来的 ( x , y ) (x, y) (x,y) 一一对应。更具体的,现在的 ( x , y ) (x, y) (x,y) 对应了原来的 ( x , y + x ) (x, y + x) (x,y+x)
那么有重要结论:

  • 所有 d e g i − 1 deg_i - 1 degi1 后,此时求得的所有二元组 ( x , y ) (x, y) (x,y),满足将 y y y 固定后合法的 x x x 是一段区间。

这个也可能是打表所有 ( x , y − x ) (x, y - x) (x,yx) 时看出的结论。

证明:
假设 d e g i − 1 deg_i - 1 degi1 后有 k k k 0 0 0。对于一个 y y y,合法的最小 x x x m n mn mn,最大 x x x m x mx mx。那么 m n mn mn 个里面一定不包含 0 0 0 m x mx mx 里面一定包含了所有的 k k k 0 0 0。因此可以将 m n mn mn 向上调整至 m n + k mn + k mn+k m x mx mx 向下调整至 m x − k mx - k mxk,只需要证明 m n + k ≥ m x − k mn + k \geq mx - k mn+kmxk,即 2 k ≥ m x − m n 2k \geq mx - mn 2kmxmn

由于 ∑ d e g i − 1 = n − 2 \sum deg_i - 1 = n - 2 degi1=n2,所以对于合法对 ( x , y ) (x, y) (x,y),有:

  • x − k ≤ y ≤ n − 2 ⇒ x ≤ y + k x - k \leq y \leq n - 2\Rightarrow x \leq y + k xkyn2xy+k

那么一定存在 ( n − x , n − 2 − y ) (n - x, n - 2 - y) (nx,n2y),因此有:

  • n − x − k ≤ n − 2 − y ≤ n − 2 ⇒ x ≥ y + 2 − k n - x - k \leq n - 2 - y \leq n - 2\Rightarrow x \geq y + 2 - k nxkn2yn2xy+2k

因此解出来 x x x 的一个范围:
y − k + 2 ≤ x ≤ y + k y - k + 2 \leq x \leq y + k yk+2xy+k

那么 m x − m n ≤ 2 k − 2 ≤ 2 k mx - mn \leq 2k - 2 \leq 2k mxmn2k22k,证毕。

因此将 度数 看作 体积点的数量 看作 价值,跑 最优化背包 即可。
但是这样暴力是 O ( n 2 ) O(n^2) O(n2) 的。注意到不同的 d e g i deg_i degi 只有 O ( n ) O(\sqrt{n}) O(n ) 种,这是因为 d e g i deg_i degi 的和为 2 n − 2 2n - 2 2n2
因此可以跑 单调队列优化多重背包。复杂度 O ( n n ) O(n \sqrt{n}) O(nn )

CODE:

// 将每个点度数减 1 后, 对于一个固定的 y, 合法的 x 是一段区间 
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n, deg[N], c[N];
int f[N], g[N], tf[N], tg[N]; // f[i] 表示现在度数和为 i 选中的最少点, g[i] 表示最多点 
int main() {scanf("%d", &n);for(int i = 1; i < n; i ++ ) {int u, v; scanf("%d%d", &u, &v);deg[u] ++; deg[v] ++;}for(int i = 1; i <= n; i ++ ) c[-- deg[i]] ++; // 每个点度数减 1 memset(f, 0x3f, sizeof f);memset(g, 0xcf, sizeof g);g[0] = f[0] = 0; for(int i = 0; i < n; i ++ ) {if(!c[i]) continue;if(i == 0) {for(int j = 0; j <= n; j ++ ) g[j] += c[i];}else {for(int r = 0; r < i; r ++ ) { // 枚举余数 deque< int > q1, q2; // 最小和最大 for(int j = 0; j <= (n - r) / i; j ++ ) { // 数量 while(!q1.empty() && f[q1.back() * i + r] - q1.back() >= f[j * i + r] - j) q1.pop_back();while(!q1.empty() && j - q1.front() > c[i]) q1.pop_front();q1.push_back(j);tf[j * i + r] = f[q1.front() * i + r] + (j - q1.front());while(!q2.empty() && g[q2.back() * i + r] - q2.back() <= g[j * i + r] - j) q2.pop_back();while(!q2.empty() && j - q2.front() > c[i]) q2.pop_front();q2.push_back(j);tg[j * i + r] = g[q2.front() * i + r] + (j - q2.front());}}for(int j = 0; j <= n; j ++ ) f[j] = tf[j], g[j] = tg[j];}}LL res = 0;for(int i = 0; i <= n; i ++ ) {if(g[i] >= f[i]) res += g[i] - f[i] + 1;}cout << res << endl;return 0;
}

[AGC049E] Increment Decrement(slope trick, 计数技巧,神仙题)

题意:
我们可以对一个非负整数序列 { A i } \{A_i\} {Ai} 进行以下两种操作任意多次:

  • 选定一项使其 + 1 +1 +1 − 1 -1 1,花费为 1 1 1
  • 选定一个区间使其中的每一项 + 1 +1 +1 − 1 -1 1,花费为 C C C

定义一个非负整数序列 { A i } \{A_i\} {Ai} 的花费为将一个初始全为 0 0 0 的序列变成序列 A A A 的最小花费。

给定 N N N 个长为 K K K 的序列 B i B_i Bi,可以将 A i A_i Ai 赋值为 B i , 1 … K B_{i, 1 \dots K} Bi,1K 中任意一项。你需要求出 K N K^N KN 种可能的 A A A 序列的花费之和。

1 ≤ N ≤ 50 , 1 ≤ C ≤ 50 , 1 ≤ K ≤ 50 1 \leq N \leq 50, 1 \leq C \leq 50, 1\leq K \leq 50 1N50,1C50,1K50 1 ≤ B i , j ≤ 1 0 9 1 \leq B_{i, j} \leq 10^9 1Bi,j109

分析:
神题!!!

这种题的一般思路是想怎样对于固定的 A A A 序列求最优答案,然后转计数就可以 d p dp dp d p dp dp 或者一些别的思路。

那么首先考虑怎么对一个固定的 A A A 求答案:

显然一操作和二操作顺序任意,先执行完所有二操作,假设得到了序列 P P P,那么一操作的花费就是 ∑ i = 1 n ∣ P i − A i ∣ \sum_{i = 1}^{n}|P_i - A_i| i=1nPiAi。二操作的花费即为 ∑ i = 1 n C × max ⁡ ( P i − P i − 1 , 0 ) \sum_{i = 1}^{n} C \times \max(P_i - P_{i - 1}, 0) i=1nC×max(PiPi1,0)。可以认为 P 0 = 0 P_0 = 0 P0=0

可以得到一个 d p dp dp
f i , j f_{i, j} fi,j 表示填过了前 i i i 个位置的 P P P,第 i i i 个位置填了 j j j 的最小花费。那么有转移:

f i , j = min ⁡ k ( f i − 1 , k + C × max ⁡ ( j − k , 0 ) ) + ∣ j − A i ∣ f_{i, j} = \min\limits_{k}(f_{i - 1, k} + C \times \max(j - k, 0)) + |j - A_{i}| fi,j=kmin(fi1,k+C×max(jk,0))+jAi

看到了加绝对值函数,猜测 f i f_i fi 关于 j j j 下凸。下面来 归纳证明 一下:
套路的,将转移拆成两步:

f i , j ← min ⁡ k ( f i − 1 , k + C × max ⁡ ( j − k , 0 ) ) f_{i, j} \gets \min\limits_{k}(f_{i - 1, k} + C \times \max(j - k, 0)) fi,jkmin(fi1,k+C×max(jk,0))

f i f_i fi 加绝对值函数 ∣ j − A i ∣ |j - A_i| jAi

第二步显然不影响下凸性,只需要证明第一步转移后下凸即可。

f i − 1 f_{i - 1} fi1 关于 j j j 下凸,最小值取到 f i − 1 , k f_{i - 1, k} fi1,k,设 w w w 为满足 f i − 1 , w + 1 − f i − 1 , w > C f_{i - 1, w + 1} - f_{i - 1, w} > C fi1,w+1fi1,w>C 的最小的数。
那么当 j ≤ k j \leq k jk,显然 f i , j ← f i − 1 , k f_{i, j} \gets f_{i - 1, k} fi,jfi1,k
j ≥ w j \geq w jw 时,显然 f i , j ← f i − 1 , w + C × ( j − w ) f_{i, j} \gets f_{i - 1, w} + C \times (j - w) fi,jfi1,w+C×(jw)
其余的 j j j 都有 f i , j ← f i − 1 , j f_{i, j} \gets f_{i - 1, j} fi,jfi1,j
概括的说就是将最小值左边部分推平,斜率大于 C C C 的部分改成一条斜率为 C C C 的直线。
那么图形的斜率仍然单调递增,还是下凸。
加上第二步转移来看,会发现实际上函数的斜率只存在 [ − 1 , C + 1 ] [-1, C + 1] [1,C+1]。第一步转移就是将斜率为 − 1 -1 1 的部分推平并将斜率为 C + 1 C + 1 C+1 的部分删去。

那么可以 s l o p e t i r c k slope \ tirck slope tirck 维护。为了方便,我们令 f 0 , j = C × j f_{0, j} = C \times j f0,j=C×j,这样正常转移也是对的。也就是首先往函数斜率变化点的可重集里插入 C C C 0 0 0。考虑两步操作变成了什么:

  • i > 1 i > 1 i>1 时,将可重集里的最小值和最大值删去。
  • 插入两个 A i A_i Ai 表示在 A i A_i Ai 部分斜率变化了 2 2 2
  • 记此时可重集的最小值为 p p p,将答案加上 A i − p A_i - p Aip,表示最小值的增量。

那么我们可以在 O ( n log ⁡ V ) O(n \log V) O(nlogV) 的复杂度解决这个问题。

考虑第二部分,改成计数:
我们要对所有可能的 A A A 求一次答案加起来。观察求解答案的过程,可以分作两部分:

  • + A i +A_i +Ai
  • 减去当前可重集的最小值

第一部分的贡献是简单的,只需要将 ∑ i = 1 n K n − 1 ∑ j = 1 K B i , j \sum_{i = 1}^{n} K^{n - 1} \sum_{j = 1}^{K}B_{i, j} i=1nKn1j=1KBi,j 加到答案中即可。
对于第二部分:
考虑枚举一种数 x x x 计算它作为最小值被减了多少次。
维护可重集是困难的,但是如果数字只有 0 / 1 0/1 0/1 是好做的。
因此我们将 x x x 被减的次数拆成 ≤ x \leq x x的数被减的次数减去 < x < x <x 的数被减的次数。
对于前者,可以将所有 ≤ x \leq x x 的数看作 0 0 0,其余看作 1 1 1。对于后者只需要将 < x < x <x 的数看作 0 0 0,其余看作 1 1 1 即可。
那么现在问题就变成了数字只有 0 , 1 0,1 0,1 两种,你需要求出 K n K^n Kn 中情况中 0 0 0 作为最小值被减了几次。
这个显然是简单的,每步操作后都有 C + 2 C+2 C+2 个数,只需要记录其中有多少个 0 0 0 就能转移。
更简单的,注意到只有可重集中所有 C + 2 C + 2 C+2 个数都是 1 1 1 0 0 0 才不是最小值。
那么考虑一个容斥,总的拿出最小值次数减去最小值取 1 1 1 的次数。
总的次数显然是 n × K n n \times K^n n×Kn
对第二部分,设一个 d p dp dp f i , j f_{i, j} fi,j 表示考虑了前 i i i A i A_i Ai 的取值,当前可重集中有 j j j 1 1 1 的方案数。转移只需要考虑当前位置选 0 0 0 还是 1 1 1 并乘上系数即可。那么第二部分的答案就是 ∑ i = 1 n f i , C + 2 × K n − i \sum_{i = 1}^{n}f_{i, C + 2} \times K^{n - i} i=1nfi,C+2×Kni

最小值种类数 n k nk nk,每次 d p dp dp 复杂度 O ( n k ) O(nk) O(nk) ,总复杂度 O ( n 2 k 2 ) O(n^2k^2) O(n2k2)

CODE:

// 绝世好题, 考察 slope trick 和 计数技巧 
#include<bits/stdc++.h>
using namespace std;
const int N = 55;
typedef long long LL;
const int mod = 1e9 + 7;
int mi[N], res;
int n, C, K, a[N][N], c[N], b[N * N], tot;
int dp[N][N]; // dp[i][j] 表示考虑到 i, 当前有 j 个 1 的方案数 
inline int del(int x, int y) {return x - y < 0 ? x - y + mod : x - y;} 
inline int add(int x, int y) {return x + y >= mod ? x + y - mod : x + y;}
inline int mul(int x, int y) {return 1LL * x * y % mod;}
inline int f(int x) { // 将 <= x 的看作 0, > x 的看作 1, 求 K^n 种有多少次最小值为 0 int ret = mul(n, mi[n]);for(int i = 1; i <= n; i ++ ) {c[i] = 0;for(int j = 1; j <= K; j ++ ) c[i] += (a[i][j] > x);}memset(dp, 0, sizeof dp);dp[0][0] = 1;for(int i = 1; i <= n; i ++ ) {if(i == 1) {dp[i][0] = mul(dp[i - 1][0], K - c[i]);dp[i][2] = mul(dp[i - 1][0], c[i]);}else {for(int j = 0; j <= C + 2; j ++ ) { // 枚举上一个有多少个 1 int &v = dp[i][j - (j == C + 2) - (j > 0)];v = add(v, mul(dp[i - 1][j], K - c[i])); // 放 0int &z = dp[i][j - (j == C + 2) - (j > 0) + 2];z = add(z, mul(dp[i - 1][j], c[i]));}}ret = del(ret, mul(dp[i][C + 2], mi[n - i]));}return ret;
}
int main() {scanf("%d%d%d", &n, &C, &K);for(int i = 1; i <= n; i ++ ) for(int j = 1; j <= K; j ++ ) {scanf("%d", &a[i][j]);b[++ tot] = a[i][j];}mi[0] = 1; for(int i = 1; i <= n; i ++ ) mi[i] = mul(mi[i - 1], K);for(int i = 1; i <= n; i ++ ) // 所有 a[i][j] 的贡献 for(int j = 1; j <= K; j ++ ) res = add(res, mul(mi[n - 1], a[i][j]));sort(b + 1, b + tot + 1);tot = unique(b + 1, b + tot + 1) - (b + 1);for(int i = 1; i <= tot; i ++ ) { // 枚举最小值, 计算它们的系数 res = del(res, mul(del(f(b[i]), f(b[i] - 1)), b[i]));}cout << res << endl;return 0;
}

相关文章:

  • 探索关系型数据库 MySQL
  • LFI to RCE
  • DiffuRec: A Diffusion Model for Sequential Recommendation
  • 将二进制的stl文件转换为ascii的形式
  • stm32F103、GD32F103读写Nor Flash实战指南
  • 吉利矩阵(DFS)
  • [AI]浅谈大模型应用开发的认知构建
  • ecovadis审核有什么原则?什么是ecovadis审核,有什么意义
  • 递归函数详解
  • Langchain-构建向量数据库和检索器
  • 【APM】NET Traces, Metrics and Logs to OLTP
  • 期货数据API对接实战指南
  • win11改回win10
  • 每日一题(小白)暴力娱乐篇31
  • Electricity Market Optimization(VI) - 机组组合问题的 GAMS 求解
  • 论文研读: LLaVA, 微调大模型以理解图像内容
  • pycharm配置python编译器,安装第三方库 (通俗易懂)
  • 【web考试系统的设计】
  • 前端开发 + Vue 2 + 卡片拖拽(带坐标系、左右互拖、多卡同容器)+ 学习参考
  • QT日历控件重写美化
  • 居委业委居民群策群力,7位一级演员来到上海一小区唱戏
  • 山西太原一处居民小区发生爆炸,现场产生大量浓烟
  • 为治理商家“卷款跑路”“退卡难”,预付式消费司法解释5月起实施
  • 招商蛇口:一季度营收约204亿元,净利润约4.45亿元
  • 2025年“投资新余•上海行”钢铁产业“双招双引”推介会成功举行
  • 年轻人的事业!6家上海人工智能企业畅想“模范生”新征程