悬线法的适用范围是单调栈的子集。具体来说,悬线法可以应用于满足以下条件的题目:
1.需要在扫描序列时维护单调的信息;
2.可以使用单调栈解决;
3.不需要在单调栈上二分。
当然,悬线法是可以通过滑动窗口等方法来代替的,但是有些题目使用悬线法的话会更好去理解。
首先我们来看一下最为重要的两个数组,一个是l[i]数组:最左边的位置,一个是r[i]数组:最右边的位置。
我们以l[i]为例子说明!
如果当前l[i<=1,则说明已经到达最左边了
如果当前a[i]>a[i-1],则说明悬线已经不能向左扩展了
如果当前a[i]<=a[i-1],则说明可以继续向左扩展,并扩展到i-1的位置做一次更新。
从上面可以看出我们求l[i]输出的方法是o(n)的,可以作为我们的预处理
r[i]数组类似,我就不在此继续称述了!那么我们接下来看一下我们的一个题目
巫师的总力量和
题意:转化一下就是在在每一个点上,找到他是最小的所有区间的区间和再乘以这个数,然后我们求所有点的和。
思路:第一反应当然是滑动窗口然后一个一个去算,这也是一种写法,本文将会换成悬线法去求解。
typedef long long ll;
const int mod = 1000000007;
class Solution {
private:
int calc(int l, int r)//求区间和的函数,也是悬线法的三个判断。
{
if (r < 0) return 0;
if (l <= 0) return s[r];
return (s[r] - s[l - 1]) % mod;
}
public:
int sum[100005];
int s[100005];
int totalStrength(vector<int>& strength) {
const int n = strength.size();
sum[0] = strength[0];
s[0] = sum[0];
for (int i = 1; i < n; i++) {
sum[i] = (sum[i - 1] + strength[i]) % mod;//求前缀和
s[i] = (s[i - 1] + sum[i]) % mod;//求前缀和的前缀和
}
stack<int> st;
vector<int> l(n), r(n);
strength.push_back(0);
for (int i = 0; i <= n; i++) {
while (!st.empty() && strength[i] <= strength[st.top()]) {
int top = st.top();
st.pop();
r[top] = i - 1;//更新右边界
if (st.empty()) l[top] = 0;
else l[top] = st.top() + 1;//更新左边界
}
st.push(i);
}
int ans = 0;
for (int i = 0; i < n; i++) {
int w = strength[i];
ll rsum = calc(i, r[i]), lcnt = i - l[i] + 1;//右边界区间和乘以左边界长度
ll lsum = calc(l[i] - 1, i - 1), rcnt = r[i] - i + 1;//这个地方有重复的地方我们要减去
ans = (ans + rsum * lcnt % mod * w) % mod;
ans = (ans - lsum * rcnt % mod * w) % mod;
}
return (ans + mod) % mod;//防止有负数
}
};