NOIP2018提高组题解(附填数游戏logn做法)

总体来说,Day1的3题非常水,Day2的难度却飙升到一定境界了……然后我就GG了……

T1 铺设道路

题目链接
这道题一眼原题,显然,如果 d i > d i − 1 d_i>d_{i-1} di>di1,那么就会对答案造成 d i − d i − 1 d_i-d_{i-1} didi1的贡献,否则无贡献。于是代码只有十行。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, t, lst = 0, res = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &t);
        if (t > lst) res += t - lst;
        lst = t;
    }
    printf("%d\n", res);
    return 0;
}

T2 货币系统

题目链接
这道题在考场上看见的时候有点慌,毕竟刚开始一点思路都没有……后来猜测了一个很有道理的结论,最小选出的集合 B B B一定是原集合 A A A的子集。
证明:
反证法,如果不满足,必然有至少一个数 k ∉ A k\notin A k/A,并且 k k k不能被 B B B中的其他数表示,并且 k k k一定可以被 A A A中的数表示(否则就多出了一个可以表示的数,不符合题意)。
考虑 A A A中可以表示 k k k且不存在于 B B B中的数的集合为 S S S,如果 S S S中的所有数字都可以被 B B B表示,那么 k k k一定也可以被 B B B表示,矛盾;否则 B B B不能表示出 A A A中的某个数字,不符合题意。
综上所述,选出集合一定是原集合的子集。
然后这道题就很简单了, B B B要求能够表示出 A A A中的所有数字,也就是说如果 A A A中某个数字可以被其它数字表示就删掉,剩下的必须保留。这个东西把 A A A从小到大排个序跑一遍完全背包就行了。

#include <bits/stdc++.h>
using namespace std;

const int maxn = 105, maxm = 25005;
int f[maxm], arr[maxn], n, T;
int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) scanf("%d", arr + i);
        sort(arr + 1, arr + 1 + n);
        memset(f, 0, sizeof(f));
        f[0] = 1;
        int res = 0;
        for (int i = 1; i <= n; i++) {
            if (f[arr[i]]) continue;
            res++;
            for (int j = arr[i]; j < maxm; j++)
                f[j] |= f[j - arr[i]];
        }
        printf("%d\n", res);
    }
    return 0;
}

T3 赛道修建

题目链接
显然,题目要求最小化长度最长路径的长度,肯定可以二分。之后就在于如何判定。假设当前二分的值为 m m m,我们对于树进行一遍dfs,每个子树尽量多地选路径,选出的个数一样多的情况下要求留给连到子树根的没用过的链最长。然后对于每个节点,它会挂着很多没有用过的链,这个可以贪心选取链两两接起来(具体操作后面再说),并返回剩下的最长链。为什么这样是对的?如果子树没有尽量多地选路径,但是可以返回一个更长的链怎么办?显然这个不够优秀。因为原来的链大不了就不选,答案最多减少1,而后面没有尽量多地选路径已经让答案至少减少了1,因此贪心是正确的。
我们接下来考虑怎么把链两两配对还能够返回剩下链的最大值。我们把链从小到大排序,对于每一条链,我们去找最短的链,使得这两条链的长度之和 ≥ m \ge m m,然后删除这两条链继续寻找。我在考场上怕set被卡常,所以用链表实现的,类似two pointer的技巧排序之后扫一遍就行了(考场上怕出锅写了很多奇怪的东西,其实估计很多都不需要)。
然后这道题就在 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)的复杂度内解决了,我相信CCF i7的CPU一定可以跑过去的!心中有理想,国家能富强
(好吧这份代码在洛谷上最慢的点40ms)

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> P;

