【题解】Luogu-P2672 推销员
貌似这题代码可以很短欸
但是我自作孽写线段树
题目链接:P2672 推销员
不过为什么会写线段树呢,其实是因为我看了标签。
申明:本题解较适合喜欢使用数据结构的 $OIers$,并且码量较其他方法来说比较大
进入正题
可以证明每一次选择都基于上一次的选择,这个结论已经有巨佬详细地证明了,这里就不再详细说明了 或者说是我不会
这道题我选择倒过来做,大致思路:
- $S$ 值是从小到大排好序的(所以下文中下标更大即意味着 $S$ 值更大),可以不用管。
- 当每一个用户的家都需要去的时候,也就是 $X = N$ 时,答案显然是所有 $A$ 值的和加上 $Smax * 2$。又因为每一次的选择都基于上一次,也就是每一次选择都可以推导出上一次选择。
- 我们在求每一个状态的时候,关注的只有目前 $A$ 值的和、目前选择的点中 $S$ 的最大值 以及 当 $S$ 最大时点的下标。又因为 $A$ 值的和可以由上一个答案与目前选择的点得到,所以可以不维护。
那么,我们只需要逐步删除选中的点,并保证局部最优。这个答案一定就是所求。
以下用 $l$、$r$表示 目前选中的点中下标最小的点的下标 和 目前选中的点中下标最大的点的下标(因此 $l$,$r$ 之间有可能会有没有选中的点),$dis$ 表示 $r$ 点的 $S$ 值的两倍(即来回距离)
这样的话,对于每一个状态,我们都有两种选择来指向两个不同的上一状态,在中间取最优值:
- 删除 $l$ ~ $r - 1$ 中 $A$ 值最小的点 $v$,这样会尽量让答案更大,此时答案只需减去 $A_v$ 即可
- 删除 $r$,这会导致右端点的变化(左移),因此 $dis$ 也会发生改变,同时,答案会减去 $A_r$
最后两个方案的答案取 $max$ 并且更新状态即可。
所以我们的线段树用在哪里呢。。。终于讲到正题了
在方案 $1$ 中,我们好像要求 $l$ ~ $r - 1$ 中 $A$ 的 $min$ 值和它的下标?
线段树!
是的,线段树就用在这里 也只用在这里
所以我们的线段树就只需要维护区间最小值和区间最小值下标即可,加一个单点修改
但是当有两个点的值均为区间最小,那我们的下标应该取哪一个呢?
显然是下标更小的那一个,因为这样可以使留下来的点的 $S$ 更大, $dis$ 更大
而删除一个点之后,我们应该把其 $A$ 值变成 $INF$,这样就不会取到了。但这个操作只需要在方案 $1$ 更优的时候进行,因为在方案 $2$ 中,我们向左更新了 $r$ 的值,又所有的线段树操作都会在 $l$ ~ $r - 1$ 中进行,所以根本就不会再得到之前的 $r$ 点了
而当我们选择了方案 $1$ ,删除 $r$ 后,哪个点来“接替” $r$ 的位置呢?显然不能是 $r$ 上一个点,因为它或许没有被选中,如果选中它,则违背了题目中 “不走多余的路的前提”
所以我们还需要维护选中的点中每一个点的前一个点 绕口令
链表!
维护每个点的上一个节点即可
好的问题解决了 看起来很简单的样子
还是分几个部分讲解好了
变量声明
#define pii pair<int,int>//这里用pair来存区间min值和min值下标
const int MAXN = 1e5 + 10;
const int INF = 1e9;
int n,pre[MAXN],nxt[MAXN];//pre、nxt:链表
int seg[MAXN << 2],p[MAXN << 2];//seg:线段树区间min,p:线段树区间min下标
int ans[MAXN],l,r;//ans:x为不同数值时的答案数组
还有一个结构体
struct house {
int a,s;
bool operator < (const house &b) const {return a < b.a;}
} node[MAXN];
跟题目中的一样
链表
void Delete(int pos) {
pre[nxt[pos]] = pre[pos];
nxt[pre[pos]] = nxt[pos];
}//这是一个极其简单的删除节点函数。。。
没啥讲的
线段树
#define l ll//懒得换变量名了,就define一下
#define r rr//到时候会undef
void pushup(int root) {//更新
if(seg[root << 1 | 1] < seg[root << 1]) {//如果右子树更优
seg[root] = seg[root << 1 | 1];//只能存右子树的答案了
p[root] = p[root << 1 | 1];
} else {
seg[root] = seg[root << 1];//否则存左子树,这样可以让下标尽量小
p[root] = p[root << 1];
}
}
void build(int root,int l,int r) {//简单的建树
if(l == r) {
seg[root] = node[l].a;
p[root] = l;
return;
}
int mid = (l + r) >> 1;
build(root << 1,l,mid);
build(root << 1 | 1,mid + 1,r);
pushup(root);
}
void update(int root,int l,int r,int pos,int val) {//简单的更新
if(l == r && l == pos) {
seg[root] = val;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) update(root << 1,l,mid,pos,val);
else update(root << 1 | 1,mid + 1,r,pos,val);
pushup(root);
}
pii getmin(pii &a,pii b) {//类似于min函数,用来找两个pii中最小值较小的那个(即当前的返回值)
if(b.first < a.first) swap(a,b);
}
pii query(int root,int l,int r,int L,int R) {
if(L <= l && r <= R) return make_pair(seg[root],p[root]);
int mid = (l + r) >> 1;
pii res = make_pair(INF,INF);
if(L <= mid) getmin(res,query(root << 1,l,mid,L,R));//这里用到了getmin,可以自己研究一下
if(R > mid) getmin(res,query(root << 1 | 1,mid + 1,r,L,R));
return res;
}
#undef l
#undef r//说了会undef
主函数
int main () {
ios::sync_with_stdio(0);//优化
cin >> n;
for(register int i = 1;i <= n; ++i)
cin >> node[i].s;
for(register int i = 1;i <= n; ++i) {
cin >> node[i].a;
pre[i] = i - 1;
nxt[i] = i + 1;//链表初始化
ans[n] += node[i].a;//最终答案
}
build(1,1,n);//建树
l = 1,r = n;//左右端点(实际只有 r 有用)
ans[n] += node[r].s * 2;//答案要加上dis
for(register int i = n - 1;i >= 1; --i) {//倒推答案
pii tmp = query(1,1,n,l,r - 1);//方案1 中用线段树
int ans1 = ans[i + 1] - tmp.first;//方案1
int ans2 = ans[i + 1] - node[r].a - node[r].s * 2 + node[pre[r]].s * 2;//方案2,也就是减去 r 的s*2和a,再加上新r的s*2
if(ans1 < ans2) {//如果方案1更优
int ss = r;
r = pre[r]; Delete(ss);//删除右端点,更新右端点
ans[i] = ans2;//更新答案
} else {
Delete(tmp.second);//删除当前下标
update(1,1,n,tmp.second,INF);//更新,把当前A变成INF
ans[i] = ans1; //更新答案
}
}
for(register int i = 1;i <= n; ++i)
cout << ans[i] << endl;//输出
return 0;//华丽丽地结束
}
最后附上完整代码
//Code By CXY
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>//这里用pair来存区间min值和min值下标
const int MAXN = 1e5 + 10;
const int INF = 1e9;
struct house {
int a,s;
bool operator < (const house &b) const {return a < b.a;}
} node[MAXN];
int n,pre[MAXN],nxt[MAXN];//pre、nxt:链表
int seg[MAXN << 2],p[MAXN << 2];//seg:线段树区间min,p:线段树区间min下标
int ans[MAXN],l,r;//ans:x为不同数值时的答案数组
#define l ll//懒得换变量名了,就define一下
#define r rr//到时候会undef
void pushup(int root) {//更新
if(seg[root << 1 | 1] < seg[root << 1]) {//如果右子树更优
seg[root] = seg[root << 1 | 1];//只能存右子树的答案了
p[root] = p[root << 1 | 1];
} else {
seg[root] = seg[root << 1];//否则存左子树,这样可以让下标尽量小
p[root] = p[root << 1];
}
}
void build(int root,int l,int r) {//简单的建树
if(l == r) {
seg[root] = node[l].a;
p[root] = l;
return;
}
int mid = (l + r) >> 1;
build(root << 1,l,mid);
build(root << 1 | 1,mid + 1,r);
pushup(root);
}
void update(int root,int l,int r,int pos,int val) {//简单的更新
if(l == r && l == pos) {
seg[root] = val;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) update(root << 1,l,mid,pos,val);
else update(root << 1 | 1,mid + 1,r,pos,val);
pushup(root);
}
pii getmin(pii &a,pii b) {//类似于min函数,用来找两个pii中最小值较小的那个(即当前的返回值)
if(b.first < a.first) swap(a,b);
}
pii query(int root,int l,int r,int L,int R) {
if(L <= l && r <= R) return make_pair(seg[root],p[root]);
int mid = (l + r) >> 1;
pii res = make_pair(INF,INF);
if(L <= mid) getmin(res,query(root << 1,l,mid,L,R));//这里用到了getmin,可以自己研究一下
if(R > mid) getmin(res,query(root << 1 | 1,mid + 1,r,L,R));
return res;
}
#undef l
#undef r//说了会undef
void Delete(int pos) {
pre[nxt[pos]] = pre[pos];
nxt[pre[pos]] = nxt[pos];
}//这是一个极其简单的删除节点函数。。。
int main () {
ios::sync_with_stdio(0);//优化
cin >> n;
for(register int i = 1;i <= n; ++i)
cin >> node[i].s;
for(register int i = 1;i <= n; ++i) {
cin >> node[i].a;
pre[i] = i - 1;
nxt[i] = i + 1;//链表初始化
ans[n] += node[i].a;//最终答案
}
build(1,1,n);//建树
l = 1,r = n;//左右端点(实际只有 r 有用)
ans[n] += node[r].s * 2;//答案要加上dis
for(register int i = n - 1;i >= 1; --i) {//倒推答案
pii tmp = query(1,1,n,l,r - 1);//方案1 中用线段树
int ans1 = ans[i + 1] - tmp.first;//方案1
int ans2 = ans[i + 1] - node[r].a - node[r].s * 2 + node[pre[r]].s * 2;//方案2,也就是减去 r 的s*2和a,再加上新r的s*2
if(ans1 < ans2) {//如果方案1更优
int ss = r;
r = pre[r]; Delete(ss);//删除右端点,更新右端点
ans[i] = ans2;//更新答案
} else {
Delete(tmp.second);//删除当前下标
update(1,1,n,tmp.second,INF);//更新,把当前A变成INF
ans[i] = ans1; //更新答案
}
}
for(register int i = 1;i <= n; ++i)
cout << ans[i] << endl;//输出
return 0;//华丽丽地结束
}
其实用线段树也蛮有趣的,大家可以试试鸭
初二的 $OIer$ ,请多关照