[HDU6757]Hunting Monsters
壹、关于题目 ¶
一道神题,没什么可以阐述的,唯一需要注意的就是在 \(\rm HDU\) 上提交好像只能使用 scanf()
.
贰、关于题解 ¶
是为神题,则定有其过人之处。题之精妙者,诚尔;吾之愚笨者,深尔!天丧予!天丧予!
◆ 加工生产调度
一共有 \(n\) 个产品,某个产品 \(i\) 在 \(A,B\) 两车间加工的时间分别为 \(a_i,b_i\). 怎样安排这 \(n\) 个产品的加工顺序,才能使总的加工时间最短?
一个很经典的贪心题,存在结论:
- 按照 \(a_i\le b_i\),与 \(a_i>b_i\) 分成两组 \(T,S\). 对于 \(T\),按照 \(a_i<a_j\) 排序;对于 \(S\),按照 \(b_i>b_j\) 排序,再将 \(T,S\) 拼接起来,按顺序选择最优;
有感性证明:
不难发现,\(A\) 会一直加工直到全部的东西都加工完毕;那么,有两个特殊位置 —— 在最开始的时候,以及 \(A\) 机器加工完毕之后;
显然,在最开始的时候,\(A\) 是单独运作而 \(B\) 停机;当 \(A\) 加工完所有的东西之后,\(B\) 仍然在加工最后的一部分产品,只有当 \(B\) 完成加工,任务结束。
那么,贪心地,在最开始的时候选择 \(a_i\) 最小的,在最后的时候选择 \(b_i\) 最小的,即让 \(a_i\) 最小的最先选,\(b_i\) 最小的最后选择。
不过,我们可以尝试选择一些更理性的证明:
设 \(S=\set{J_1,J_2,\cdots,J_s}\) 为等待加工的集合,其中 \(J_i=(a_i,b_i)\),定义 \(T(S,t)\) 表示 \(B\) 机器要在 \(t\) 之后才可以开始加工,目前剩下的代加工集合为 \(S\) 时的最小加工时间,不难发现:
于是,我们分别假设先加工 \(i\) 比先加工 \(j\) 更优,算出 \(T_{ij}\) 与 \(T_{ji}\) 进行比较(过程过于冗杂便不给出),最后我们可以得到,当先执行 \(i\) 比先执行 \(j\) 更优时,应当满足:
这是 \(\rm Johnson\) 法则的数学表达式。
不过,当我们实际操作的时候,发现这个表达式可能存在一些问题:它不满足不可比性传递性。
所以,我们才要对 \(\set{J_i}\) 进行分组,使得组内比较满足不可比性传递性。于是才有了上述分组。
◆ 问题划分
该题可类比上面的题,于是,我们可以先对所有 \((a_i,b_i)\) 分组:相同地,按照 \(a_i\le b_i\),与 \(a_i>b_i\) 分成两组 \(T,S\). 排序也是同样的道理。
到现在,问题就变成:
- 对于任意 \(k\),在 \(S\) 中选择 \(k\) 个任务的最小初始能量;
- 对于任意 \(k\),在 \(T\) 中选择 \(k\) 个任务的最小初始能量;
- 将上面两种合并;
接下来考虑逐个击破;
◆ 我与 \(S\) 最后的战场,亦或是世界起始的圣战
先考虑简单 \(\rm DP\):定义 \(f(i,j)\) 表示在 \(S\) 最后 \(i\) 个数对中,选出 \(j\) 个的最小初始值,那么有转移:
另外,我们不难看出几个比较显然的不等关系:\(f(i,j-1)\le f(i,j)\le f(i-1,j)\),后者是因为遵循排序规则选择,从越前面的开始选择答案应该更优。
不过,我们似乎不能直接从这个转移得到什么东西,考虑挖掘这个转移的一些关系:
- 取 \(\min\) 的前一项中,最小值显然为 \(a_i\),所以当 \(f(i-1,j)\le a_i\) 时,后一项一定更优,我们可以直接从后一项转移;
-
- 更进一步地,若 \(f(i-1,j)\le b_i\),又 \(b_i<a_i\),且 \(b_i\) 上升,所以对于后面任意时刻都满足前面的不等式,这个时候后面的每一项 \(f(k,j)(k\in [i,|S|])\) 值都为 \(f(i-1,j)\) 了;
- 当 \(f(i-1,j)>a_i\) 时,我们又应该考察另外的情况了:
-
- 当 \(f(i-1,j)\le f(i-1,j-1)-b_i+a_i\) 时,显然对 \(f(i-1,j-1)-b_i\) 再套上和 \(0\) 取 \(\max\) 只会让值更大,此时 \(f(i,j)\) 也可以直接由 \(f(i-1,j)\) 转移了;
- 当 \(f(i-1,j)>f(i-1,j-1)-b_i+a_i\) 时,看似因为 “对 \(f(i-1,j-1)-b_i\) 再套上和 \(0\) 取 \(\max\) 只会让值更大” 导致我们不能直接看出 \(f(i,j)\) 该由哪边转移,可是若 \(f(i-1,j-1)-b_i\) 真的都比 \(0\) 小了,那么转移的前一项就是 \(a_i\),根据前提 \(f(i-1,j)>a_i\),我们也可以直接由前一项转移了,即在这种情况下 \(f(i,j)=\max\set{f(i-1,j-1)-b_i,0}+a_i\);
于是,我们似乎可以这样实现转移:
维护一个数据结构,这个数据结构中保留着 \(f(i-1)\) 的所有信息,现在到了 \(i\),找到某些位置 \(j\) 使得 \(f(i-1,j)\le b_i\),这些位置直到最后都一定会是 \(f(i-1,j)\),所以将这些位置删去,同时保留 \(f(n,j)=f(i-1,j)\),对于那些 \(f(i-1,j)\le a_i\) 的位置,我们不需要管它,因为这些位置是 \(f(i,j)=f(i-1,j)\),即直接继承上一次的值,同样对于那些 \(f(i-1,j)\le f(i-1,j-1)-b_i+a_i\) 的位置,我们也不需要管这些 \(j\). 需要特殊处理的,只有 \(f(i-1,j)>f(i-1,j-1)-b_i+a_i\) 的 \(j\),我们将这些 \(j\) 整体更新为 \(\max\set{f(i-1,j-1)-b_i,0}+a_i\).
不过,遗憾的是,似乎并没有这样一种精妙的数据结构支持以上的操作,一切的一切都要我们暴力地修改判断,也就是说,如果迟迟不出现 \(f(i,j)\le b_i\) 的情况,最终的复杂度仍然会达到 \(\mathcal O(n^2)\),这是不好的......
然而,这个转移有着它神奇的地方。
具体来说,我们可以将每个状态看做 \((i,f(i,j))\) 的点,而转移则是两个向量 \((0,0),(1,a_i-b_i)\),由于是取 \(\min\),我们则可以将其类比为,当前的一个下凸壳在与两个向量构成的一条线作闵科夫斯基和,由于初始的 \(f\) 只是一个点,转移始终是一条线,最初显然是俩凸包,那么后面每个时刻都会是俩凸包!也就是说,\((i,f(i,j))\) 是具有凸性的,也即
于是,我们只需要使用优先队列维护差分即可,因为差分具有单调性,我们每次看看到队头的元素和 \(b_i\) 的关系即可。如果当前的元素比 \(b_i\) 小,那么可以弹出,因为它直到最后一定都是一个值。对于剩下的部分,首先他们一定都是大于 \(b_i\) 的,在这些部分中,在 \((b_i,a_i]\) 的部分可以直接继承上一个的值,剩下的部分应该加上 \(a_i-b_i\),观察转移 \(f(i-1,j)>f(i-1,j-1)-b_i+a_i\),其实际上就是 \(f(i-1,j)-f(i-1,j-1)>a_i-b_i\),这个转移其实还是差分与 \(a_i-b_i\) 比较,那这就好办了,我们只需要加入一个 \(a_i-b_i\) 的虚元素进去,在堆中它一定会找到自己应该在的位置,并且由于我们维护的是差分,所以它到该到的位置之后,就表示了将比他大的元素都加上 \(a_i-b_i\). 该复杂度为 \(\mathcal O(n\log n)\).
◆ \(T\) 是我最后的归宿
这很好办,按照 \(a_i\le b_i\) 之后选择较小的 \(K\) 个即可。
◆ \(S\) 与 \(T\) 的统一
设在 \(T\) 中选择 \(i\) 个的最小初始值为 \(g(i)\),选完这些的增加量为 \(d(i)\),在 \(S\) 中选择 \(j\) 个的最小初始值为 \(f(j)\),最后的结果为 \(h(k)\).
显然最后我们会先在 \(T\) 中选择,再在 \(S\) 中选择,显然在 \([g(i),g(i+1))\) 这个区间中,我们最开始都只能选 \(i\) 个,于是我们可以找到 \(j\) 使得 \(f(j)\le g(i+1)-1+d(i)\),然后将 \(h(i+j)\) 更新为 \(\max\set{g(i),f(j)-d(i)}\). 看似是 \(\mathcal O(n^2)\) 的,不过 \(g,f\) 都是单调的,并不需要那么麻烦。
然后就做完了。
◆ 实现细节
- 注意,由于我们的 \(f\) 是倒着定义的,所以排序时也应该倒着来;
- 处理 \(S\) 时,我们在加入 \(a_i-b_i\) 之前,需要先整体减少 \(b_i\),因为我们下一次的标准会是当前的 \(b_i\),所以在堆中,如果有元素,那么应该先更新一下堆顶;
叁、参考代码 ¶
# include <bits/stdc++.h>
using namespace std;
// # define USING_STDIN
// # define NDEBUG
// # define NCHECK
# include <cassert>
namespace Elaina {
# define rep(i, l, r) for(int i=(l), i##_end_=(r); i <= i##_end_; ++i)
# define drep(i, l, r) for(int i=(l), i##_end_=(r); i >= i##_end_; --i)
# define fi first
# define se second
# define mp(a, b) make_pair(a, b)
# define Endl putchar('\n')
# define whole(v) ((v).begin()), ((v).end())
# define bitcnt(s) (__builtin_popcount(s))
# ifdef NCHECK
# define iputs(Content) ((void)0)
# define iprintf(Content, argvs...) ((void)0)
# else
# define iputs(Content) fprintf(stderr, Content)
# define iprintf(Content, argvs...) fprintf(stderr, Content, argvs)
# endif
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair <int, int> pii;
typedef pair <ll, ll> pll;
template <class T> inline T fab(T x) { return x < 0? -x: x; }
template <class T> inline void getmin(T& x, const T rhs) { x=min(x, rhs); }
template <class T> inline void getmax(T& x, const T rhs) { x=max(x, rhs); }
# ifndef USING_STDIN
inline char freaGET() {
# define BUFFERSIZE 5000
static char BUF[BUFFERSIZE], *p1=BUF, *p2=BUF;
return p1 == p2 && (p2=(p1=BUF)+fread(BUF, 1, BUFFERSIZE, stdin), p1 == p2)? EOF: *p1++;
# undef BUFFERSIZE
}
# define CHARGET freaGET()
# else
# define CHARGET getchar()
# endif
template <class T> inline T readret(T x) {
x=0; int f=0; char c;
while((c=CHARGET) < '0' || '9' < c) if(c=='-') f=1;
for(x=(c^48); '0' <= (c=CHARGET) && c <= '9'; x=(x<<1)+(x<<3)+(c^48));
return f? -x: x;
}
template <class T> inline void readin(T& x) { x=readret(T(1)); }
template <class T, class... Args> inline void readin(T& x, Args&... args){
readin(x), readin(args...);
}
template <class T> inline void writc(T x, char s='\n') {
static int fwri_sta[55], fwri_ed=0;
if(x < 0) putchar('-'), x=-x;
do fwri_sta[++fwri_ed]=x%10, x/=10; while(x);
while(putchar(fwri_sta[fwri_ed--]^48), fwri_ed);
putchar(s);
}
} using namespace Elaina;
const int maxn=3e5;
const ll inf=1ll<<50;
int n, tmpa[maxn+5], tmpb[maxn+5];
struct node { int a, b; };
vector <node> S, T;
inline void input() {
readin(n), S.clear(), T.clear();
rep(i, 1, n) readin(tmpa[i]);
rep(i, 1, n) readin(tmpb[i]);
rep(i, 1, n) {
int a=tmpa[i], b=tmpb[i];
if(a <= b) T.emplace_back(node{a, b});
else S.emplace_back(node{a, b});
}
sort(whole(S), [](const node& x, const node& y) { return x.b<y.b; } );
sort(whole(T), [](const node& x, const node& y) { return x.a<y.a; } );
}
ll f[maxn+5];
priority_queue < ll, vector<ll>, greater<ll> > Q;
inline void solveS() {
while(!Q.empty()) Q.pop();
ll pre=0, cur; int id=0;
for(auto x: S) {
/// the part lower than @p b[i]
while(!Q.empty()) {
cur=Q.top()+pre;
if(cur > x.b) break;
pre=cur, Q.pop();
f[++id]=pre;
}
/// we're to change @p low , so need to update the @p Q first
if(!Q.empty()) {
cur=Q.top()+pre-x.b;
Q.pop(), Q.push(cur);
}
Q.push(x.a-x.b), pre=x.b;
}
for(; !Q.empty(); Q.pop()) f[++id]=(pre+=Q.top());
}
ll g[maxn+5], d[maxn+5];
inline void solveT() {
int id=0;
for(auto x: T) {
++id;
g[id]=max(x.a-g[id-1]-d[id-1], 0ll)+g[id-1];
d[id]=d[id-1]+x.b-x.a;
}
g[id+1]=inf; /// deal with the special situation
}
const ll mod=1e9+7;
ll h[maxn+5];
inline void combine() {
int Tsz=int(T.size()), Ssz=int(S.size()), cur=0;
for(int i=0, j=0; i <= Tsz; ++i) {
if(g[i] == g[i+1]) continue; // same cost
ll r=g[i+1]-1+d[i];
while(j < Ssz && f[j+1] <= r) ++j;
rep(k, cur+1, i+j) {
if(k<i) h[k]=g[i];
else h[k]=max(f[k-i]-d[i], g[i]);
}
cur=i+j;
}
ll ans=0;
rep(k, 1, n) ans=(ans+1ll*h[k]%mod*k%mod)%mod;
writc(ans);
}
inline void work() {
input();
solveS();
solveT();
combine();
}
signed main() {
// freopen("hunting-monsters.in", "r", stdin);
// freopen("hunting-monsters.out", "w", stdout);
rep(_, 1, readret(1)) work();
return 0;
}