const int maxn = 50005;
struct Edge { int to, val, next; } edge[maxn * 2];
int par[maxn], arr[maxn], ord[maxn], head[maxn];
int used[maxn], tot, n, m, lim;
P f[maxn];
void addedge(int u, int v, int w) {
	edge[++tot] = (Edge) { v, w, head[u] };
	head[u] = tot;
}
void efs(int u, int fa) {
	par[ord[++tot] = u] = fa;
	for (int i = head[u]; i; i = edge[i].next) {
		int v = edge[i].to;
		if (v != fa) efs(v, u);
	}
}
int rgt[maxn], lft[maxn];
void del(int x) {
	lft[rgt[x]] = lft[x];
	rgt[lft[x]] = rgt[x];
	used[x] = 1;
}
int get_rgt(int x) {
	while (used[rgt[x]]) x = rgt[x];
	return rgt[x];
}
int get_lft(int x) {
	while (used[lft[x]]) x = lft[x];
	return lft[x];
}
int check(int mid) {
	memset(used, 0, sizeof(used));
	lim = mid;
	for (int i = tot; i > 0; i--) {
		int u = ord[i];
		int cnt = 0, nn = 0;
		for (int i = head[u]; i; i = edge[i].next) {
			int v = edge[i].to, w = edge[i].val;
			if (v == par[u]) continue;
			P p = f[v];
			cnt += p.first;
			arr[++nn] = p.second + w;
		}
		sort(arr + 1, arr + 1 + nn);
		arr[nn + 1] = 0;
		for (int i = 1; i <= nn; i++)
			rgt[i] = i + 1, lft[i] = i - 1;
		rgt[0] = 1, lft[nn + 1] = nn;
		for (int i = 0; i <= nn + 1; i++) used[i] = 0;
		for (int i = nn; i > 0; i--) if (arr[i] >= lim) del(i), ++cnt;
		for (int i = rgt[0], j = lft[nn + 1]; i < nn && rgt[i] <= nn;) {
			int t = arr[i];
			if (j <= i) j = rgt[i];
			while (lft[j] > i && arr[lft[j]] + t >= lim) j = lft[j];
			if (arr[j] + t < lim) { i = rgt[i]; continue; }
			del(i), del(j), ++cnt;
			i = get_rgt(i), j = get_rgt(j);
		}
		f[u] = P(cnt, arr[lft[nn + 1]]);
	}
	return f[1].first >= m;
}
int main() {
	scanf("%d%d", &n, &m);
	int sum = 0;
	for (int i = 1; i < n; i++) {
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		addedge(u, v, w);
		addedge(v, u, w);
		sum += w;
	}
	efs(1, tot = 0);
	int l = 0, r = sum / m + 10;
	while (l + 1 < r) {
		int mid = (l + r) >> 1;
		if (check(mid)) l = mid;
		else r = mid;
	}
	printf("%d\n", l);
	fclose(stdin), fclose(stdout);
	return 0;
}

T4 旅行

题目链接
本来是一道水题,我先开始没看到 m ≤ n m\le n mn的条件,后来又以为数据量1E5,白白浪费了20min……
树肯定很简单,每次贪心地往最小的儿子走即可;奇环树就暴力剖环,然后当成树跑一遍取最小值就行了。

#include <bits/stdc++.h>
using namespace std;

const int maxn = 5005, maxm = 10005;
vector<int> gra[maxn];
int head[maxn], vis[maxn], sta[maxn], isc[maxn], tot, n, m, ca, cb;
bool findc(int u, int fa) {
	vis[sta[++tot] = u] = 1;
	int s = gra[u].size();
	for (int i = 0; i < s; i++) {
		int v = gra[u][i];
		if (v == fa) continue;
		if (vis[v]) {
			int x = 0;
			while (x != v) isc[x = sta[tot--]] = 1;
			return true;
		}
		if (findc(v, u)) return true;
	}
	--tot;
	return false;
}
int temp[maxn], ans[maxn];
void dfs(int u, int fa) {
	temp[++tot] = u;
	int s = gra[u].size();
	for (int i = 0; i < s; i++) {
		int v = gra[u][i];
		if (v == fa || (u == ca && v == cb) || (u == cb && v == ca)) continue;
		dfs(v, u);
	}
}
void solve() {
	dfs(1, tot = 0);
	int flag = 1;
	for (int i = 1; i <= n; i++) if (temp[i] != ans[i])
		{ if (temp[i] > ans[i]) flag = 0; break; }
	if (flag) memcpy(ans, temp, sizeof(temp));
}
int main() {
	scanf("%d%d", &n, &m);
	memset(ans, 0x3f, sizeof(ans));
	for (int i = 1; i <= m; i++) {
		int u, v;
		scanf("%d%d", &u, &v);
		gra[u].push_back(v);
		gra[v].push_back(u);
	}
	for (int i = 1; i <= n; i++)
		sort(gra[i].begin(), gra[i].end());
	if (n == m + 1) {
		solve();
		for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
		return 0;
	}
	findc(1, tot = 0);
	for (int i = 1; i <= n; i++) if (isc[i]) {
		int s = gra[i].size();
		for (int j = 0; j < s; j++) {
			int v = gra[i][j];
			if (isc[v] && v > i)
				ca = i, cb = v, solve();
		}
	}
	for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
	return 0;
}

