Loading

题目归档 #1

目录

  • [Luogu P6059] 纯粹容器
  • [JSOI2015] Salesman
  • [HNOI2007] 梦幻岛宝珠

[Luogu P6059] 纯粹容器

一道 Luogu 的个人公开赛题目,感觉这道题应该比较经典。

题意

  • \(n\) 个容器排成了一队,从左到右依次编号为 1∼n。第 \(i\) 个容器的强度为 \(a_i\) 且互不相同。为了挑选出最纯粹的容器,进行 \(n-1\) 轮操作,每轮操作中,等概率随机挑选两个 位置相邻未被击倒 的容器,令它们进行决斗,在一次决斗中,强度较小的容器将会被击倒并移出队列。
  • 显然最后留下的是强度最大的容器,但是,可怜的容器们很想知道自己能够活多久,于是,它们请你对每个容器求出它存活轮数的期望。答案对 \(998244353\) 取模。
  • \(n \leq 50\)

解题

首先我们很快可以看出一个结论:显然容器存活时间只和 两边离他最近的强度大于他的容器 的位置有关。至于周围其他的容器是什么强度对答案没有影响。

然后开始想。一开始我以为直接推出公式算(一看就是期望 dp 做少了),于是开始从贡献的角度去考虑。先算出一个容器两边消完时间的期望再合并答案。

然而并不行。推了两个小时都没推出个所以然。而后看题解,原来就是期望 dp。

