AHOI2018 Day2

前言

day2相对于day1代码上短了不少,但想拿到200+还是有难度的。

游戏

思考一下有些点:(1)只有当人和钥匙在门的同一侧,才能通过这道门;(2)如果以房间\(x\)为起始位置,能到达\([l,r]\)(一定是一个连续区间),那么对于其他房间,只要能到达房间\(x\),能到达的区间一定包含\([l,r]\)

结合以上两点:不难发现:每次选择一个房间(这里定义房间为两道门之间所有的区域),看一看左右门能不能打开,如果能到达左边门之后的房间\(x\),由(1),\(x\)不可能到右边房间,故只能到达\([l,x]\),那么在起点就一定能到达\([l,x]\)。此时发现访问区间一定是严格的包含关系,故可以记搜解决。之后继续扩展。能继续拓展当且仅当门的钥匙在能到达的区间内。一开始博主一直在想set合并,后来发现直接判一下在不在区间内就好了,果然我还是tcl

这道题有点细节需要处理好。最后复杂度\(\mathcal O(n)\)

#include <bits/stdc++.h>

#define rep(i, a, b) for (int i = a, i##end = b; i <= i##end; ++i)
#define per(i, a, b) for (int i = a, i##end = b; i >= i##end; --i)
#define rep0(i, a) for (int i = 0, i##end = a; i < i##end; ++i)
#define per0(i, a) for (int i = a-1; ~i; --i)
#define chkmin(a, b) a = std::min(a, b)
#define chkmax(a, b) a = std::max(a, b)

typedef long long ll;

const int maxn = 1145145;

int n, m, p, l[maxn], r[maxn], door[maxn], z[maxn];

void dfs(int i) {
	if (l[i]) return;
	l[i] = r[i] = i;
	while (l[i] > 1 && !door[l[i]-1]) l[i]--;
	while (r[i] < n && !door[r[i]]) r[i]++;
	int L = l[i], R = r[i];
	for (;;) {
		int flag = 0;
		while (l[i] > 1 && l[i] <= z[l[i]-1] && z[l[i]-1] <= r[i]) dfs(l[i]-1), l[i] = l[l[i]-1], flag = 1;
		while (r[i] < n && l[i] <= z[r[i]] && z[r[i]] <= r[i]) dfs(r[i]+1), r[i] = r[r[i]+1], flag = 1;
		if (!flag) break;
	}
	rep(x, L, R) l[x] = l[i], r[x] = r[i];
}

int main() {
	scanf("%d%d%d", &n, &m, &p);
	rep(i, 1, m) {
		int x, y;
		scanf("%d%d", &x, &y);
		z[x] = y; door[x] = 1;
	}
	rep(i, 1, n) dfs(i);
	while (p--) {
		int s, t;
		scanf("%d%d", &s, &t);
		if (l[s] <= t && t <= r[s]) printf("YES\n"); else printf("NO\n");
	}
	return 0;
}

排列

这道题自己只会暴力做法,猜的\(\mathcal O(n^2)\)做法假了,故看题解学习。

把题目翻译一下,不难发现实质就是有若干棵树,父亲对儿子约束先后关系,求一个合法排列使得\(\sum\limits_{i=1}^n i\times w_{p_i}\)最大。如果有环,答案显然是\(-1\)

想一想发现不好来dp。如果没有约束,显然一顿排序从小到大就好了。可是加上约束似乎变得很棘手。现在有这样的一个小结论:如果当前结点是最小的结点,显然在父亲(如果有)选完之后第一个选择这个结点,这个是显然的。这样子这两个结点就被打包到一起了(不会有其它的结点穿插在之间),这点大概是正确性的前提。那对于其它子结点什么时候被选择呢?那么考虑两个被打包的结点,它们之间有先后顺序。假设两个结点对应的序列为\(\{a_1,a_2,\dots,a_n\}\)\(\{b_1,b_2,\dots,b_m\}\),前者合并时有在前或者在后两种,故得到

\[\left(\sum_{i=1}^na_i\times i\right)+\left(\sum_{i=1}^mb_i\times (n+i)\right)\ \ \ \ \ \ ① \]

\[\left(\sum_{i=1}^mb_i\times i\right)+\left(\sum_{i=1}^na_i\times (m+i)\right)\ \ \ \ \ \ ② \]

\(①>②\),即

\[\left(\sum_{i=1}^na_i\times i\right)+\left(\sum_{i=1}^mb_i\times (n+i)\right)>\left(\sum_{i=1}^mb_i\times i\right)+\left(\sum_{i=1}^na_i\times (m+i)\right) \]

\[\Rightarrow \sum_{i=1}^na_i\times m>\sum_{i=1}^mb_i\times n \]