T5 填数游戏

题目链接
好吧我刚开始看到这道题 n n n那么小,肯定是什么状压dp,以至于想到做法之后以为不是正解,再加上巨难写,考场上就放弃了GG……最后只剩了20min给T3。
当然这道题可以打表,但我觉得这也太tm没意思了吧(主要是CCF脑子抽,明明 n , m n,m n,m都可以出到10的一百万次方的题非要出成一个打表题来迷惑人)。
第一,行数为 n n n,列数为 m m m的矩阵方案数量等价于行数为 m m m,列数为 n n n的矩阵方案数量。这个把原矩阵转置一下每个数取反即可得到新矩阵中的方案。因此它们一一对应。
剩下来的其实思路很简单,满足条件的矩形有两个性质:
1.每个数小于等于它右上角的数(即每条从右上到左下的斜线上的数字单调不降)。
2.如果一个数字等于它右上角的数字,那么他们右下角的矩形是条纹状的(即右下角的子矩形每条从右上到左下的斜线上的数字相等)。
可以证明,上面两个条件是矩形合法的充要条件。于是我们考虑分类讨论(记矩形为 A A A):
1. A 1 , 2 = A 2 , 1 A_{1,2}=A_{2,1} A1,2=A2,1,这样有两种情况,全为0和全为1.右下角的矩形是条纹状的,对答案的贡献为 2 × 4 n − 2 × 3 m − n × 2 n − 1 2\times 4^{n-2}\times 3^{m-n}\times 2^{n-1} 2×4n2×3mn×2n1.
2. A 1 , 2 ̸ = A 2 , 1 A_{1,2}\not=A_{2,1} A1,2̸=A2,1,我们考虑第三条斜线上的数值。这个时候就要特判 n ≤ 3 n\le3 n3的情况了。不过这些情况用上面的结论还是挺好算的。剩下的情况第三条斜线可能是000,001,011,111四种情况,000,111的情况其实和上面的那个差不多,贡献是 2 × 5 × 4 n − 4 × 3 m − n × 2 n − 1 2\times 5\times 4^{n-4}\times 3^{m-n}\times 2^{n-1} 2×5×4n4×3mn×2n1
否则,我们不妨设 n ≤ m n\le m nm,先考虑110的情况。我们可以枚举另外一个限制出现在什么位置。这个玩意儿对答案的贡献首先有
∑ i = 4 n − 1 4 × 5 × 4 n − i − 1 × 3 m − n × 2 n − 1 \sum_{i=4}^{n-1}4\times5\times4^{n-i-1}\times3^{m-n}\times2^{n-1} i=4n14×5×4ni1×3mn×2n1
这个其实是等比数列求和,可以快速幂计算。剩下的分两种讨论(包含了没有剩余限制的情况)。
2.1 n = m n=m n=m。剩余贡献为 4 × 3 × 2 n − 2 + 3 × 2 n − 2 4\times3\times2^{n-2}+3\times2^{n-2} 4×3×2n2+3×2n2
2.2 n ̸ = m n\not=m n̸=m。剩余贡献为 ∑ i = n + 1 m − 1 3 × 4 × 3 m − i − 1 × 2 n − 1 + 3 × 3 × 2 n − 2 + 2 n − 2 × 3 \sum_{i=n+1}^{m-1}3\times4\times3^{m-i-1}\times2^{n-1}+3\times3\times2^{n-2}+2^{n-2}\times3 i=n+1m13×4×3mi1×2n1+3×3×2n2+2n2×3,这也是等比数列,快速幂计算。
对于001的情况,和上述情况很相似了,而且讨论的部分更少。于是答案就可以在 O ( l o g n ) O(logn) O(logn)的时间内计算完毕。
嘤嘤嘤考场上被数据量坑了啊!!!!
(代码有很多地方没优化,就是按照上面的算式直接打的……)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1000000007;
ll modpow(ll a, int b) {
    ll res = 1;
    for (; b; b >>= 1) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
    }
    return res;
}
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    if (n > m) swap(n, m);
    if (n == 1) return printf("%lld\n", modpow(2, m)) * 0;
    if (n == 2) return printf("%lld\n", modpow(3, m - 1) * 4 % mod) * 0;
    ll res = 2LL * modpow(4, n - 2) * modpow(3, m - n) % mod * modpow(2, n - 1) % mod;
    if (n == 3) {
        if (m == 3) {
            res += 12 + 6 + 6;
            return printf("%lld\n", res * 2 % mod) * 0;
        }
        res = (res + 32LL * modpow(3, m - 4)) % mod;
        res = (res + 48LL * (modpow(3, m - 4) - 1) % mod * (mod + 1) / 2 + 24) % mod;
        res = (res + 16LL * modpow(3, m - 4)) % mod;
        return printf("%lld\n", res * 2 % mod) * 0;
    }
    res = (res + 10LL * modpow(4, n - 4) % mod * modpow(3, m - n) % mod * modpow(2, n - 1)) % mod;
    res = (res + 20LL * modpow(3, m - n) % mod * modpow(2, n - 1) % mod * (modpow(4, n - 4) - 1) % mod * (mod + 1) / 3) % mod;
    if (n == m) res = (res + 15LL * modpow(2, n - 2)) % mod;
    else res = (res + 16LL * modpow(3, m - n - 1) % mod * modpow(2, n - 1) + 12LL * (modpow(2, n - 2) + modpow(2, n - 1) * (modpow(3, m - n - 1) - 1) % mod * (mod + 1) / 2 % mod)) % mod;
    res = (res + 20LL * modpow(3, m - n) % mod * modpow(2, n - 1) % mod * (modpow(4, n - 4) - 1) % mod * (mod + 1) / 3) % mod;
    if (n == m) res = (res + 15LL * modpow(2, n - 2)) % mod;
    else res = (res + 20LL * modpow(3, m - n - 1) % mod * modpow(2, n - 1)) % mod;
    return printf("%lld\n", res * 2 % mod) * 0;
    return 0;
}

