区间 DP
思路:按区间的 \(len\) 从小到大 DP,枚举左端点,算出右端点,再枚举中间的分界点转移
有可能是向左右两端各扩展 \(1\) 步
还有时要记录在左/右端点,分开转移
把问题划分为区间长度更短的最优子结构
注意 \(len=1\) 时初始化及边界
这题已经说了去掉一条边后执行计算,明显提示要断环为链
枚举断开的边,然后转化为序列上的问题,容易想到区间 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;
}
发现最终一定有一种方案满足值均在 \(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;
}
由于代价与人的位置有关,因此记录 \(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;
}