\[\Rightarrow \overline a>\overline b \]

这里\(\overline a\)表示\(a_i\)的平均值,即\((\sum_{i=1}^na_i)/n\),另一个同理。我们惊奇地发现合并后可以拿之后的平均值继续合并打包。于是就得到了一个贪心合并的算法,拿个可修改的堆搞一搞就好了,当然也可以像Dijkstra算法类似不用可修改堆。正确性用前面讲的归纳就行了。复杂度\(\mathcal O(n\log n)\)

最后注意数据范围以及unsigned long longlong double就行了(博主当时由于-(ull)是正数WA了许久)

#include <bits/stdc++.h>

#define rep(i, a, b) for (int i = a, i##end = b; i <= i##end; ++i)
#define per(i, a, b) for (int i = a, i##end = b; i >= i##end; --i)
#define rep0(i, a) for (int i = 0, i##end = a; i < i##end; ++i)
#define per0(i, a) for (int i = a-1; ~i; --i)
#define chkmin(a, b) a = std::min(a, b)
#define chkmax(a, b) a = std::max(a, b)
#define x first
#define y second

typedef unsigned long long ll;

const int maxn = 555555;

int f[maxn];
int fset(int x) { return f[x] == x ? x : f[x] = fset(f[x]); }
void merge(int x, int y) { f[fset(y)] = fset(x); }

int n, a[maxn], inq[maxn], tot[maxn], cnt = 0;
ll sum[maxn], w[maxn];
std::priority_queue<std::pair<long double, int> > Q;

int main() {
    scanf("%d", &n);
    rep(i, 0, n) f[i] = i;
    rep(i, 1, n) {
        scanf("%d", &a[i]);
        if (fset(i) == fset(a[i])) { printf("-1"); return 0; }
        merge(i, a[i]);
    }
    rep(i, 1, n) scanf("%llu", &w[i]), Q.push(std::make_pair(-(long double)(sum[i] = w[i]), i)), tot[i] = inq[i] = 1;
    rep(i, 0, n) f[i] = i;
    while (!Q.empty()) {
        int u = Q.top().y; Q.pop();
        if (!inq[u]) continue;
        inq[u] = 0; merge(a[u], u); fset(u);
		sum[f[u]] += sum[u], w[f[u]] += w[u]+tot[f[u]]*sum[u], tot[f[u]] += tot[u];
		Q.push(std::make_pair(-(long double)sum[f[u]]/tot[f[u]], f[u]));
    }
    printf("%llu", w[0]);
    return 0;
}

道路

一道很水的dp题,按照题意设dp式子。注意到深度不超过40,这个很重要。设\(f_{u,x,y}\)表示结点\(u\)上面有\(x\)条铁路没删,\(y\)条公路没删,然后转移方程\(f_{u,x,y}=\max\{f_{lson,x,y}+f_{rson,x,y+1},f_{lson,x+1,y}+f_{rson,x,y}\}\)表示选择翻修左边铁路或者右边公路两种决策,边界是叶子结点对应的公式\(c_i(a_i+x)(b_i+y)\)。记忆化即可,复杂度\(\mathcal O(n\times 40^2)\)

#include <bits/stdc++.h>

#define rep(i, a, b) for (int i = a, i##end = b; i <= i##end; ++i)
#define per(i, a, b) for (int i = a, i##end = b; i >= i##end; --i)
#define rep0(i, a) for (int i = 0, i##end = a; i < i##end; ++i)
#define per0(i, a) for (int i = a-1; ~i; --i)
#define chkmin(a, b) a = std::min(a, b)
#define chkmax(a, b) a = std::max(a, b)

typedef long long ll;

const int maxn = 20000 + 5;

ll f[maxn][41][41];
int a[maxn], b[maxn], c[maxn], l[maxn], r[maxn], n;

ll dp(int u, int x, int y) {
	if (u < 0) { u = -u; return 1ll*c[u]*(a[u]+x)*(b[u]+y); }
	if (~f[u][x][y]) return f[u][x][y];
	return f[u][x][y] = std::min(dp(l[u], x, y) + dp(r[u], x, y+1), dp(l[u], x+1, y) + dp(r[u], x, y));
}

int main() {
	scanf("%d", &n);
	rep(i, 1, n-1) scanf("%d%d", &l[i], &r[i]);
	rep(i, 1, n) scanf("%d%d%d", &a[i], &b[i], &c[i]);
	memset(f, -1, sizeof f);
	printf("%lld", dp(1, 0, 0));
	return 0;
}
posted @ 2020-05-30 22:33  AC-Evil  阅读(206)  评论(0编辑  收藏  举报