T6 保卫王国

题目链接
考场上看完题就只剩15min了,于是连裸的dp都写挂了呜呜呜……
好吧这道题的确可以动态dp,不过我只会 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)的ddp诶,肯定就算写出来也会被卡常。(听说可以LCT维护ddp?听起来是个好办法……不过也感觉细节好多好难写啊)
下面讲个比较NOIP的思路吧……
令倍增数组 s [ i ] [ j ] [ 0 / 1 ] [ 0 / 1 ] s[i][j][0/1][0/1] s[i][j][0/1][0/1]表示从点 i i i算起往上到第 2 j 2^j 2j个点的子树中,子树根状态为0/1,点 i i i状态为0/1时的最小值。
再令 f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1]表示除去 i i i的子树(不包括 i i i),点 i i i的状态为0/1时剩下连通块的最小值。
以下的区间均表示一条链。有了上面两个东西,对于查询的两个点 u , v u,v u,v,求出其 l c a lca lca,如果一个是另一个的father(不妨令 u u u v v v的father),我们显然可以利用 s s s O ( l o g n ) O(logn) O(logn)的时间内合并出 [ v , u ) [v,u) [v,u)的答案,并且还可以确定 v v v的状态。接下来就可以根据 f f f求出 u u u除去含 v v v的那个儿子所在子树后的最小值。合并这两个答案即可。
否则,分别统计出 [ u , l c a ) [u,lca) [u,lca) [ v , l c a ) [v,lca) [v,lca)的答案,然后枚举lca的状态,利用 f [ l c a ] f[lca] f[lca]和上面统计的两个答案就可以合并出答案了。总复杂度 O ( n l o g n ) O(nlogn) O(nlogn),不过常数似乎很大?

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;

