Featured image of post 算法板子的扩大化思路(长期更新)

算法板子的扩大化思路(长期更新)

On Expanding Algorithm Templates (OEAT)

算法板子的扩大化思路(长期更新)

数字量偏移的01背包

例题:P2340 Cow Exhibition G

思路:

1.DP的想法

  • Q: 为啥要用01背包而不是其它算法?
  • A:我们发现,每只奶牛只有选和不选两种状态,而且是需要决策的,所以01背包绝对是最好的选择

01背包中,一定要想好什么是体积,什么是价值,什么是背包容量。此题中,我们可以把体积看成奶牛的智商,背包容量看成奶牛的个数,价值看成奶牛的情商。每只牛有两种选择:参加会展和不参加会展。我们要求在智商一定的情况下情商的最大值,这其实就是01背包的模板。状态转移方程的推导如下:

  • 第 i 只奶牛不参加会展:$dp[i][j]=dp[i−1][j]$
  • 第 i 只奶牛参加会展:$dp[i][j]=dp[i−1][j−a[i].iq]+a[i].eq$
  • 加上决策,选取上面两种情况的最大值:$dp[i][j]=max(dp[i−1][j],dp[i−1][j−a[i].iq]+a[i].eq)$

2.数字量偏移详解

dp[i] 表示「经过偏移处理后,下标为 i 时,对应的实际智商和为 i - tot,此时的最大情商和为 dp[i]

这里的 tot 是偏移量(在代码中等于 n*1000),它的作用是将可能出现的负数智商和映射到数组的非负下标上。例如:

  • 当实际智商和为 0 时,对应的数组下标是 tot(因为 0 + tot = tot)。
  • 当实际智商和为 正数(如 5)时,对应的数组下标是 tot + 5
  • 当实际智商和为 负数(如 - 5)时,对应的数组下标是 tot - 5

因为数组下标不能是负数,但我们需要处理智商和为负数的情况。通过引入 tot 作为偏移量:

  • i < tot 的部分:对应实际智商和为负数(例如 i = tot - 10 对应实际智商和为 -10)。
  • i = tot 的部分:对应实际智商和为 0(初始状态)。
  • i > tot 的部分:对应实际智商和为正数(例如 i = tot + 10 对应实际智商和为 +10)。

特别的:中间态实际上会出现一个花费背包空间为负的物品需要放入,此时会从比更大的背包转移,所以是正序遍历。需要注意的是:我们只对$j$这个变量做了偏移处理!

STD:

 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
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int N = 2e5+10;

struct node
{
    int u,v;
};
node ipt[N];
int dp[N];
void solve()
{
    int n;cin >> n;
    for(int i=1;i<=n;i++) cin >> ipt[i].u >> ipt[i].v;
    int tot = n*1000;
    for(int i=0;i<=tot*2;i++) dp[i] = -1e8;
    dp[tot] = 0;
    for(int i=1;i<=n;i++)
    {
        if(ipt[i].u<0 and ipt[i].v<0) continue;
        if(ipt[i].u<0)
        {
            for(int j=0;j<=tot*2+ipt[i].u;j++)
                dp[j] = max(dp[j],dp[j-ipt[i].u]+ipt[i].v);
        }
        else
        {
            for(int j=tot*2;j>=ipt[i].u;j--)
                dp[j] = max(dp[j],dp[j-ipt[i].u]+ipt[i].v);
        }
    }
    int ans = -1;
    for(int i=tot;i<=2*tot;i++) 
    {
        if(dp[i]<0) continue;
        ans = max(ans,dp[i]+i-tot);
    }
    cout << (ans==-1 ? 0 : ans) << endl;
}

signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _ = 1;
    //cin >> _;
    while(_--)
    {
        solve();
    }
    return 0;
}

单调队列+01背包问题

例题:P2627 Mowing the Lawn G

思路:

1.dp的思路来源

我们发现,对于每个奶牛,我们要考虑选还是不选,于是有了第一种想法:

  • 对于一头奶牛,如果我们不选,则有:$dp[i][0] = max(dp[i-1][0],dp[i-1][0])$

那考虑选这头奶牛:

从第一头奶牛开始看,我们发现到了第$k+1$头的时候,需要满足第二个条件,即不能连续的超过$k$头,如果我们要选择这头奶牛的话,意味着前面$k$头需要考虑一头不选,这头不选的设为$j,j\in(1\le j \le k)$,并且,如果我们不选$j$,那么在$j$到$i$之间会出现$[j+1,i-1]$是会被选择的,所以总的来看,对于第$k+1$头牛而言,选它的转移为

$$ dp[k+1][1] = max\{dp[j][0]+sum[k]-sum[j],j\in[1,k]\} $$

推广到任意一头牛,则有$j\in(i-k\le j \le i-1)$,那么需要找出这个区间中最大的那个$dp[j][0]$,那么转移方程为:

$$ dp[i][1] = max\{dp[j][0]-sum[j],j\in[i-k,i-1]\}+sum[i-1] $$

2.单调队列的思路来源

我们发现,对于第$i$头牛而言,总是需要维护一个最大值:

$$ mx = max\{dp[j][0]-sum[j],j\in[i-k,i-1]\} $$

于是考虑最大最小值动态维护的办法:单调队列+滑动窗口。

STD:

 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
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int N = 2e5+10;

int n,k;
int pre[N];
int dp[N][2];

void solve()
{
	cin >> n >> k;
	for(int i=1;i<=n;i++) 
	{
		int t;cin >> t;
		pre[i] = pre[i-1] + t;
	}
	deque<int> dq;
	dq.push_back(0);
	for(int i=1;i<=n;i++)
	{
		dp[i][0] = max(dp[i-1][0],dp[i-1][1]);
		while(!dq.empty() and dq.front()<i-k) dq.pop_front();
		dp[i][1] = dp[dq.front()][0] + pre[i] - pre[dq.front()];
		while(!dq.empty() and dp[dq.back()][0] - pre[dq.back()] < dp[i][0] - pre[i]) dq.pop_back();
		dq.push_back(i);
	}
	cout << max(dp[n][1],dp[n][0]) << endl;
}

signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int _ = 1;
	//cin >> _;
	while(_--)
	{
		solve();
	}
	return 0;
}
本作品采用知识共享署名-非商业性使用-相同方式共享4.0国际许可协议进行许可(CC BY-NC-SA 4.0)
文章浏览量:Loading
Powered By MC ZBD Studio
发表了43篇文章 · 总计69.19k字
载入天数...载入时分秒...
总浏览量Loading | 访客总数Loading

主题 StackJimmy 设计
由ZephyrBD修改