时间安排与成绩
- 7:40 开题
- 7:40 - 8:00 看完T1,感觉很一眼。写了个 n 3 n^3 n3 的三维dp,没过样例
- 8:00 - 8:10 感觉要多加两维状态,改了改,把样例过了。卡了卡空间。扔了看下一题
- 8:10 - 8:13 把T2看了,肚子疼,去厕所里想
- 8:13 - 8:18 在厕所里把题意想清楚了,感觉是一个类似区间覆盖的问题。应该可以dp,有点没想清楚
- 8:18 - 8:20 先开T3,发现T3部分分很一眼,然后把特殊性质拓展一下就是正解了
- 8:20 - 9:00 把T3暴力写完了,检查没有正确性的问题,然后把正解写完了。正解过大样例了。
- 9:00 - 9:20 把对拍写了,开拍没多久就拍出来一组数据,赶紧调了调,发现vec里忘插入元素了,改完就过了。大样例太水了。
- 9:20 - 9:40 想到了T2的一种简单写法,单 l o g log log。但是时限只有 200 m s 200ms 200ms???,不管先写了。
- 9:40 - 10:20 终于把暴力写完了,发现没过样例,再看一看题,发现题意有点理解错。但是稍微改一下就好了。自测发现大样例T了。
- 10:20 - 10:25 发现可以用单调队列优化成线性。开写,细节有点多。
- 10:25 - 11:20 过了大样例,尝试对拍,但是数据一直造的不好。过了看T4。
- 11:20 - 12:00 写了40暴力,正解没想出来
得分:100 + 100 + 100 + 40 = 340
rk 6
题解
A. 江桥的必胜策略
分析:
水题。发现 n , m , k n, m, k n,m,k 很小。将 a , b a,b a,b 序列从小到大排序。 设 d p i , j , k , 0 / 1 , 0 / 1 dp_{i, j, k, 0/1, 0/1} dpi,j,k,0/1,0/1 表示考虑到 a a a 的第 i i i 个位置, b b b 的第 j j j 个位置,已经选中了 k k k 张牌, a i a_i ai 不选/选, b j b_j bj 不选/选的方案数。转移是简单的。但是开 l o n g l o n g long \ long long long 会 MLE, 但是前两维可以滚动。
时间复杂度: O ( n × m × k ) O(n \times m \times k) O(n×m×k)
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
const int M = 11;
typedef long long LL;
const int mod = 1000000009;
int n, m, K, a[N], b[N];
int dp[N][N][M][2][2];
int Mod(int x) {
return x >= mod ? x - mod : x;
}
int main() {
scanf("%d%d%d", &n, &m, &K);
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for(int i = 1; i <= m; i ++ ) scanf("%d", &b[i]);
sort(a + 1, a + n + 1); sort(b + 1, b + m + 1);
for(int i = 0; i <= m; i ++ ) dp[0][i][0][0][0] = 1;
for(int i = 0; i <= n; i ++ ) dp[i][0][0][0][0] = 1;
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
for(int k = 0; k <= min({i, j, K}); k ++ ) {
dp[i][j][k][0][0] = Mod(Mod(Mod(dp[i - 1][j - 1][k][0][0] + dp[i - 1][j - 1][k][0][1]) + dp[i - 1][j - 1][k][1][0]) + dp[i - 1][j - 1][k][1][1]);
dp[i][j][k][0][1] = Mod(dp[i - 1][j][k][1][1] + dp[i - 1][j][k][0][1]);
dp[i][j][k][1][0] = Mod(dp[i][j - 1][k][1][0] + dp[i][j - 1][k][1][1]);
if(a[i] > b[j] && k) dp[i][j][k][1][1] = Mod(Mod(Mod(dp[i - 1][j - 1][k - 1][0][0] + dp[i - 1][j - 1][k - 1][0][1]) + dp[i - 1][j - 1][k - 1][1][0]) + dp[i - 1][j - 1][k - 1][1][1]);
}
}
}
printf("%d\n", Mod(Mod(Mod(dp[n][m][K][0][0] + dp[n][m][K][0][1]) + dp[n][m][K][1][0]) + dp[n][m][K][1][1]));
return 0;
}
B. 江桥的灌溉
分析:
实际上是给你 n n n 条线段,你需要将整条链划分成若干段,使得每条线段都被一段包含,并且每段的长度是在 [ 2 X , 2 Y ] [2X, 2Y] [2X,2Y] 之间的偶数。问最小的段数。
我们考虑朴素 d p dp dp:设 d p i dp_i dpi 表示前 i i i 个点进行划分,满足 所有右端点小于等于 i i i 的线段被一段包含 的最小段数。那么转移就是 d p i ← d p j dp_i \gets dp_j dpi←dpj, j j j 需要满足 没有一条线段的端点在 j j j 两侧, i − j ∈ [ 2 X , 2 Y ] i - j \in [2X, 2Y] i−j∈[2X,2Y] 且是偶数。 暴力枚举复杂度 O ( L 2 ) O(L^2) O(L2)
不难发现随着 i i i 的增大,原来 因被线段包含而不能转移 的点仍然不能转移。因此我们可以把每条线段存到它的右端点里,每次到一个 i i i 就枚举它 v e c t o r vector vector 里的线段,把原来的一些被线段覆盖的转移点删去。查询要查一个区间里跟当前位置奇偶性相同的位置上的最小值。要支持 单点修改,区间查询。可以用线段树维护。复杂度 O ( L × l o g 2 L ) O(L \times log_2L) O(L×log2L)。
但是这个题卡 l o g log log。我们发现每次转移的区间长度是固定的,这相当于一个区间在滑动。想到 单调队列。我们考虑维护两个单调队列 q j , q o qj,qo qj,qo,分别维护奇数位置和偶数位置。对于当前点 i i i,我们插入 i − 2 X i - 2X i−2X 这个决策点,然后维护区间长度时弹出队首。转移时从队首取决策点即可。 注意: 这里我们不能在进行每次枚举右端点为 i i i 的线段弹出队尾,因为队尾可能已经把一些合法的转移点弹出队列。但是注意到 至少存在一条线段的左右端点在其两边的点 x x x 一定不会成为最终答案的一段的端点,因此我们可以先标记出那些点是作为转移点是不合法的,然后插入 i − 2 X i - 2X i−2X 时先判断,如果合法再插入即可。
时间复杂度 O ( L ) O(L) O(L)
CODE:
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1e6 + 10;
const int INF = 1e8;
int n, l, x, y, S, T, dp[N];
int cnt[N];
bool vis[N];
vector< int > lt[N];
deque< int > qj, qo; // 单调队列优化
int main() {
scanf("%d%d", &n, &l);
scanf("%d%d", &x, &y);
for(int i = 1; i <= n; i ++ ) {
scanf("%d%d", &S, &T);
if(T > S + 1) cnt[S + 1] ++, cnt[T] --;
}
for(int i = 0; i <= l; i ++ ) {
if(i != 0) cnt[i] += cnt[i - 1];
if(cnt[i] > 0) vis[i] = 1;
}
for(int i = 0; i <= l; i ++ ) {
if(i == 0) dp[i] = 0;
else {
if(i - 2 * x >= 0) {
int p = i - 2 * x;
if(!vis[p]) { // 能够成为决策点
if(i & 1) {
while(!qj.empty() && (dp[p] <= dp[qj.back()])) qj.pop_back();
qj.push_back(p);
}
else {
while(!qo.empty() && (dp[p] <= dp[qo.back()])) qo.pop_back();
qo.push_back(p);
}
}
}
while(!qj.empty() && (i - qj.front() > 2 * y)) {
qj.pop_front();
}
while(!qo.empty() && (i - qo.front() > 2 * y)) {
qo.pop_front();
}
if(i & 1) { // 放
if(qj.empty()) dp[i] = INF;
else {
int u = qj.front();
dp[i] = dp[u] + 1;
}
}
else {
if(qo.empty()) dp[i] = INF;
else {
int u = qo.front();
dp[i] = dp[u] + 1;
}
}
}
}
if(dp[l] > l + 10) puts("-1");
else printf("%d\n", dp[l]);
return 0;
}
C. 江桥的书架
分析:
实际上是给你 n n n 个元素,每个元素有两种数值 h i h_i hi 和 w i w_i wi。你需要将序列划分为若干段,需要保证每一段的 w i w_i wi 之和小于等于 L L L,一段的权值为区间最大的 h i h_i hi。求最小的划分权值和。
好像一段的价值函数满足 反四边形不等式(包含优于交叉),因此dp转移满足决策单调性。但是一个 d p i dp_i dpi 的转移点是一段区间。直接把每个位置插入它的转移点区间对应的线段树上 l o g 2 n log_2n log2n 个节点上,然后按照 左 - 右 - 根 的顺序在线段树的每个节点上分治转移即可。复杂度 O ( l o g 2 2 n ) O(log_2^2n) O(log22n)。
但是可以单 l o g log log。
发现如果 h i h_i hi 单调不减,那么每个 i i i 显然是找到最前面满足 ∑ k = j i w k ≤ L \sum_{k = j}^{i} w_k \leq L ∑k=jiwk≤L 的 j j j 转移即可。这是因为 j j j 靠前 d p j dp_j dpj 越小,但是 j ∼ i j \sim i j∼i 这一段的权值仍然是 h i h_i hi。 这启示我们 用 [ 1 , i ] [1, i] [1,i]的后缀最大值 将 [ 1 , i ] [1, i] [1,i] 划分为若干段,那么左端点 j j j 在同一段内时 [ j , i ] [j, i] [j,i] 区间的权值相同,所以用这一段最左边的转移。
维护 [ 1 , i ] [1, i] [1,i] 的后缀最大值可以用 单调栈。我们考虑用 线段树 在每个后缀最大值的位置插入以它为上一段的右端点时的转移值。然后在 弹栈和入栈 时修改线段树中对应的转移值,查询就是通过 二分 找到最左边的 j j j,然后在线段树上查一段区间的最小值。注意还要将 j j j 作为决策点转移一次。这是要考虑没有覆盖完一段的情况。
时间复杂度 O ( n × l o g 2 n ) O(n \times log_2n) O(n×log2n)
CODE:
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
const LL INF = 1e15;
int n;
LL L, h[N], w[N], sum[N], dp[N];
LL mx[N][21];
LL query(int l, int r) {
int k = log2(r - l + 1);
return max(mx[l][k], mx[r - (1 << k) + 1][k]);
}
void build_st() {
for(int i = 1; i <= n; i ++ ) mx[i][0] = h[i];
for(int i = 1; (1 << i) <= n; i ++ ) {
for(int j = 1; j + (1 << i) - 1 <= n; j ++ ) {
mx[j][i] = max(mx[j][i - 1], mx[j + (1 << (i - 1))][i - 1]);
}
}
}
void solve1() { // n^2
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
for(int i = 1; i <= n; i ++ ) {
LL maxh = 0, nw = 0;
for(int j = i; j >= 1; j -- ) {
maxh = max(maxh, h[j]); nw += w[j];
if(nw > L) break;
dp[i] = min(dp[i], dp[j - 1] + maxh);
}
}
printf("%lld\n", dp[n]);
}
void solve2() {
memset(dp, 0x3f, sizeof dp); dp[0] = 0;
vector< LL > vec; vec.pb(0);
for(int i = 1; i <= n; i ++ ) {
if(sum[i] <= L) dp[i] = h[i];
else {
int idx = lower_bound(vec.begin(), vec.end(), sum[i] - L) - (vec.begin());
dp[i] = dp[idx] + h[i];
}
vec.pb(sum[i]);
}
printf("%lld\n", dp[n]);
}
struct SegmentTree {
int l, r;
LL val;
#define l(x) t[x].l
#define r(x) t[x].r
#define val(x) t[x].val
}t[N * 4];
void update(int p) {
val(p) = min(val(p << 1), val(p << 1 | 1));
}
void build(int p, int l, int r) {
l(p) = l, r(p) = r;
if(l == r) {val(p) = INF; return ;}
int mid = (l + r >> 1);
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
update(p);
}
void change(int p, int pos, LL c) {
if(l(p) == r(p)) {
val(p) = c;
return ;
}
int mid = (l(p) + r(p) >> 1);
if(pos <= mid) change(p << 1, pos, c);
else change(p << 1 | 1, pos, c);
update(p);
}
LL ask(int p, int l, int r) {
if(l <= l(p) && r >= r(p)) return val(p);
int mid = (l(p) + r(p) >> 1);
if(r <= mid) return ask(p << 1, l, r);
else if(l > mid) return ask(p << 1 | 1, l, r);
else return min(ask(p << 1, l, r), ask(p << 1 | 1, l, r));
}
stack< int > s;
void solve3() {
memset(dp, 0x3f, sizeof dp); dp[0] = 0;
build(1, 1, n);
vector< LL > vec; vec.pb(0);
for(int i = 1; i <= n; i ++ ) {
while(!s.empty() && h[i] >= h[s.top()]) {
int x = s.top();
change(1, x, INF);
s.pop();
}
if(!s.empty()) {
int x = s.top();
change(1, x, dp[x] + h[i]);
}
s.push(i);
change(1, i, INF);
if(sum[i] <= L) dp[i] = query(1, i);
else {
int idx = lower_bound(vec.begin(), vec.end(), sum[i] - L) - (vec.begin());
dp[i] = min(dp[i], dp[idx] + query(idx + 1, i));
LL c = ask(1, idx, i);
dp[i] = min(dp[i], c);
}
vec.pb(sum[i]);
}
printf("%lld\n", dp[n]);
}
int main() {
scanf("%d%lld", &n, &L);
bool flag = 1;
for(int i = 1; i <= n; i ++ ) {
scanf("%lld%lld", &h[i], &w[i]);
if(h[i] < h[i - 1]) flag = 0;
sum[i] = sum[i - 1] + w[i];
}
build_st();
if(n <= 1000) solve1(); // 40
else if(flag) solve2(); // 20
else solve3(); // 40
return 0;
}
D. 江桥的疑惑
分析:
感觉这题真的没黑啊。
我们先考虑暴力。显然有一个广搜:记状态 v i s p x , p y , b x , b y vis_{px, py, bx, by} vispx,py,bx,by 表示人在 ( p x , p y ) (px, py) (px,py),箱子在 ( b x , b y ) (bx, by) (bx,by) 的状态是否可行。然后标记初始状态跑广搜即可。时空复杂度 O ( N M × N M ) O(NM \times NM) O(NM×NM)。
我们发现很多状态是无用的。更具体的:人推箱子走的几步是关键的。我们只关心对于箱子的位置 ( b x , b y ) (bx, by) (bx,by),人能不能 不经过箱子 移动到箱子的上下左右位置。
于是我们设 v i s i , j , F vis_{i, j, F} visi,j,F 表示箱子在 ( i , j ) (i, j) (i,j) 位置,人在它的 F F F 方向,与它相邻的状态是否可行。考虑如何拓展:首先可以推箱子,这个是简单的。关键是能否 从一个方向移动到另一个方向。
对于一个方向 F F F 和另一个方向 F ′ F' F′,显然可以通过箱子的位置由 F F F 到 F ′ F' F′。这是一条路径,由于我们不能经过箱子,因此还需另一条不经过 箱子的路径。我们发现这等价于 两个位置都在同一个点双中。
因此我们处理出来每个位置所属的点双。拓展判断暴力枚举两个点所属的点双编号看有没有交。
简单说一下暴力枚举点双编号为什么是对的:
- 两个点被判断当且仅当队首状态的位置与这两个点都相邻,一个点对最多被判断 4 4 4 次。那么一个点的点双最多被枚举 32 32 32 次
- 一条边最多属于一个点双,每个点最多通过每一条边分到一个点双中,因此所有点所属的点双数量之和为 O ( n m ) O(nm) O(nm) 级别。
- 因此复杂度最多是 1500 × 1500 × 32 = 72000000 1500 \times 1500 \times 32 = 72000000 1500×1500×32=72000000,但实际上严重跑不满。
最后每次询问 O ( 1 ) O(1) O(1) 判断一下即可。
时间复杂度 O ( n × m ) O(n \times m) O(n×m)
CODE:
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1505;
int n, m, Q, px, py, bx, by, rk, bin[N * N];
int dfn[N * N], low[N * N], cnt;
int dx[5] = {0, -1, 0, 0, 1}, dy[5] = {0, 0, 1, -1, 0};
char mp[N][N];
bool vis[N][N][4];
vector< int > bel[N * N]; // 每个点所属点双的编号
int ID(int x, int y) {
return (x - 1) * m + y;
}
vector< int > E[N * N];
void add(int x, int y) {E[x].pb(y);}
struct state {
int x, y, F;
};
queue< state > q;
stack< int > scc;
void tarjan(int x) {
low[x] = dfn[x] = ++ rk; scc.push(x);
for(auto v : E[x]) {
if(!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
if(low[v] >= dfn[x]) { // 点双
cnt ++;
for(int u = 0; u != v; ) {
u = scc.top(); scc.pop();
bel[u].pb(cnt);
}
bel[x].pb(cnt);
}
}
else low[x] = min(low[x], dfn[v]);
}
}
bool bok[N * N * 4];
bool check(int px, int py, int qx, int qy) {
for(int b : bel[ID(px, py)]) bok[b] = 1;
bool flag = 0;
for(int b : bel[ID(qx, qy)]) flag |= bok[b];
for(int b : bel[ID(px, py)]) bok[b] = 0;
return flag;
}
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
int main() {
scanf("%d%d%d", &n, &m, &Q);
for(int i = 1; i <= n * m; i ++ ) bin[i] = i;
for(int i = 1; i <= n; i ++ ) {
scanf("%s", mp[i] + 1);
}
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
if(mp[i][j] == 'A') px = i, py = j;
else if(mp[i][j] == 'B') bx = i, by = j;
}
}
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
if(mp[i][j] == '#') continue;
for(int k = 1; k <= 4; k ++ ) {
int tx = i + dx[k], ty = j + dy[k];
if(tx >= 1 && tx <= n && ty >= 1 && ty <= m && mp[tx][ty] != '#') {
if(mp[i][j] != 'B' && mp[tx][ty] != 'B') {
int f1 = Find(ID(i, j)), f2 = Find(ID(tx, ty));
if(f1 != f2) bin[f1] = f2;
}
add(ID(i, j), ID(tx, ty)); // 连一条边
}
}
}
}
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
if(!dfn[ID(i, j)]) {
stack< int > tmp; swap(tmp, scc);
tarjan(ID(i, j));
}
}
}
for(int i = 1; i <= 4; i ++ ) {
int rx = bx + dx[i], ry = by + dy[i];
if(rx >= 1 && rx <= n && ry >= 1 && ry <= m && mp[rx][ry] != '#') {
if(Find(ID(px, py)) == Find(ID(rx, ry))) {
q.push((state) {bx, by, i}); // i 表示方向
vis[bx][by][i] = 1;
}
}
}
while(!q.empty()) {
state msk = q.front(); q.pop();
int nx = msk.x, ny = msk.y, F = msk.F;
// 沿原来方向推
int tx = nx + dx[5 - F], ty = ny + dy[5 - F];
if(tx >= 1 && tx <= n && ty >= 1 && ty <= m && mp[tx][ty] != '#') {
if(!vis[tx][ty][F]) {
vis[tx][ty][F] = 1;
q.push((state) {tx, ty, F});
}
}
int ux = nx + dx[F], uy = ny + dy[F];
for(int i = 1; i <= 4; i ++ ) {
if(i != F) {
int ox = nx + dx[i], oy = ny + dy[i];
if(ox >= 1 && ox <= n && oy >= 1 && oy <= m && mp[ox][oy] != '#') {
if(check(ux, uy, ox, oy)) {
if(!vis[nx][ny][i]) {
vis[nx][ny][i] = 1;
q.push((state) {nx, ny, i});
}
}
}
}
}
}
for(int i = 1; i <= Q; i ++ ) {
int gx, gy; scanf("%d%d", &gx, &gy);
bool f = 0;
for(int j = 1; j <= 4; j ++ ) f |= vis[gx][gy][j];
if(f || (bx == gx && by == gy)) puts("YES");
else puts("NO");
}
return 0;
}