[题解]NOIP2018 Day2 Solution - by xyz32768

Z natizen gala ovasug narrin qhe ba zbik
Quatorm arrticive nafjenvt uane hak cfnik
Wafhte unitalize napivet uavidnafk fhatox
Orz pyz ak ioi
(火星文)
——《Ydjadf fha de NOIP 2018》

Day2 T1 旅行 travel

算法:模拟

  • 一棵树的情况,直接从 1 1 1 开始 DFS ,为了保证字典序最小,递归时需要按标号从小到大枚举子节点
  • 复杂度 O ( n ) O(n) O(n)
  • 基环树的情况,枚举环上一条边删掉,再执行上述过程
  • 复杂度 O ( n 2 ) O(n^2) O(n2)
  • 注意子节点的编号大小顺序需要预处理
  • 否则复杂度多个 log ⁡ \log log

代码

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

const int N = 5005, M = N << 1;

int n, m, ecnt, nxt[M], adj[N], st[M], go[M], a[N], T, ans[N], lins[N][N], X, Y;
bool vis[N], adiao;

void add_edge(int u, int v)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; st[ecnt] = u; go[ecnt] = v;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; st[ecnt] = v; go[ecnt] = u;
}

void dfs(int u, int fu)
{
	int i;
	a[++T] = u;
	For (i, 1, lins[u][0])
	{
		if (lins[u][i] == fu) continue;
		dfs(lins[u][i], u);
	}
}

void dfs2(int u, int fu)
{
	int i;
	a[++T] = u;
	vis[u] = 1;
	For (i, 1, lins[u][0])
	{
		if (lins[u][i] == fu || (u == X && lins[u][i] == Y)
			|| (u == Y && lins[u][i] == X)) continue;
		if (!vis[lins[u][i]]) dfs2(lins[u][i], u);
		else adiao = 1;
	}
}

bool compares()
{
	int i;
	For (i, 1, n)
	{
		if (a[i] < ans[i]) return 1;
		if (a[i] > ans[i]) return 0;
	}
	return 0;
}

int main()
{
	int i, j, x, y;
	n = read(); m = read();
	For (i, 1, m) x = read(), y = read(),
		add_edge(x, y);
	For (i, 1, n) Edge(i) lins[i][++lins[i][0]] = v;
	For (i, 1, n) std::sort(lins[i] + 1, lins[i] + lins[i][0] + 1);
	if (m == n - 1)
	{
		dfs(1, 0);
		For (i, 1, n) printf("%d ", a[i]);
		std::cout << std::endl;
		return 0;
	}
	For (i, 1, n) ans[i] = n + 1;
	For (i, 1, m)
	{
		T = 0; adiao = 0;
		X = st[i << 1], Y = go[i << 1];
		For (j, 1, n) vis[j] = 0;
		dfs2(1, 0);
		if (!adiao && T == n && compares())
		{
			For (j, 1, n) ans[j] = a[j];
		}
	}
	For (i, 1, n) printf("%d ", ans[i]);
	std::cout << std::endl;
	return 0;
}

Day2 T2 填数游戏 game

算法:状压 DP + 找规律

  • 先思考一下什么样的 01 01 01 矩阵满足条件
  • 先把矩阵画成对角线的形式,如下图
    在这里插入图片描述
  • 上图中同色的方块在一个对角线上
  • 瞎 jb 推一波,可以得出一个合法的 01 01 01 矩阵需要同时满足两个条件:
  • (1)同一个对角线要么全 0 0 0 ,要么全 1 1 1 ,要么下面一段全 1 1 1 而上面一段全 0 0 0
  • (2)对于任意一个 1 &lt; i ≤ n , 1 ≤ j &lt; m 1&lt;i\le n,1\le j&lt;m 1<in,1j<m ,如果 ( i , j ) (i,j) (i,j) ( i − 1 , j + 1 ) (i-1,j+1) (i1,j+1) 填的数相同,那么 ( i , j + 1 ) (i,j+1) (i,j+1) 及右下角的所有方块中,一条斜线上的所有数需要全相等,如图
    在这里插入图片描述
  • 证明由读者自行撕烤

