题意 输入一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大。
思考 状态表示-集合 $f_{i}$ : 以 $i$ 为 右端点, 长度 不超过 $m$ 连续子区间 状态表示-属性 $f_{i}$ : 区间的总和最大 $M a x$ 状态计算 $f_{i}$ :
观察这个转移方程, 首先这里的 $j$ 是有范围的: $i-m \leq j \leq i-1$ 其次, $s_{i}$ 作为一个常量, 可以提到外面去: $f_{i}=s_{i}-\min \left\{s_{j}\right\} \quad(1 \leq i-j \leq m)$ 从前向后 维护一个长度不超过 $m$ 的区间的 最小值, 就想到我们最熟悉的 滑动窗口模型 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <iostream> #include <vector> #include <cstring> #include <algorithm> #include <queue> #include <deque> #include <cmath> #include <map> using namespace std;typedef long long LL;typedef pair<int ,int > PII;const int N = 100000 ,M=30010 ,INF = 0x3f3f3f3f ,mod=998244353 ;deque<int > qu; int n,m;LL s[300010 ],res; int main () { cin>>n>>m; for (int i=1 ;i<=n;i++){ cin>>s[i]; s[i]+=s[i-1 ]; } res=-1000000000000 ; qu.push_back (0 ); for (int i=1 ;i<=n;i++){ if (!qu.empty ()&& i - qu.front ()>m){ qu.pop_front (); } res=max (res,s[i]-s[qu.front ()]); while (!qu.empty ()&&s[qu.back ()]>=s[i]){ qu.pop_back (); } qu.push_back (i); } cout<<res<<endl; return 0 ; }
题意 给定一个长度为 n 的数组 w,其中 Wi 是第 i 个元素的 贡献
我们可以选择的 数组 中的一些元素,这些元素的贡献总和表示我们该种 方案 的 价值
但是,如果方案中出现选择了连续相邻且超过m个元素,则这些连续相邻的元素贡献归零
求解一种方案,使得选择的元素贡献总和 最大
思考 由于 连续选择 超过 $m$ 个元素时, 这些元素的 贡献 为 0 (相当于没选) 而本题, 所有的元素值都是 正整数, 故我们的方案中, 连续选择的元素数量 一定是不超过 $m$ 的
可以用 反证法 证明, 如果方案中有超过 $m$ 个连续元素, 则我们不选中间的一个, 使他断开, 必然不会使方案变差 于是, 我们就可以通过 最后一次没有选择的元素, 对 集合进行划分 间氏DP分析法 状态表示-集合 $f_{i}$ : 以 $i$ 为右端点的 前缀数组 的选择方案 状态表示-属性 $f_{i}$ : 方案的 价值 最大 $M a x$ 状态计算:跟上一题一样, 首先我们把 常量 提出来: $f_{i}=s_{i}+\max \left\{f_{j-1}-s_{j}\right\} \quad 0 \leq i-j \leq m$ 由于 $j$ 是有范围的: $i-m \leq j \leq i$, 于是问题就转化为 滑动窗口求极值 的问题了 我们用一个记录 $f_{j-1}-s_{j}$ 的 单调递减 的队列 在 队头 维护一个 最大值 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <cstring> #include <algorithm> using namespace std;typedef long long LL;const int N = 1e5 + 10 ;int n, m;LL s[N], f[N]; int que[N];LL g (int i) { return f[max (0 , i - 1 )] - s[i]; } int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i ++ ) scanf ("%lld" , &s[i]), s[i] += s[i - 1 ]; int hh = 0 , tt = 0 ; que[0 ] = 0 ; for (int i = 1 ; i <= n; i ++ ) { while (hh <= tt && g (i) >= g (que[tt])) tt -- ; que[ ++ tt] = i; while (hh <= tt && i - que[hh] > m) hh ++ ; f[i] = s[i] + g (que[hh]); } printf ("%lld\n" , f[n]); return 0 ; }
题意 给定一个 环,环 上有 n 个节点,编号从 1∼n,以及一辆小车车。每一个 节点 i 有一个 权值 pi 表示当车到达该点时,可以获得的油量还有一个权值 di表示当车从 节点 i 到 节点 i+1 所需要 消耗的油量。现有一辆车想从环上任意点出发,顺时针或逆时针绕环一圈走回起点行驶的过程中,油量不能为负数,初始油量为起点处所能获得的油量判断能否完成环圈行驶。
3≤n≤10^6
思考 先破环成链。该区间合法必然等价于任意前缀获油量大于前缀使用量。
相对应的 前缀和 含义:
sd[i] :从 1 出发到 i+1 所消耗的总油量 sp[i] :从 1 出发到 i 所获得的总油量
状态表示-集合 $f_{i}$ : 以 $i$ 为 起点 和 终点, 顺时针 走一圈的方案 状态表示-属性 $f_{i}$ : 方案 是否可行 true 状态计算:
和往常一样, 提出 常量 $: f_{i}=\left[s d_{i-1}-s p_{i-1}+\min \left\{s p_{j}-s d_{j}\right\} \geq 0\right] \quad(i \leq j \leq i+n-1)$ 这样就变成一个 滑动窗口求最小值 的问题了, 直接套 单调队列 即可 再推导一下滑动窗口的范围: $1 \leq i+n-j \leq n$
逆时针的走法:
状态表示-集合 $f_{i}$ : 以 $i$ 为 起点 和 终点, 逆时针 走一圈的 方案 状态表示-属性 $f_{i}$ : 方案 是否可行 true 状态计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <iostream> #include <cstring> #include <algorithm> #include <deque> using namespace std;typedef long long LL;const int N = 1e6 + 10 ;int n;LL d[N],p[N],sd[N],sp[N]; bool f1[N];deque<int > qu; LL g (int j) { return sp[j] - sd[j]; } LL h (int j) { return sd[j - 2 ] - sp[j - 1 ]; } LL get1 (int i, int j) { return sd[i - 1 ] - sp[i - 1 ] + g (j); } LL get2 (int i, int j) { return sp[i + n] - sd[i + n - 1 ] + h (j); } int main () { cin>>n; for (int i=1 ;i<=n;i++){ scanf ("%lld%lld" ,&p[i],&d[i]); p[i+n]=p[i]; d[i+n]=d[i]; } for (int i = 1 ; i <= n << 1 ; i ++ ){ sd[i] = sd[i - 1 ] + d[i]; sp[i] = sp[i - 1 ] + p[i]; } for (int i=1 ;i<=n<<1 ;i++){ if (qu.size ()&&i-qu.front ()>n) qu.pop_front (); if (i>n){ if (get1 (i-n,qu.front ())>=0 ) f1[i-n]=true ; } while (qu.size ()&&g (qu.back ())>=g (i)) qu.pop_back (); qu.push_back (i); } qu.clear (); for (int i = n << 1 ; i; i -- ) { while (qu.size ()&&qu.front ()-i>n) qu.pop_front () ; if (i <= n){ if (get2 (i, qu.front ()) >= 0 ){ f1[i] = true ; } } while (qu.size ()&&h (qu.back ())>=h (i)) qu.pop_back () ; qu.push_back (i); } for (int i = 1 ; i <= n; i ++ ) puts (f1[i] ? "TAK" : "NIE" ); return 0 ; }
题意 有一个 a×b的整数组成的矩阵,现请你从中找出一个 n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
思考 我们可以预处理出每列的行中最值,再对该列求一个最值,就是该子矩阵的最值
该降维手段,使得一个 二维滑动窗口问题 变成了 n行+n列 的 一维滑动窗口问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <iostream> #include <cstring> #include <algorithm> #include <deque> using namespace std;typedef long long LL;const int N=1e5 +10 ;int m,n,k;int min_row[1010 ][1010 ];int max_row[1010 ][1010 ];int mp[1010 ][1010 ];int ax[1010 ],bx[1010 ],cx[1010 ];void getmax (int aa[],int bb[],int len) { deque<int > qu; for (int i=1 ;i<=len;i++){ if (qu.size ()&&qu.front ()<=i-k) qu.pop_front (); while (qu.size ()&&aa[qu.back ()]<=aa[i]){ qu.pop_back (); } qu.push_back (i); bb[i]=aa[qu.front ()]; } } void getmin (int aa[],int bb[],int len) { deque<int > qu; for (int i=1 ;i<=len;i++){ if (qu.size ()&&qu.front ()<=i-k) qu.pop_front (); while (qu.size ()&&aa[qu.back ()]>=aa[i]) qu.pop_back (); qu.push_back (i); bb[i]=aa[qu.front ()]; } } int main () { cin>>n>>m>>k; for (int i=1 ;i<=n;i++){ for (int j=1 ;j<=m;j++){ scanf ("%d" ,&mp[i][j]); } } for (int i=1 ;i<=n;i++){ getmax (mp[i],max_row[i],m); getmin (mp[i],min_row[i],m); } int res=0x3f3f3f3f ; for (int i=k;i<=m;i++){ for (int j=1 ;j<=n;j++) ax[j]=min_row[j][i]; getmin (ax, bx, n); for (int j=1 ;j<=n;j++) ax[j]=max_row[j][i]; getmax (ax, cx, n); for (int j=k; j<=n;j++) res=min (res,cx[j]-bx[j]); } cout<<res; return 0 ; }