对于每个位置做一次 dp,大概如下:(令这个位置为 \(u\),两边离他最近的强度大于他的容器位置分别为 \(pre\)\(nex\)
\(f[k][i][j]\) 表示第 \(k\)\(u\) 左侧直到 \(pre\) 还有 \(i\) 个容器完好,右侧则为 \(j\) 个容器。转移显然为 \(f[k][i][j]=f[k-1][i][j]\times\frac{n-k-i-j}{n-k} + f[k-1][i+1][j] \times \frac{i+1}{n-k} + f[k-1][i][j+1] \times \frac{j+1}{n-k}\)

注意当 \(pre = 0\)或者 \(nex = N+1\) (即某边没有强度大于他的容器)时需要特殊处理,体现为转移方程的分子要进行微调。详细见代码,注意取模要用到逆元。

程序

#include <iostream>
#include <cstring>
#include <cstdio>

#define Maxn 55
#define LL long long

using namespace std;

const LL MOD = 1ll * 998244353;

int read() {
	int x = 0, f = 1;
	char c = getchar();
	while(c < '0' || c > '9') {
		if(c == '-') f = -1;
		c = getchar();
	}
	while('0' <= c && c <= '9') {
		x = x * 10 + c - '0';
		c = getchar();
	}
	return x * f;
}

int N, a[Maxn], pre[Maxn], nex[Maxn];
LL f[Maxn][Maxn][Maxn], ans;

LL Pow(int a, int b) {
	LL res = 1, base = a;
	while(b) {
		if(b & 1) res = res * base % MOD;
		base = base * base % MOD;
		b >>= 1;
	}
	return res;
}

int main() {
	N = read();
	for(int i = 1; i <= N; ++i) a[i] = read();
	for(int i = 1; i <= N; ++i) {
		pre[i] = i, nex[i] = N - i + 1;
		for(int j = i - 1; j >= 1; --j) if(a[i] < a[j]) {pre[i] = i - j; break;}
		for(int j = i + 1; j <= N; ++j) if(a[i] < a[j]) {nex[i] = j - i; break;}
	}
	for(int u = 1; u <= N; ++u) {
		memset(f, 0, sizeof(f));
		f[0][pre[u]][nex[u]] = 1;
		ans = 0;
		for(int k = 1; k < N; ++k) {
			LL inv = Pow(N - k, MOD - 2) % MOD;
			for(int i = 1; i <= pre[u]; ++i)
			 for(int j = 1; j <= nex[u]; ++j) {
			 	if(i + j > N - k - 1 + (pre[u] == u) + (nex[u] == N - u + 1)) continue;
			 	if(pre[u] == u) f[k][i][j] = f[k - 1][i + 1][j] * i % MOD;
			 	else f[k][i][j] = f[k - 1][i + 1][j] * (i + 1) % MOD;
			 	if(nex[u] == N - u + 1) f[k][i][j] += f[k - 1][i][j + 1] * j % MOD;
			 	else f[k][i][j] += f[k - 1][i][j + 1] * (j + 1) % MOD;
			 	f[k][i][j] += f[k - 1][i][j] * (N - k - i - j + (pre[u] == u) + (nex[u] == N - u + 1)) % MOD;
				f[k][i][j] = f[k][i][j] * inv % MOD;
				ans += f[k][i][j]; ans %= MOD;
			//	cout << u << " " << k << " " << i << " " << j << " " << f[k][i][j] << endl;
			 }
		}
		printf("%lld ", ans);
	}
	return 0;
} 

[JSOI2015] Salesman

一道比较考察细节处理能力的 dp 题目。

题意

  • 一个 \(N\) 个点的树。某个点有一个可正可负的点权以及经过次数的限制。现在从 \(1\) 号点出发,可以自行规划路径,最终回到 \(1\) 号点。你的得分为途中经过所有点的点权之和。求得分最大值。特别地,\(1\) 号点可经过无数次,且其点权为 \(0\)
  • Task 1:得分的最大值?
  • Task 2:使得分最大化的路径是否唯一存在?(Yes/No)
  • \(n \leq 10^5\),所有点经过次数限制均一定不小于 \(2\)

解题

这道题要 easy 一些了。显然我们可以得出一个规律:如果你只能经过一个点 \(x\) 次,那么你可以选择这个点的 \(x-1\) 个子树经过。至此「树形 DP」和「贪心」都很明显了。对于每个点,贪心地选取前 \(x-1\) 个儿子的 dp 值并计入该节点的 dp 值。

至于 Task 2,对于某个节点,如果第 \(x-1\) 大的子树的 dp 值与第 \(x\) 大的子树的 dp 值相等,那么路径就不唯一,因为选两棵子树走下去都可以。然后把信息更新到其父节点即可。

程序

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>

#define Maxn 100010
#define Maxm 100010

using namespace std;

int read() {
	int x = 0, f = 1;
	char c = getchar();
	while(c < '0' || c > '9') {
		if(c == '-') f = -1;
		c = getchar();
	}
	while('0' <= c && c <= '9') {
		x = x * 10 + c - '0';
		c = getchar();
	}
	return x * f;
}

int N, val[Maxn], lim[Maxn];

struct Edge {
	int next, to;
}
edge[Maxm * 2];
int head[Maxn], edge_num;

void add_edge(int from, int to) {
	edge[++edge_num].next = head[from];
	edge[edge_num].to = to;
	head[from] = edge_num;
}

struct Node {
	int data;
	bool uni;
}
node[Maxn];
int tot, dp[Maxn], flag[Maxn];

int comp(Node x, Node y) {
	return x.data > y.data;
}

void dfs(int u, int f) {
	int tot = 0;
	dp[u] = val[u];
	for(int i = head[u]; i; i = edge[i].next) {
		int v = edge[i].to;
		if(v == f) continue;
		dfs(v, u);
	}
	for(int i = head[u]; i; i = edge[i].next) {
		int v = edge[i].to;
		if(v == f) continue;
		node[++tot].data = dp[v];
		node[tot].uni = flag[v];
	}
	sort(node + 1, node + tot + 1, comp);
	int rec_i = 0;
	for(int i = 1; i <= lim[u] && i <= tot && node[i].data > 0; ++i) {
		dp[u] += node[i].data;
		flag[u] |= node[i].uni;
		rec_i = i;
	}
	if(rec_i != tot && (node[rec_i].data == node[rec_i + 1].data || (node[rec_i + 1].data == 0 && rec_i != lim[u] - 1))) 
		flag[u] = 1;
}

int main() {
	N = read();
	val[1] = 0, lim[1] = N;
	for(int i = 2; i <= N; ++i) val[i] = read();
	for(int i = 2; i <= N; ++i) lim[i] = read() - 1;
	int u, v;
	for(int i = 1; i < N; ++i) {
		u = read(); v = read();
		add_edge(u, v);
		add_edge(v, u);
	}
	dfs(1, 0);
//	for(int i = 1; i <= N; ++i) cout << dp[i] << endl;
	cout << dp[1] << endl; 
	if(flag[1]) cout << "solution is not unique";
	else cout << "solution is unique";
	return 0;
}

[HNOI2007] 梦幻岛宝珠

很有意思的一个背包问题变种。细节堪称工艺品级别的动态规划,和二进制联系紧密但并非如同多重背包问题的二进制分组。

题意

  • 同「01背包问题」,但是在数据范围上有变化:
  • 总容量 \(W \leq 2^{30}\)。保证单个物品的体积 \(w_i\) 可以拆分为 \(a \times 2^b\) 的形式,其中 \(a \leq 10, b \leq 30\)

解题

很明显这道题要用到如上性质,大多数人第一反应肯定是二进制分组。没错,但是我们该如何分组呢?

首先我们一定可以把每个 \(w_i\) 拆分为 \(a \times 2^b\) 的形式,且使得 \(a\) 为奇数。如果忽视掉 \(b\),只考虑 \(a\),显然这个「01背包」大家都会做。

那么何不先对于每个物品根据其体积拆分后的 \(b\) 来分组呢?显然最多只有 \(31\) 组。对这 \(31\) 组暴力完成「01背包」,我们可以得到一个数组 \(f[i][j]\)。表示如果仅使用 \(b=i\) 的物品,在容积为 \(j \times 2^i\) 下的最大价值。

显然现在的问题就是如何合并不同的 \(i\)(说 \(b\) 其实也可以)。首先我们现在要改变 \(f[i][j]\) 的定义——现在 \(f[i][j]\) 表示在 \(2^0 \sim 2^i\) 这几个组内,容量为 \(j \times 2^i + q\) 的最大价值。其中 \(q\) 代表二进制表示 \(W\)\(0\sim i-1\) 这几位的部分。例如 \(W=01001\underline{01101}\),当 \(i=5,j=3\)时,\(q\) 即为下划线部分,容量为 \(1101101\)

接着给出状态转移方程:

\(f[i][j] = max(f[i][j], f[i][j - k] + f[i - 1][min(val[i - 1], k * 2 + ((W >> (i - 1)) & 1))])\)

其中,\(W\) 即为总容积,\(val[i]\) 表示最初我们把每个 \(w_i\) 拆分为 \(a \times 2^b\) 的形式后,\(2^i\) 的系数之和(即若干个 \(a\) 的和)。

其中 \(i\) 是最外层循环,\(j\) 是中层循环(并且采用 倒序循环),\(k\) 是内层循环,从 \(1\) 循环到 \(j\)\(k\) 的存在实际上有一种「借位」的思想。肉眼可见,从 \(f[i][j]\) 中借走了个 \(k\) ,在 \(f[i-1][...]\) 中自然要加上 \(k \times 2\)。而后面的 \(((W >> (i - 1)) & 1)\) 则是为了判断第 \(i-1\) 位是否为 \(1\)。为 \(1\) 则还要再加个 \(1\),因为这是原本在 \(f[i-1][...]\) 中就存在的。

由于中层倒序循环,\(f[i][j-k]\) 尚且还在旧定义中,即 \(2^0 \sim 2^i\) 这几个组内,容量为 \(j \times 2^i + q\) 的最大价值。而后面的 \(f[i-1][...]\) 已经是新定义了,所以它就包含了它本身的旧定义以及之前所说的 \(q\) 那一部分,凑起来刚好是 \(f[i][j]\)。中途对 \(val[i-1]\)\(\min\),是为了确认范围的合法。

大概就是这些。细节请移步代码。

程序

#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>

#define Maxn 110
#define Maxa 12
#define Maxb 36

using namespace std;

int read() {
	int x = 0, f = 1;
	char c = getchar();
	while(c < '0' || c > '9') {
		if(c == '-') f = -1;
		c = getchar();
	}
	while('0' <= c && c <= '9') {
		x = x * 10 + c - '0';
		c = getchar();
	}
	return x * f;
}

int N, W, tot, f[Maxb][Maxn * Maxa], val[Maxb];

vector <int> C[Maxb];
vector <int> V[Maxb];

void init() {
	tot = 0;
	memset(f, 0, sizeof(f));
	memset(val, 0, sizeof(val));
	memset(C, 0, sizeof(C));
	memset(V, 0, sizeof(V));
}

int main() {
	while(true) {
		N = read(); W = read();
		if(N == -1 && W == -1) break;
		init();
		int w, v;
		for(int i = 1; i <= N; ++i) {
			w = read(); v = read();
			int x = 0;
			while(!(w & 1)) ++x, w >>= 1;
			val[x] += w; tot = max(tot, x);
		//	cout << "x " << x << "w " << w << endl;
			C[x].push_back(w);
			V[x].push_back(v);
		}
		for(int x = 0; x <= tot; ++x) {
			for(int i = 0; i < C[x].size(); ++i) {
				for(int j = val[x]; j >= C[x][i]; --j) {
					f[x][j] = max(f[x][j], f[x][j - C[x][i]] + V[x][i]);
				}
			}
		}
		tot = 0;
		while(W >> tot) ++tot;
		--tot;
		for(int x = 1; x <= tot; ++x) {
			val[x] += (val[x - 1] + 1) / 2;
			for(int j = val[x]; j >= 0; --j)
				for(int k = 0; k <= j; ++k)
					f[x][j] = max(f[x][j], f[x][j - k] + f[x - 1][min(val[x - 1], k * 2 + ((W >> (x - 1)) & 1))]);
		}
		printf("%d\n", f[tot][1]);
	}
	return 0;
} 
posted @ 2020-08-02 10:30  Sqrtyz  阅读(69)  评论(0)    收藏  举报