80pts

  • 我们有了一个 80 80 80 分的状压 DP
  • f [ i ] [ j ] [ S ] f[i][j][S] f[i][j][S] 表示从下(右)往左(上)前 i i i 个对角线,第 i i i 个对角线放了 j j j 1 1 1 (由第一点性质得到, j j j 唯一确定了第 i i i 个对角线的方案), S S S 是一个集合,如果第 i i i 个对角线第 k k k 个格子及右下角的子矩阵每个对角线都满足其中填的数全相等则 S S S 中包含 k k k ,否则 S S S 不包含 k k k
  • 大力枚举 k k k ,进行转移 f [ i − 1 ] [ j ] [ S ] → f [ i ] [ k ] [ T ] f[i-1][j][S]\rightarrow f[i][k][T] f[i1][j][S]f[i][k][T]
  • T T T 可以由 j , S , k j,S,k j,S,k 计算出
  • 注意需要判断第二点性质是否满足
  • 使用滚动数组优化空间
  • 理论复杂度 O ( 2 n m n 3 ) O(2^nmn^3) O(2nmn3) (计算 T T T 需要 O ( n ) O(n) O(n) 的时间)
  • 但实际上很多转移都不满足第二点性质,可以通过 80 % 80\% 80% 的数据

100pts

  • 如果你有兴趣去找规律,设 F ( n , m ) F(n,m) F(n,m) 表示 n × m n\times m n×m 的矩阵的结果
  • 你会发现当 n + 2 ≤ m n+2\le m n+2m 时, F ( n , m ) = 3 × F ( n , m − 1 ) F(n,m)=3\times F(n,m-1) F(n,m)=3×F(n,m1)
  • 于是当 n + 2 &gt; m n+2&gt;m n+2>m 时仍然状压 DP
  • 否则算出 n × ( n + 1 ) n\times(n+1) n×(n+1) 的矩阵的结果
  • 最后乘上 3 m − n − 1 3^{m-n-1} 3mn1
  • 复杂度 O ( 2 n n 4 + log ⁡ m ) O(2^nn^4+\log m) O(2nn4+logm)
  • 不知道为什么出题人不把 m 加强到 10^9

代码

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)

template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}

const int N = 40, M = (1 << 8) + 5, ZZQ = 1e9 + 7;

int n, m, AAm, Cm, len, ls[N], f[2][N][M];

int qpow(int a, int b)
{
	int res = 1;
	while (b)
	{
		if (b & 1) res = 1ll * res * a % ZZQ;
		a = 1ll * a * a % ZZQ;
		b >>= 1;
	}
	return res;
}

int o(int S, int x)
{
	return (S >> x - 1) & 1;
}

int le(int x, int y)
{
	if (len - x + 1 >= n) return y - 1;
	return y;
}

int ri(int x, int y)
{
	if (len - x + 1 >= n) return y;
	return y + 1;
}

int main()
{
	int i, j, k, h, r;
	std::cin >> n >> m;
	if (n == 1) return printf("%d\n", qpow(2, m)), 0;
	AAm = m;
	if (m > n + 1) m = n + 1;
	len = n + m - 1;
	Cm = (1 << n) - 1;
	f[1][1][1] = f[1][0][1] = 1;
	For (i, 1, n) ls[len - i + 1] = Min(i, m);
	For (i, 2, m) ls[len - (i + n - 1) + 1] = Min(n, m - i + 1);
	For (i, 2, len)
	{
		int op = i & 1;
		For (j, 0, n) For (k, 0, Cm) f[op][j][k] = 0;
		int Cn = (1 << ls[i - 1]) - 1;
		For (j, 0, ls[i - 1]) For (k, 0, Cn)
		{
			if (!f[op ^ 1][j][k]) continue;
			For (h, 0, ls[i])
			{
				bool is = 1;
				For (r, 1, ls[i] - 1)
					if (r != h && !o(k, ri(i, r)))
						{is = 0; break;}
				if (is)
				{
					int S = 0;
					For (r, 1, ls[i])
					{
						int kl = le(i, r), kr = ri(i, r);
						if (kl < 1 || kl > ls[i - 1])
						{
							if (o(k, kr)) S |= 1 << r - 1;
						}
						else if (kr < 1 || kr > ls[i - 1])
						{
							if (o(k, kl)) S |= 1 << r - 1;
						}
						else if (o(k, kl) && o(k, kr)
							&& j != kl) S |= 1 << r - 1;
					}
					f[op][h][S] = (f[op][h][S] + f[op ^ 1][j][k]) % ZZQ;
				}
			}
		}
	}
	int res = ((f[len & 1][0][0]
		+ f[len & 1][0][1]) % ZZQ + (f[len & 1][1][0]
		+ f[len & 1][1][1]) % ZZQ) % ZZQ;
	if (AAm > m) res = 1ll * res * qpow(3, AAm - m) % ZZQ;
	std::cout << res << std::endl;
	return 0;
}

Day2 T3 保卫王国 defense

