区间 DP

思路:按区间的 \(len\) 从小到大 DP,枚举左端点,算出右端点,再枚举中间的分界点转移

有可能是向左右两端各扩展 \(1\)

还有时要记录在左/右端点,分开转移

把问题划分为区间长度更短的最优子结构

注意 \(len=1\) 时初始化及边界

  1. P4342 [IOI1998]Polygon

这题已经说了去掉一条边后执行计算,明显提示要断环为链

枚举断开的边,然后转化为序列上的问题,容易想到区间 DP

\(f(i,j)\) 为边的区间 \((i,j)\) 断开后能算出的最大值

加法最大能由加法的最大值转移过来,但乘法会出现负负得正这种情况导致最小值变最大值

那就再记录 \(g(i,j)\) 为最小值

转移时不用人工分类讨论,直接丢掉 \(\min/\max\) 函数中

注意边界情况,当断开某个区间最边上的边时要涉及旁边单独的数,需要单独处理

code:

typedef long long ll;
ll n, a[110], f[110][110], g[110][110], d, mx = -1e15, lsh;
char ch, b[110];
vector<ll> lin; // f[i][j] max(i~j)	g[i][j] min(i~j)
inline ll work(ll l, ll r)
{
	memset(f, 0xcf, sizeof(f)), memset(g, 0x3f, sizeof(g));
	for(reg ll i = 1; i <= n * 2; ++i)	
	{
		if(b[i] == 't')	f[i][i] = g[i][i] = a[i - 1] + a[i];
		else	f[i][i] = g[i][i] = a[i - 1] * a[i];
	}
	for(reg ll i = 2; i < n; ++i)
	{
		for(reg ll j = l; j <= r - i + 1; ++j)
		{
			reg ll k = j + i - 1;
			if(b[j] == 't')	f[j][k] = max(f[j][k], a[j - 1] + f[j + 1][k]), g[j][k] = min(g[j][k], a[j - 1] + g[j + 1][k]);
			else
			{
			    f[j][k] = max({f[j][k], a[j - 1] * f[j + 1][k], a[j - 1] * g[j + 1][k]});
				g[j][k] = min({g[j][k], a[j - 1] * f[j + 1][k], a[j - 1] * g[j + 1][k]});	
			}
			if(b[k] == 't') f[j][k] = max(f[j][k], a[k] + f[j][k - 1]), g[j][k] = min(g[j][k], a[k] + g[j][k - 1]);
			else
			{
				f[j][k] = max({f[j][k], a[k] * f[j][k - 1], a[k] * g[j][k - 1]});
				g[j][k] = min({g[j][k], a[k] * f[j][k - 1], a[k] * g[j][k - 1]});
			}
			for(reg ll u = j + 1; u < k; ++u)
			{ 
			    if(b[u] == 't')
			    {
			    	f[j][k] = max(f[j][k], f[j][u - 1] + f[u + 1][k]); 
					g[j][k] = min(g[j][k], g[j][u - 1] + g[u + 1][k]);
				}
			    else
			    {
			    	f[j][k] = max({f[j][k], f[j][u - 1] * f[u + 1][k], g[j][u - 1] * g[u + 1][k], f[j][u - 1] * g[u + 1][k], g[j][u - 1] * f[u + 1][k]});
			    	g[j][k] = min({g[j][k], g[j][u - 1] * f[u + 1][k], f[j][u - 1] * g[u + 1][k], g[j][u - 1] * g[u + 1][k], f[j][u - 1] * f[u + 1][k]});
				}
			}
		}
	}
	return f[l][r];
}
int main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n;
	for(reg ll i = 1; i <= 2 * n; ++i)
	{
		if(i & 1)	cin >> ch, b[i / 2 + 1] = ch;
		else	cin >> d, a[i / 2] = d;
	}
	for(reg ll i = n + 1; i <= 2 * n; ++i)	a[i] = a[i - n], b[i] = b[i - n];
	for(reg ll i = 1; i <= n; ++i)
	{
		lsh = work(i + 1, n + i - 1);
		if(lsh > mx)	lin.clear(), lin.pb(i), mx = lsh;
		else if(lsh == mx)	lin.pb(i);
	}
	cout << mx << endl;
	for(reg ll i = 0; i < (ll)lin.size(); ++i)	cout << lin[i] << " ";
	return 0;
}
  1. P3592 [POI2015] MYJ

发现最终一定有一种方案满足值均在 \(c_i\) 中出现,值域降至 \(O(m)\)

转移跟区间的最小值有关,设 \(f(l,r,i)\) 表示 \([l,r]\) 最小值为 \(i\) 时,完全包含在区间 \([l,r]\) 中的询问产生的最大收益

枚举最小值取到的地方为中间断点,两边的最小值 \(\ge i\),处理跨过最小值的区间贡献,按 \(c_i\) 排序后用指针移动 \(O(1)\) 计算

会超时,可以预处理后缀最大值优化转移