const int maxn = 100005;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
struct Edge { int to, next; } edge[maxn * 2];
int head[maxn], p[maxn], par[20][maxn], dep[maxn], tot, n, m;
ll f[maxn][2];
struct Number {
	ll a[2][2] = {{INF, INF}, {INF, INF}}; int u;
	Number operator-(pair<ll, ll> p) const {
		Number res = *this;
		p.first = min(p.first, p.second);
		res.a[0][0] -= p.second, res.a[0][1] -= p.second;
		res.a[1][0] -= p.first, res.a[1][1] -= p.first;
		return res;
	}
	Number operator+(const Number &n) const {
		Number res; res.u = n.u;
		for (int i = 0; i < 2; i++)
		for (int j = 0; j < 2; j++)
			res.a[i][j] = min(INF, min(min(a[i][0], a[i][1]) + n.a[1][j], a[i][1] + min(n.a[0][j], n.a[1][j])));
		return res;
	}
} st[20][maxn];
void addedge(int u, int v) {
	edge[++tot] = (Edge) { v, head[u] };
	head[u] = tot;
}
void dfs(int u, int fa) {
	dep[u] = dep[par[0][u] = fa] + 1;
	st[0][u].u = u;
	st[0][u].a[0][0] = 0;
	st[0][u].a[1][1] = p[u];
	for (int i = head[u]; i; i = edge[i].next) {
		int v = edge[i].to;
		if (v == fa) continue;
		dfs(v, u);
		st[0][u].a[0][0] += st[0][v].a[1][1];
		st[0][u].a[1][1] += min(st[0][v].a[0][0], st[0][v].a[1][1]);
	}
	st[0][u].a[0][0] = min(st[0][u].a[0][0], INF);
	st[0][u].a[1][1] = min(st[0][u].a[1][1], INF);
}
void efs(int u, int fa) {
	ll a = st[0][u].a[0][0], b = st[0][u].a[1][1];
	for (int i = head[u]; i; i = edge[i].next) {
		int v = edge[i].to;
		if (v == fa) continue;
		ll ta = f[u][1] + b - min(st[0][v].a[0][0], st[0][v].a[1][1]) - p[u];
		ll tb = f[u][0] + a - st[0][v].a[1][1];
		f[v][0] = ta, f[v][1] = min(ta, tb) + p[v];
		efs(v, u);
	}
}
char type[5];
int get_lca(int u, int v) {
	if (dep[u] < dep[v]) swap(u, v);
	for (int i = 19; i >= 0; i--) if (dep[par[i][u]] >= dep[v]) u = par[i][u];
	if (u == v) return u;
	for (int i = 19; i >= 0; i--) if (par[i][u] != par[i][v]) u = par[i][u], v = par[i][v];
	return par[0][u];
}
Number get_ans(int u, int d) {
	Number res = st[0][u]; --d; u = par[0][u];
	for (int i = 19; i >= 0; i--) if (d >= (1 << i))
		d -= 1 << i, res = res + (st[i][u] - make_pair(st[0][res.u].a[0][0], st[0][res.u].a[1][1])), u = par[i][u];
	return res;
}
int main() {
	scanf("%d%d%s", &n, &m, type);
	for (int i = 1; i <= n; i++) scanf("%d", p + i);
	for (int i = 1; i < n; i++) {
		int u, v;
		scanf("%d%d", &u, &v);
		addedge(u, v);
		addedge(v, u);
	}
	dfs(1, 0);
	f[1][1] = p[1];
	efs(1, 0);
	for (int i = 1; i < 20; i++)
	for (int j = 1; j <= n; j++) {
		par[i][j] = par[i - 1][par[i - 1][j]];
		int u = st[i - 1][j].u;
		st[i][j] = st[i - 1][j] + (st[i - 1][par[i - 1][j]] - make_pair(st[0][u].a[0][0], st[0][u].a[1][1]));
	}
	while (m--) {
		int a, x, b, y;
		scanf("%d%d%d%d", &a, &x, &b, &y);
		if (dep[a] < dep[b]) swap(a, b), swap(x, y);
		int l = get_lca(a, b);
		if (l == b) {
			Number num = get_ans(a, dep[a] - dep[l]);
			int la = num.u;
			ll t = f[l][y] + st[0][l].a[y][y] - (y ? min(st[0][la].a[0][0], st[0][la].a[1][1]) + p[l] : st[0][la].a[1][1]);
			ll res = t + (y ? min(num.a[x][0], num.a[x][1]) : num.a[x][1]);
			printf("%lld\n", res >= 1E12 ? -1 : res);
		} else {
			Number na = get_ans(a, dep[a] - dep[l]), nb = get_ans(b, dep[b] - dep[l]);
			int la = na.u, lb = nb.u;
			ll ta = f[l][0] + st[0][l].a[0][0] - st[0][la].a[1][1] - st[0][lb].a[1][1];
			ll tb = f[l][1] + st[0][l].a[1][1] - min(st[0][la].a[0][0], st[0][la].a[1][1]) - min(st[0][lb].a[0][0], st[0][lb].a[1][1]) - p[l];
			ll res = min(ta + na.a[x][1] + nb.a[y][1], tb + min(na.a[x][0], na.a[x][1]) + min(nb.a[y][0], nb.a[y][1]));
			printf("%lld\n", res >= 1E12 ? -1 : res);
		}
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值