算法:树形 DP + 树上倍增

  • 仿佛是 NOIP 第一道 5K+ 的码农题
  • 如果没有询问,那么就是经典的树形 DP
  • f [ u ] [ 0 ] f[u][0] f[u][0] 表示 u u u 的子树内,不选点 u u u 并覆盖所有边的最小代价
  • f [ u ] [ 1 ] f[u][1] f[u][1] 表示 u u u 的子树内,选点 u u u 并覆盖所有边的最小代价
  • f [ u ] [ 0 ] = ∑ v ∈ s o n ( u ) f [ v ] [ 1 ] f[u][0]=\sum_{v\in son(u)}f[v][1] f[u][0]=vson(u)f[v][1]
  • f [ u ] [ 1 ] = p u + ∑ v ∈ s o n ( u ) min ⁡ ( f [ v ] [ 0 ] + f [ v ] [ 1 ] ) f[u][1]=p_u+\sum_{v\in son(u)}\min(f[v][0]+f[v][1]) f[u][1]=pu+vson(u)min(f[v][0]+f[v][1])
  • 据说很多大佬都用动态 DP ,是不是就我这个傻逼写倍增
  • 再设 g [ u ] [ 0 ] g[u][0] g[u][0] g [ u ] [ 1 ] g[u][1] g[u][1] 表示 u u u 的父亲的子树去掉 u u u 的子树,不选 / 选 u u u 的父亲的最小代价
  • 然后再进行一次换根, h [ u ] [ 0 ] h[u][0] h[u][0] h [ u ] [ 1 ] h[u][1] h[u][1] 表示整棵树去掉 u u u 的子树, u u u 的父亲不选 / 选的最小代价
  • 预处理出倍增数组 s u m [ u ] [ i ] [ s 1 ] [ s 2 ] sum[u][i][s_1][s_2] sum[u][i][s1][s2] ,表示 u u u u u u 的第 2 i − 1 2^i-1 2i1 级祖先共 2 i 2^i 2i 个点的下面的式子( s 1 , s 2 , s 3 , s 4 s_1,s_2,s_3,s_4 s1,s2,s3,s4 为布尔值)
  • max ⁡ o p k = 0 / 1 , k ∈ [ 1 , 2 i ] o p 1 = s 1 , o p 2 = s 2 i ( ∀ 1 ≤ k &lt; 2 i , o p k  OR  o p k + 1 ≠ 0 ) ∑ k = 1 2 i g [ u k ] [ o p k ] \max_{op_{k}=0/1,k\in[1,2^i]}^{op_1=s_1,op_2=s_{2^i}}(\forall1\le k&lt;2^i,op_k\text{ OR }op_{k+1}\ne 0)\sum_{k=1}^{2^i}g[u_k][op_k] opk=0/1,k[1,2i]maxop1=s1,op2=s2i(1k<2i,opk OR opk+1̸=0)k=12ig[uk][opk]
  • 其中 u k u_k uk 表示 u u u k − 1 k-1 k1 级祖先
  • s u m [ u ] [ i + 1 ] [ s 1 ] [ s 2 ] = max ⁡ s 3 ∈ [ 0 , 1 ] , s 4 ∈ [ 0 , 1 ] s 3  OR  s 4 ≠ 0 { s u m [ u ] [ i ] [ s 1 ] [ s 3 ] + s u m [ f a [ u ] [ i ] ] [ s 4 ] [ s 2 ] } sum[u][i+1][s_1][s_2]=\max_{s_3\in[0,1],s_4\in[0,1]}^{s_3\text{ OR }s_4\ne 0}\{sum[u][i][s_1][s_3]+sum[fa[u][i]][s_4][s_2]\} sum[u][i+1][s1][s2]=s3[0,1],s4[0,1]maxs3 OR s4̸=0{sum[u][i][s1][s3]+sum[fa[u][i]][s4][s2]}
  • 其中 f a [ u ] [ i ] fa[u][i] fa[u][i] 表示 u u u 2 i 2^i 2i 级祖先
  • 询问时分三种情况
  • (1) a a a b b b 相邻且 x = y = 0 x=y=0 x=y=0 ,输出 − 1 -1 1
  • (2) a a a b b b 的祖先(如果 b b b a a a 的祖先则交换 a a a b b b 并交换 x x x y y y
  • ①处理 b b b 的子树:为答案贡献 f [ b ] [ y ] f[b][y] f[b][y]
  • ②处理 a a a 的子树之外:如果 x = 1 x=1 x=1 则为答案贡献 min ⁡ ( h [ a ] [ 0 ] , h [ a ] [ 1 ] ) \min(h[a][0],h[a][1]) min(h[a][0],h[a][1]) 否则贡献 h [ a ] [ 1 ] h[a][1] h[a][1]
  • ③处理 a a a 的子树(不包括包含 a a a 的子树包含 b b b 的子节点的子树):如果 x = 1 x=1 x=1 则为答案贡献 f [ a ] [ 1 ] − min ⁡ ( f [ d ] [ 0 ] , f [ d ] [ 1 ] ) f[a][1]-\min(f[d][0],f[d][1]) f[a][1]min(f[d][0],f[d][1]) ,否则贡献 f [ a ] [ 0 ] − f [ d ] [ 1 ] f[a][0]-f[d][1] f[a][0]f[d][1]
  • d d d b b b 向上跳 d e p [ b ] − d e p [ a ] dep[b]-dep[a] dep[b]dep[a] 后到达的点,可以用倍增找到
  • d e p [ u ] dep[u] dep[u] 表示 u u u 的深度)
  • ④也是最重要的情况: a a a b b b 的路径
  • 回忆刚刚定义的 s u m [ u ] [ i ] [ 0 / 1 ] [ 0 / 1 ] sum[u][i][0/1][0/1] sum[u][i][0/1][0/1] ,我们可以使用二进制分组的思想
  • a a a b b b 之间(不包括 a a a b b b )一共 d e p [ b ] − d e p [ a ] − 1 dep[b]-dep[a]-1 dep[b]dep[a]1 个点拆成不超过 O ( log ⁡ n ) O(\log n) O(logn)
  • 利用预处理出的 s u m [ u ] [ i ] [ 0 / 1 ] [ 0 / 1 ] sum[u][i][0/1][0/1] sum[u][i][0/1][0/1] 倍增合并,得到 a a a b b b 的路径的贡献
  • 注意需要考虑在 x x x y y y 的限制下路径的首尾是否能选
  • (3) a a a b b b 的 LCA 不是 a a a b b b
  • 枚举 LCA 选或不选,拆成 a a a 到 LCA 和 b b b 到 LCA 两条路径进行判断
  • 和(2)基本相同
  • 复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
  • 由于 s u m sum sum 合并时有 16 16 16 的常数,所以实际效率不高
  • 如果这题出成省选题,怕不是会要限制多个点,然后虚树 DP