同时记录转移点,递归还原方案

复杂度 \(O(n^3m)\)

void find(int l, int r, int val) // 找 [l,r],最小值至少为 val时取哪个为最小值,递归找方案 
{
	if(l > r)	return;
	int mxa = 0, mn = 0;
	for(int i = val; i <= cnt; ++i)
		if(f[l][r][i] >= mxa)	mxa = f[l][r][i], mn = i;
	a[fr[l][r][mn]] = lsh[mn];
	find(l, fr[l][r][mn] - 1, mn), find(fr[l][r][mn] + 1, r, mn);
}
int main()
{
	n = read(), m = read();
	for(int i = 1; i <= m; ++i)
	{
		p[i].a = read(), p[i].b = read(), p[i].c = read(), lsh[i] = p[i].c, p[i].id = i;
		for(int j = p[i].a; j <= p[i].b; ++j)	crs[j].pb(i);
	}
	sort(lsh + 1, lsh + m + 1), cnt = unique(lsh + 1, lsh + m + 1) - (lsh + 1);
	for(int i = 1; i <= m; ++i)	p[i].num = lower_bound(lsh + 1, lsh + cnt + 1, p[i].c) - lsh;
	for(int i = 1; i <= m; ++i)
		if(p[i].a == p[i].b)
		{
			for(int j = 1; j <= p[i].num; ++j)	f[p[i].a][p[i].b][j] += lsh[j];
			for(int j = p[i].num; j > 0; --j)	
				mx[p[i].a][p[i].b][j] = max(f[p[i].a][p[i].b][j], mx[p[i].a][p[i].b][j + 1]);
		}
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= cnt; ++j)	fr[i][i][j] = i;
	for(int len = 2; len <= n; ++len)
	{
		for(int l = 1; l + len - 1 <= n; ++l)
		{
			int r = l + len - 1;
			for(int k = l; k <= r; ++k) // 枚举最小值所在位置 
			{
				idx = 0;
				for(int i : crs[k])	
					if(p[i].a >= l && p[i].b <= r)	lin[++idx] = i;
				sort(lin + 1, lin + idx + 1, cmp); 
				for(int i = 1, j = 1; i <= cnt; ++i) // 枚举最小值为 lsh[i] 
				{
					int tot = 0; 
					while(j <= idx && p[lin[j]].num < i)	++j;
					if(k > l)	tot += mx[l][k - 1][i];
					if(k < r)	tot += mx[k + 1][r][i];
					if(f[l][r][i] <= tot + (idx - j + 1) * lsh[i]) 
					{
						fr[l][r][i] = k; // 记录转移点 
						f[l][r][i] = tot + (idx - j + 1) * lsh[i];
					}
				}
			}
			for(int i = cnt; i > 0; --i)	mx[l][r][i] = max(mx[l][r][i + 1], f[l][r][i]);
		}
	}
	printf("%d\n", mx[1][n][1]);
	find(1, n, 1);
	for(int i = 1; i <= n; ++i)	printf("%d ", a[i]);
	return 0;
}
  1. P1220 关路灯

由于代价与人的位置有关,因此记录 \(f(l,r,0/1)\) 表示关掉 \([l,r]\) 中的路灯,最后人在左/右端点的最小代价

而且因为跨过一段一定不优,人在走过去的同时完全可以顺便把经过的灯关掉,可以看作一次多处理一盏灯,只需向两边扩展即可

注意代价的计算要携带上没有关掉的灯,初始时只关了 \(c\) 处的灯

复杂度 \(O(n^2)\)\(n\) 后可以加 \(2\)\(0\)

int main()
{
	n = read(), c = read();
	for(int i = 1; i <= n; ++i)	a[i] = read(), b[i] = read(), sum[i] = sum[i - 1] + b[i];
	memset(f, 0x3f, sizeof(f));
	f[c][c][0] = f[c][c][1] = 0;
	for(int i = c; i > 0; --i)
	{
		for(int j = (i == c) ? c + 1 : c; j <= n; ++j)
  		{
  			f[i][j][0] = min(f[i][j][0], f[i + 1][j][0] + (a[i + 1] - a[i]) * (sum[n] - sum[j] + sum[i]));
  			f[i][j][0] = min(f[i][j][0], f[i + 1][j][1] + (a[j] - a[i]) * (sum[n] - sum[j] + sum[i]));
  			f[i][j][1] = min(f[i][j][1], f[i][j - 1][0] + (a[j] - a[i]) * (sum[n] - sum[j - 1] + sum[i - 1]));
  			f[i][j][1] = min(f[i][j][1], f[i][j - 1][1] + (a[j] - a[j - 1]) * (sum[n] - sum[j - 1] + sum[i - 1]));
        }
	}
	printf("%d", min(f[1][n][0], f[1][n][1]));
	return 0;
}
posted @ 2024-02-14 22:23  KellyWLJ  阅读(3)  评论(0编辑  收藏  举报