T1
有 n n n 个只有时针的时钟,初始时针指向 3 , 6 , 9 , 12 3,6,9,12 3,6,9,12 点钟方向中的一个。每次操作可以选择一段编号连续的时钟使时针顺时针旋转 9 0 ∘ 90^\circ 90∘,给定每个时钟时针的目标方向求最小操作数量。
n ≤ 1 0 5 n\le 10^5 n≤105。
赛时打了个看起来很假的 O ( n 2 ) O(n^2) O(n2) dp 拿了 40 p t s 40\mathrm{pts} 40pts:令 f ( i ) f(i) f(i) 表示使 [ 1 , i ] [1,i] [1,i] 合法的最小操作数量,转移时考虑把之前某个操作延长到自己使自己可以少操作几步,对于中间的部分直接按照不考虑时针转过头的情况计算最小步数。一开始打的暴搜根本跑不出来。
namespace Subtask1 {
const int maxn = 1e5 + 5;
int a[maxn], b[maxn], f[maxn], n;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++) scanf("%d", &b[i]), b[i] = (b[i] - a[i] + 4) % 4;
f[1] = b[1];
for (int i = 2; i <= n; i ++) {
f[i] = f[i - 1] + max(0, b[i] - b[i - 1]);
int mor = 0, lst = 0;
for (int j = i - 1; j >= 2; j --) {
int now = (b[j] - b[i] + 4) % 4;
mor += max(0, now - lst), lst = now;
f[i] = min(f[i], f[j - 1] + mor + max(0, b[i] - b[j - 1]));
}
// cout << i << ' ' << f[i] << '\n';
} printf("%d\n", f[n]);
return 0;
}
}
同机房 AC 的非常玄学,挂的也非常玄学,令
f
(
i
,
j
)
f(i,j)
f(i,j) 表示先使第
i
i
i 个钟转了
j
j
j 次,然后再考虑合法的最小操作数,玄学的地方在于他们令
j
∈
[
0
,
100
]
j\in[0,100]
j∈[0,100] 非常骗分,对于每个
i
i
i 枚举时针转几周,然后从
f
(
i
−
1
,
k
)
f(i-1,k)
f(i−1,k) 转移,玄学的地方也在于他们令
k
∈
[
j
−
w
,
j
+
w
]
k\in [j-w,j+w]
k∈[j−w,j+w](其中
w
w
w 为一个个位数)……就很难评价,转移和我的想法差不多,也是考虑从前一个的操作中进行扩展,使自己少操作一些。总之非常玄学。有几个人挂就因为这几个区间没调好挂了了
10
∼
30
p
t
s
10\sim 30\mathrm{pts}
10∼30pts。这玩意可以构造类似于
01230123
⋯
0
32103210
⋯
3210
01230123\cdots {\color{orange}0}32103210\cdots 3210
01230123⋯032103210⋯3210 使得中间橙色的
0
0
0 转非常多圈。
正解:先考虑不 m o d 4 \bmod 4 mod4 的情况,那么答案就是 ∑ max { a i − a i − 1 , 0 } \sum\max\{a_i-a_{i-1},0\} ∑max{ai−ai−1,0},证明的话考虑差分数组,对于 a i > a i − 1 a_i>a_{i-1} ai>ai−1 那么显然需要在 a i a_i ai 处作为左端点新增一个加一的区间;对于 a i < a i − 1 a_i<a_{i-1} ai<ai−1 那么会有一个区间在 a i − 1 a_{i-1} ai−1 处作为右端点结束。考虑扩展到 m o d 4 \bmod 4 mod4 的情况,那我们肯定是让若干个 a i ← a i + 4 a_i\gets a_i+4 ai←ai+4 变为前一种情况。我们不妨令 b i = a i − a i − 1 b_i=a_i-a_{i-1} bi=ai−ai−1,显然 b i ∈ [ − 3 , 3 ] b_i\in[-3,3] bi∈[−3,3],但是负数在求和时会被视为为 0 0 0。考虑 + 4 +4 +4 的影响,若 a i ← a i + 4 a_i\gets a_i+4 ai←ai+4 那么 b i ← b i + 4 b_i\gets b_i+4 bi←bi+4 但 b i + 1 ← b i + 1 − 4 b_{i+1}\gets b_{i+1}-4 bi+1←bi+1−4,扩展一下我们可以“花费”一个 + 4 +4 +4 并选择一个位置 − 4 -4 −4,在 a i a_i ai 上就表现为区间加一个 4 4 4。
我们贪心地将所有 b i < 0 b_i<0 bi<0 的数拿来 + 4 +4 +4(观察到 b i = − 1 b_i=-1 bi=−1 时这么做是无意义的可以忽略),然后把正数 − 4 -4 −4(同理 b i = 1 b_i=1 bi=1 无意义)。我们把所有 − 2 , − 3 -2,-3 −2,−3 存起来,遇到一个 2 2 2 我们考虑拿一个 − 3 -3 −3 使得答案减一(使用 − 2 -2 −2 相当于没用,不如存着),遇到一个 3 3 3 我们拿一个 − 2 / − 3 -2/-3 −2/−3 使得答案减一/建二,优先使用 − 3 -3 −3。然后就做完了,很奇妙的贪心方法。赛时想到给若干项加 4 4 4 但没有把不 m o d 4 \bmod 4 mod4 的情况抽象成若干个 b i b_i bi 的变化。代码挺好写的就不放了。
T2
有 n n n 根柱子,第 i i i 根柱子高度为 H i H_i Hi 且 H H H 为 1 ∼ n 1\sim n 1∼n 的排列;定义高跳表示从 i i i 柱子跳到一个 j j j 柱子满足 i < j , h i < h j i<j,h_i<h_j i<j,hi<hj,定义低跳表示从 i i i 柱子跳到一个 j j j 柱子满足 i < j , h i > h j i<j,h_i>h_j i<j,hi>hj。要求选定一个起点,第一次高跳低跳皆可,满足相邻两次跳跃的方式不同,求最多的跳跃次数和达到这个跳跃次数的路径数。
n ≤ 3 × 1 0 5 n\le 3\times 10^5 n≤3×105。
一开始想的是建分层图,两层分别表示高跳过来 / 低跳过来,然后 spfa 跑最长路的同时维护方案数。赛后看提交记录发现 20 p t s 20\mathrm{pts} 20pts。
namespace Subtask0 {
const int maxn = 1005;
namespace Graph {
struct Edge { int to, nxt; } e[(maxn * maxn) << 1];
int head[maxn << 1], ecnt = 0;
void addEdge(int u, int v) { e[++ ecnt] = Edge { v, head[u]}, head[u] = ecnt; }
} using namespace Graph;
const int P = 1e9 + 7;
int n, H[maxn]; queue<int> q; int f[maxn], g[maxn];
bool vis[maxn];
pair<int, int> SPFA() {
for (int i = 1; i <= (n << 1); i ++)
f[i] = 1, g[i] = 1, vis[i] = 1, q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop(); vis[u] = 0;
// cout << u << ' ' << f[u] << ' ' << g[u] << '\n';
for (int i = head[u], v; i; i = e[i].nxt) {
if (f[v = e[i].to] < f[u] + 1) {
// cout << "update:" << v << '\n';
f[v] = f[u] + 1, g[v] = g[u];
if (!vis[v]) vis[v] = 1, q.push(v);
} else if (f[v] == f[u] + 1) g[v] = (1ll * g[v] + g[u]) % P;
}
}
int ans1 = 0, ans2 = 1;
for (int i = 1; i <= (n << 1); i ++)
if (f[i] > ans1) ans1 = f[i], ans2 = g[i];
else if (f[i] == ans1) ans2 = (1ll * ans2 + g[i]) % P;
return { ans1, ans2 };
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%d", &H[i]);
for (int i = 1; i <= n; i ++)
for (int j = i + 1; j <= n; j ++)
if (H[i] > H[j]) addEdge(i, j + n);
else addEdge(i + n, j);
auto [dis, way] = SPFA();
return printf("%d %d\n", dis, way), 0;
}
}
然后发现做一个简单 dp 即可做到 O ( n 2 ) O(n^2) O(n2),令 f ( i , 0 / 1 ) f(i,0/1) f(i,0/1) 表示从 i i i 出发下一步高跳 / 低跳的最长路径,再记一个 g ( i , 0 / 1 ) g(i,0/1) g(i,0/1) 维护方案数,转移很朴素。拿了 50 p t s 50\mathrm{pts} 50pts。
namespace Subtask1 {
const int maxn = 1005, P = 1e9 + 7;
int f[maxn][2], g[maxn][2], H[maxn], n;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++) scanf("%d", &H[i]);
f[n][0] = f[n][1] = 1, g[n][0] = g[n][1] = 1;
for (int i = n - 1; i >= 1; i --)
for (int j = i + 1; j <= n; j ++)
if (H[i] > H[j]) {
if (f[i][0] < f[j][1] + 1)
f[i][0] = f[j][1] + 1, g[i][0] = g[j][1];
else if (f[i][0] == f[j][1] + 1)
g[i][0] = (1ll * g[i][0] + g[j][1]) % P;
} else {
if (f[i][1] < f[j][0] + 1)
f[i][1] = f[j][0] + 1, g[i][1] = g[j][0];
else if (f[i][1] == f[j][0] + 1)
g[i][1] = (1ll * g[i][1] + g[j][0]) % P;
}
int dis = 0, way = 1;
for (int i = 1; i <= n; i ++) {
// cout << f[i][0] << ' ' << f[i][1] << ',' << g[i][0] << ' ' << g[i][1] << '\n';
if (f[i][0] > dis) dis = f[i][0], way = g[i][0];
else if (f[i][0] == dis) way = (1ll * way + g[i][0]) % P;
if (f[i][1] > dis) dis = f[i][1], way = g[i][1];
else if (f[i][1] == dis) way = (1ll * way + g[i][1]) % P;
} printf("%d %d\n", dis, way);
return 0;
}
}
考虑优化时赛时认为要树套树,因为维护 f f f 后要在 g g g 中取最长路为 f f f 的值求和,然后成功被自己薄纱。优化十分简单,直接用两棵权值树状数组分别维护高跳和低跳的情况,前者可以把数组倒过来后缀变前缀;树状数组中的元素直接是最长路 + 方案数,合并时讨论两者最长路即可。时间复杂度 O ( n log n ) O(n\log n) O(nlogn) 真给我糖丸了。
namespace ACCode {
const int maxn = 3e5 + 5, P = 1e9 + 7;
int n, h[maxn];
struct Answer {
int dis, way; Answer(int x = 0, int y = 0) { dis = x, way = y; }
Answer operator+(const Answer &oth) {
if (dis != oth.dis) return dis > oth.dis ? Answer(dis, way) : oth;
return Answer(dis, (way + oth.way) % P);
}
};
namespace BIT {
#define lowbit(x) ((x) & (-(x)))
struct bit {
Answer b[maxn];
void add(int pos, Answer x) {
for (; pos <= n; pos += lowbit(pos))
b[pos] = b[pos] + x;
}
Answer query(int pos) {
Answer res;
for (; pos; pos -= lowbit(pos)) res = res + b[pos];
return res;
}
} low, hig;
} using namespace BIT;
Answer f[maxn][2];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++) scanf("%d", &h[i]);
for (int i = n; i; i --) {
Answer up = hig.query(n - h[i] + 1), down = low.query(h[i] - 1);
f[i][0] = Answer(up.dis + 1, up.way == 0 ? 1 : up.way), f[i][1] = Answer(down.dis + 1, down.way == 0 ? 1 : down.way);
// cout << f[i][0].dis << ' ' << f[i][0].way << " " << f[i][1].dis << ' ' << f[i][1].way << '\n';
hig.add(n - h[i] + 1, f[i][1]), low.add(h[i], f[i][0]);
} Answer ans;
for (int i = 1; i <= n; i ++) for (int j = 0; j < 2; j ++)
ans = ans + f[i][j];
return printf("%d %d", ans.dis, ans.way), 0;
}
}
T3
赛时前两题部分分写爽了看这题时没仔细看数据范围导致错过 20 p t s 20\mathrm{pts} 20pts 特殊性质,实际上一分没拿。 m = 0 m=0 m=0 时可以发现答案即为卡特兰数第 n n n 项, n ≤ 20 n\le 20 n≤20 考虑状压 dp 即可。这里放一下特殊性质 20 p t s 20\mathrm{pts} 20pts。
namespace SubtaskSpecial {
const int P = 1e9 + 7, maxn = 405;
namespace Binom {
int fac[maxn << 1], inv[maxn << 1];
int qp(int x, int y) {
int res = 1;
for (; y; y >>= 1, x = 1ll * x * x % P)
if (y & 1) res = 1ll * res * x % P;
return res;
}
void init() {
fac[0] = fac[1] = 1;
for (int i = 2; i < maxn; i ++) fac[i] = 1ll * fac[i - 1] * i % P;
inv[maxn - 1] = qp(fac[maxn - 1], P - 2);
for (int i = maxn - 2; i >= 0; i --) inv[i] = 1ll * inv[i + 1] * (i + 1) % P;
}
int C(int x, int y) {
if (x < y) return 0;
return 1ll * fac[x] * inv[y] % P * inv[x - y] % P;
}
} using namespace Binom;
// 直接用通项公式即可
int Cat(int x) { return (C(x << 1, x) - C(x << 1, x - 1) + P) % P; }
void solve(int n) { printf("%d\n", Cat(n)); }
}