码农报告

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)
#define Tree(u) for (int e = adj[u], v; e; e = nxt[e]) if ((v = go[e]) != fu)

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

typedef long long ll;

template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}

template <class T>
inline void Swap(T &a, T &b) {a ^= b; b ^= a; a ^= b;}

const ll INF = 1ll << 61;

const int N = 1e5 + 5, M = N << 1, LogN = 20;

int n, m, ecnt, nxt[M], adj[N], go[M], p[N], dep[N], fa[N][LogN], tot, sons[N];
ll f[N][2], g[N][2], h[N][2], as[N], bs[N], sumla[N], sumra[N], sumlb[N], sumrb[N];

struct node
{
	ll a[2][2];
	
	void init()
	{
		a[0][0] = a[1][1] = a[0][1] = a[1][0] = INF;
	}
	
	friend inline node operator + (node a, node b)
	{
		int i, j, k, h;
		node res; res.init();
		For (i, 0, 1) For (j, 0, 1) For (k, 0, 1) For (h, 0, 1)
			if (k || h) res.a[i][j] = Min(res.a[i][j], a.a[i][k] + b.a[h][j]);
		return res;
	}
} sum[N][LogN];

void add_edge(int u, int v)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}

char s[22];

void dfs(int u, int fu)
{
	dep[u] = dep[fu] + 1;
	f[u][0] = 0; f[u][1] = p[u];
	fa[u][0] = fu;
	Tree(u)
	{
		dfs(v, u);
		f[u][0] += f[v][1];
		f[u][1] += Min(f[v][0], f[v][1]);
	}
	Tree(u)
	{
		g[v][0] = f[u][0] - f[v][1];
		g[v][1] = f[u][1] - Min(f[v][0], f[v][1]);
	}
}

void dfs2(int u, int fu)
{
	int i;
	tot = 0;
	Tree(u)
	{
		as[++tot] = f[v][1];
		bs[tot] = Min(f[v][0], f[v][1]);
		sons[tot] = v;
	}
	sumla[0] = sumra[tot + 1] = sumlb[0] = sumrb[tot + 1] = 0;
	For (i, 1, tot)
	{
		sumla[i] = sumla[i - 1] + as[i];
		sumlb[i] = sumlb[i - 1] + bs[i];
	}
	Rof (i, tot, 1)
	{
		sumra[i] = sumra[i + 1] + as[i];
		sumrb[i] = sumrb[i + 1] + bs[i];
	}
	For (i, 1, tot)
	{
		int v = sons[i];
		h[v][0] = sumla[i - 1] + sumra[i + 1] + h[u][1];
		h[v][1] = sumlb[i - 1] + sumrb[i + 1] + Min(h[u][0], h[u][1]) + p[u];
	}
	Tree(u) dfs2(v, u);
}

void bz(int u, int fu)
{
	int i, j, h, k, w;
	sum[u][0].a[0][0] = g[u][0];
	sum[u][0].a[0][1] = sum[u][0].a[1][0] = INF;
	sum[u][0].a[1][1] = g[u][1];
	For (i, 0, 17)
	{
		fa[u][i + 1] = fa[fa[u][i]][i];
		sum[u][i + 1] = sum[u][i] + sum[fa[u][i]][i];
	}
	Tree(u) bz(v, u);
}

int lca(int u, int v)
{
	if (dep[u] < dep[v]) Swap(u, v);
	int i;
	Rof (i, 18, 0)
	{
		if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
		if (u == v) return u;
	}
	Rof (i, 18, 0)
		if (fa[u][i] != fa[v][i])
			u = fa[u][i], v = fa[v][i];
	return fa[u][0];
}

int jmp(int u, int dis)
{
	int i;
	Rof (i, 18, 0)
		if ((dis >> i) & 1) u = fa[u][i];
	return u;
}

node anc(int u, int dis)
{
	int i, tmp = -1;
	node res;
	dis++;
	Rof (i, 18, 0)
		if ((dis >> i) & 1)
		{
			if (tmp == -1) tmp = 0, res = sum[u][i];
			else res = res + sum[u][i];
			u = fa[u][i];
		}
	return res;
}

int main()
{
	int i, j, k, a, b, x, y;
	n = read(); m = read();
	scanf("%s", s + 1);
	For (i, 1, n) p[i] = read();
	For (i, 1, n - 1) x = read(), y = read(), add_edge(x, y);
	dfs(1, 0); dfs2(1, 0); bz(1, 0);
	while (m--)
	{
		a = read(); x = read(); b = read(); y = read();
		if ((fa[a][0] == b || fa[b][0] == a) && !x && !y)
		{
			puts("-1");
			continue;
		}
		int c = lca(a, b);
		if (c == a || c == b)
		{
			if (c == b) Swap(a, b), Swap(x, y);
			ll ans = x ? Min(h[a][0], h[a][1]) : h[a][1];
			ans += f[b][y];
			if (fa[a][0] != b && fa[b][0] != a)
			{
				node tmp = anc(b, dep[b] - dep[a] - 2);
				ll delta = INF;
				For (i, 0, 1)
				{
					if (!y && !i) continue;
					For (j, 0, 1)
					{
						if (!x && !j) continue;
						delta = Min(delta, tmp.a[i][j]);
					}
				}
				ans += delta;
			}
			int d = jmp(b, dep[b] - dep[a] - 1);
			if (x) ans += f[a][1] - Min(f[d][0], f[d][1]);
			else ans += f[a][0] - f[d][1];
			printf("%lld\n", ans);
		}
		else
		{
			ll ans = INF;
			For (i, 0, 1)
			{
				ll res = f[a][x] + f[b][y]
					+ (i ? Min(h[c][0], h[c][1]) : h[c][1]);
				if ((fa[a][0] == c && !x && !i) ||
					(fa[b][0] == c && !y && !i)) continue;
				if (fa[a][0] != c)
				{
					node tmp = anc(a, dep[a] - dep[c] - 2);
					ll delta = INF;
					For (j, 0, 1)
					{
						if (!x && !j) continue;
						For (k, 0, 1)
						{
							if (!i && !k) continue;
							delta = Min(delta, tmp.a[j][k]);
						}
					}
					res += delta;
				}
				if (fa[b][0] != c)
				{
					node tmp = anc(b, dep[b] - dep[c] - 2);
					ll delta = INF;
					For (j, 0, 1)
					{
						if (!y && !j) continue;
						For (k, 0, 1)
						{
							if (!i && !k) continue;
							delta = Min(delta, tmp.a[j][k]);
						}
					}
					res += delta;
				}
				int d = jmp(a, dep[a] - dep[c] - 1),
					e = jmp(b, dep[b] - dep[c] - 1);
				if (i) res += f[c][1] - Min(f[d][0], f[d][1]) - Min(f[e][0], f[e][1]);
				else res += f[c][0] - f[d][1] - f[e][1];
				ans = Min(ans, res);
			}
			printf("%lld\n", ans);
		}
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值