10.12 周一
P3373 【模板】线段树 2
这道题真的一波三折
这题干了好久,大概想了三四天,每天一两个小时刚这道题,终于tm独立想出来了
这题要写一个支持区间每个数乘法的线段树
第一个坎是怎么弄标记,这里我卡了挺久
想用一个标记,发现不行。那就用两个标记,加和乘
但是两个标记怎么共存,什么顺序,我卡了
计算机导论课嫌无聊自己回来想这道题,突然醒悟先乘后加,先加后乘很麻烦
然后交上去发现30分
又懵逼了
然后又过了几天,突然醒悟一个条件写错了,乘法标记并不是大于1才下放,因为可能乘以0
所以我还是忽略了极端情况,极大或0
最后还是过了
坚持不看题解,独立思考,坚持下去!!
#include<bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) ((k << 1) + 1)
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 10;
int MOD;
struct node
{
ll f1, f2, w;
int l, r;
int len() { return r - l + 1; }
int m() { return (l + r) >> 1; }
}t[MAXN << 2];
//1 jia 2 cheng
void lazy(int k, ll p, int q)
{
if(q == 1)
{
t[k].f1 = (t[k].f1 + p) % MOD;
t[k].w = (t[k].w + t[k].len() * p) % MOD;
}
else
{
t[k].f1 = t[k].f1 * p % MOD;
t[k].f2 = t[k].f2 * p % MOD;
t[k].w = t[k].w * p % MOD;
}
}
void up(int k) { t[k].w = (t[l(k)].w + t[r(k)].w) % MOD; }
void down(int k, int q)
{
if(q == 1)
{
lazy(l(k), t[k].f1, q);
lazy(r(k), t[k].f1, q);
t[k].f1 = 0;
}
else
{
lazy(l(k), t[k].f2, q);
lazy(r(k), t[k].f2, q);
t[k].f2 = 1;
}
}
void build(int k, int l, int r)
{
t[k].l = l; t[k].r = r; t[k].f1 = 0; t[k].f2 = 1;
if(l == r)
{
scanf("%lld", &t[k].w);
return;
}
int m = t[k].m();
build(l(k), l, m);
build(r(k), m + 1, r);
up(k);
}
void change(int k, int L, int R, ll p, int q)
{
if(L <= t[k].l && t[k].r <= R)
{
lazy(k, p, q);
return;
}
if(t[k].f2 != 1) down(k, 2); if(t[k].f1) down(k, 1);
int m = t[k].m();
if(L <= m) change(l(k), L, R, p, q);
if(R > m) change(r(k), L, R, p, q);
up(k);
}
ll sum(int k, int L, int R)
{
if(L <= t[k].l && t[k].r <= R) return t[k].w % MOD;
if(t[k].f2 != 1) down(k, 2); if(t[k].f1) down(k, 1);
ll res = 0;
int m = t[k].m();
if(L <= m) res = (res + sum(l(k), L, R)) % MOD;
if(R > m) res = (res + sum(r(k), L, R)) % MOD;
return res;
}
int main()
{
int n, m, q, x, y, k;
scanf("%d%d%d", &n, &m, &MOD);
build(1, 1, n);
while(m--)
{
scanf("%d%d%d", &q, &x, &y);
if(q <= 2)
{
if(q == 1) q = 2; else q = 1;
scanf("%d", &k);
change(1, x, y, k, q);
}
else printf("%lld\n", sum(1, x, y));
}
return 0;
}
发现寒训内容非常大,还是要多提前复习自学
现在开始复习动态规划
先从01背包开始
背包9讲写得太好了
01背包模板
这个方法有亮点
一 空间优化 发现方程每次只与上一行有关,所以可以把二维变成一维,而且要逆推
奇妙的是顺推就变成完全背包了
二 初始化问题 如果要求完全装满,可以设置f[0] = 0,其他负无穷,表示不合法
三 注意数组空间,f是总重量不是物品个数
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
const int MAXM = 1e5 + 10;
int f[MAXM], c[MAXN], w[MAXN], n, v;
int main()
{
scanf("%d%d", &n, &v);
_for(i, 1, n) scanf("%d%d", &w[i], &c[i]);
_for(i, 1, n)
for(int j = v; j >= w[i]; j--)
f[j] = max(f[j], f[j-w[i]] + c[i]);
printf("%d\n", f[v]);
return 0;
}
RQNJOJ 72
这个是01背包的一个转化,即可以推出所有可能的重量组合
1表示存在,0表示不存在
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e2 + 10;
const int MAXM = 1e4 + 10;
int m[MAXN], f[MAXM], n, v;
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &m[i]), v += m[i];
f[0] = 1;
_for(i, 1, n)
for(int j = v; j >= m[i]; j--)
f[j] |= f[j-m[i]];
int t = v / 2;
while(!f[t]) t--;
printf("%d\n", v - t - t);
return 0;
}
完全背包
前面已经讲了,第二层循环顺序就是完全
背包九讲中谈到一个物品化成多个物品转化成01背包额思路
其中谈到可以用二进制拆,而不是1234这样
很秀,可能以后其他题目可以用到
RQNOJ 162
完全背包裸题
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 5e3 + 10;
const int MAXM = 1e4 + 10;
int f[MAXM], w[MAXN], v[MAXN], n, m;
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d%d", &w[i], &v[i]);
_for(i, 1, m) f[i] = -1e9;
_for(i, 1, n)
_for(j, w[i], m)
f[j] = max(f[j], f[j-w[i]] + v[i]);
printf("%d\n", f[m]);
return 0;
}
大致规划一下,今天学习前三种背包
明天学完剩下的背包
后天刷背包杂题
多重背包
多重背包化成01背包就完事,用二进制拆
RQNOJ 98
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXM = 500 + 10;
struct node{ int w, v; };
vector<node> g;
int f[MAXM], n, m;
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
int k, w, v;
scanf("%d%d%d", &k, &w, &v);
int t = 1;
while(k > t * 2 - 1)
{
g.push_back(node{w * t, v * t});
t <<= 1;
}
t = k - t + 1;
g.push_back(node{w * t, v * t});
}
REP(i, 0, g.size())
{
int w = g[i].w, v = g[i].v;
for(int j = m; j >= w; j--)
f[j] = max(f[j], f[j-w] + v);
}
printf("%d\n", f[m]);
return 0;
}
10.13 周二
二维费用背包
在原来的基础上多加一维即可,这是动态规划题目多增加条件后的常用方法
RQNOJ 57
这道题要充分利用多加维度的思想
我这里多加了两维,但真正的实现的时候有挺多细节要处理好
一 初始化 把最开始f[0][0][0]为0,其他全部正无穷,包括f[0][0][1]之类的,下标从0开始遍历
同时注意这样有无穷的处理之后,费用就是恰好这个费用的状态。答案可以边做边统计
二 自己现在纸上把状态和转移方程搞清楚,写程序才清晰明了
状态是四维,要四层循环,循环时为了只放一次,要逆序。空间上可以省去一维,因为这一维只与上一行有关
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 110;
int f[MAXN][MAXN][MAXN], w1[MAXN], w2[MAXN], t[MAXN];
int n, m1, m2;
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d%d%d", &w1[i], &w2[i], &t[i]);
scanf("%d%d", &m1, &m2);
_for(i, 0, m1)
_for(j, 0, m2)
_for(k, 0, n)
f[i][j][k] = 1e9;
f[0][0][0] = 0;
int ans = 1e9, ma = 0;
_for(i, 1, n)
for(int j = m1; j >= w1[i]; j--)
for(int k = m2; k >= w2[i]; k--)
for(int p = i; p >= 1; p--)
{
int temp = f[j-w1[i]][k-w2[i]][p-1] + t[i];
if(f[j][k][p] > temp)
{
f[j][k][p] = temp;
if(p > ma) ma = p, ans = temp;
else if(p == ma && temp < ans) ans = temp;
}
}
printf("%d\n", ans);
return 0;
}
上面是一种复杂度n四次方的做法,因为n<=100所以可以做,但n大一点就tle了
还有一种n的三次方做法,挺秀的,这种思路要学习
这里要求最大价值时的最小时间
那就开两个数组,一个维护价值,一个维护时间,同步更新
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 110;
int f1[MAXN][MAXN], f2[MAXN][MAXN], w1[MAXN], w2[MAXN], t[MAXN];
int n, m1, m2;
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d%d%d", &w1[i], &w2[i], &t[i]);
scanf("%d%d", &m1, &m2);
int ans = 1e9, ma = 0;
_for(i, 1, n)
for(int j = m1; j >= w1[i]; j--)
for(int k = m2; k >= w2[i]; k--)
{
int t1 = f1[j-w1[i]][k-w2[i]] + 1;
int t2 = f2[j-w1[i]][k-w2[i]] + t[i];
if(t1 > f1[j][k] || (t1 == f1[j][k] && t2 < f2[j][k]))
{
f1[j][k] = t1, f2[j][k] = t2;
if(t1 > ma || (t1 == ma && t2 < ans)) ma = t1, ans = t2;
}
}
printf("%d\n", ans);
return 0;
}
恪守一件事
把能控制的事情做到最好
不能控制的事情不理它
分组背包
分组背包的核心问题是如何保持一组的物品最多放一个
答案是把费用的循环放在枚举物品循环的外面
为什么把费用的循环放在枚举物品外面就能保证同一组只放一次呢
如果先物品后费用,那么在更新的过程中,f[j-w[i]]就包含了i之前的物品,也就是既放了之前的物品,又放了现在的物品
即可以放多个物品
如果先费用后物品,那么在更新的过程中,f[j-w[i]]就不包含同组的物品,因为是逆序的
有点抽象,我也想了蛮久
P1757 通天之分组背包
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int f[MAXN], w[MAXN], v[MAXN], k[MAXN], n, m, cnt;
int main()
{
scanf("%d%d", &m, &n);
_for(i, 1, n)
{
scanf("%d%d%d", &w[i], &v[i], &k[i]);
cnt = max(cnt, k[i]);
}
_for(p, 1, cnt)
for(int j = m; j >= 0; j--)
_for(i, 1, n)
if(k[i] == p && j >= w[i])
f[j] = max(f[j], f[j-w[i]] + v[i]);
printf("%d\n", f[m]);
return 0;
}
有依赖的背包问题
P1064 金明的预算方案
前面题都独立做出,现在做这道题就很简单了
看似有依赖麻烦,其实利用拆分物品的思想就可以转化为分组背包了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 100;
const int MAXM = 32000 + 10;
int f[MAXM], w[MAXN], v[MAXN], k[MAXN], n, m;
void update(int j, int w, int v)
{
if(j >= w) f[j] = max(f[j], f[j-w] + v);
}
int main()
{
scanf("%d%d", &m, &n);
_for(i, 1, n)
{
scanf("%d%d%d", &w[i], &v[i], &k[i]);
v[i] *= w[i];
}
_for(p, 1, n)
if(k[p] == 0)
for(int j = m; j >= 0; j--)
{
update(j, w[p], v[p]);
int cnt = 0, t[2] = {0};
_for(i, 1, n)
if(k[i] == p)
t[cnt++] = i;
if(cnt == 1) update(j, w[p] + w[t[0]], v[p] + v[t[0]]);
if(cnt == 2)
{
update(j, w[p] + w[t[0]], v[p] + v[t[0]]);
update(j, w[p] + w[t[1]], v[p] + v[t[1]]);
update(j, w[p] + w[t[0]] + w[t[1]], v[p] + v[t[0]] + v[t[1]]);
}
}
printf("%d\n", f[m]);
return 0;
}
今天到这,时间花的比我想的要久
不过没事,认真思考,独立写题,稳扎稳打就好,不要使劲赶进度
加油
10.14 周三
poj 2392
多重背包
二进制拆法要清晰
注意这里还要再排序处理一下
#include<cstdio>
#include<algorithm>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 500;
const int MAXM = 4e4 + 10;
struct node
{
int h, k, m;
bool operator < (const node& rhs) const
{
return m < rhs.m;
}
}a[MAXN];
int f[MAXM], n, mm;
void update(int i, int h)
{
for(int j = a[i].m; j >= h; j--)
f[j] |= f[j-h];
}
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%d%d%d", &a[i].h, &a[i].m, &a[i].k);
mm = max(mm, a[i].m);
}
sort(a + 1, a + n + 1);
f[0] = 1;
_for(i, 1, n)
{
int t = 1;
while(a[i].k > t * 2 - 1) // t * 2 - 1表示目前所有的和
{
update(i, a[i].h * t);
t <<= 1;
}
t = a[i].k - (t - 1); // t - 1示目前所有的和
update(i, a[i].h * t);
}
while(!f[mm]) mm--;
printf("%d\n", mm);
return 0;
}
P1504 积木城堡
01背包推出所有可能即可
昨晚晚睡了,现在状态迷离
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 110;
const int MAXM = 1e4 + 10;
int f[MAXN][MAXM], w[MAXN], ans = 1e9, n;
int main()
{
scanf("%d", &n);
_for(k, 1, n)
{
int cnt = 0, x, m = 0;
while(1)
{
scanf("%d", &x);
if(x == -1) break;
w[++cnt] = x; m += x;
}
f[k][0] = 1;
ans = min(ans, m);
_for(i, 1, cnt)
for(int j = m; j >= w[i]; j--)
f[k][j] |= f[k][j-w[i]];
}
while(1)
{
int flag = 1;
_for(i, 1, n)
if(!f[i][ans])
{
flag = 0;
break;
}
if(flag)
{
printf("%d\n", ans);
break;
}
ans--;
}
return 0;
}
最长不下降子序列
现在开始复习简单的dp
一定要独立思考,自己想清楚
n方做法很显然,复习了nlogn的做法
这个做法的亮点是d数组
表示长度为i的最长不下降子序列的最小末尾元素
很骚
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e3 + 10;
int a[MAXN], d[MAXN], n, len;
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
d[1] = a[1]; len = 1;
_for(i, 2, n)
{
if(a[i] >= d[len]) d[++len] = a[i];
else d[upper_bound(d + 1, d + len + 1, a[i]) - d] = a[i];
}
printf("%d\n", len);
return 0;
}
10.15 周四
P1439 【模板】最长公共子序列
本来想练一下模板的
没想到要用nlogn的做法
这题给的是1到n的全排列,很特殊
方法很秀,强行有序
就是把第一个序列都标号1到n
然后第二个序列就改成对应的id
然后第二个序列额最长不下降子序列就是答案
太秀了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 1e5 + 10;
int a[MAXN], b[MAXN], id[MAXN], n;
int d[MAXN], len;
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]), id[a[i]] = i;
_for(i, 1, n)
{
int x; scanf("%d", &x);
b[i] = id[x];
}
d[1] = b[1]; len = 1;
_for(i, 2, n)
{
if(b[i] >= d[len]) d[++len] = b[i];
else d[upper_bound(d + 1, d + len + 1, b[i]) - d] = b[i];
}
printf("%d\n", len);
return 0;
}
学会从数据范围反推时间复杂度
10.17 周六
昨天很疲惫就没写题了。可以接受,学会劳逸结合。
P1802 5倍经验日
01背包变形
可以看作分组背包,每一组有两种策略
或者01背包,有两种更新方法
注意竟然有可能费用为0,这里坑了我挺久
看数据范围时不仅仅看最大来看时间复杂度
还要看最小的,比如可能为0,这里有坑
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int MAXM = 1e3 + 10;
const int MAXN = 1e3 + 10;
int f[MAXM], w[MAXN], v1[MAXN], v2[MAXN], n, m;
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n) scanf("%d%d%d", &v1[i], &v2[i], &w[i]);
_for(i, 1, n)
for(int j = m; j >= 0; j--)
{
if(w[i] == 0) f[j] += v2[i];
else
{
f[j] += v1[i];
if(j >= w[i]) f[j] = max(f[j], f[j-w[i]] + v2[i]);
}
}
printf("%lld\n", (ll)f[m] * 5);
return 0;
}
P2196 挖地雷
看到数据范围直接暴搜
这里的记录路径需要学
不一定逆推,顺推也可以,看情况
每次找最优的记录路径
注意每次搜的时候初始化数组,这些细节要注意
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 25;
int v[MAXN], vis[MAXN], Map[MAXN][MAXN];
int r[MAXN], k[MAXN], flag, ans, st, n;
int dfs(int x)
{
vis[x] = 1;
int ma = 0;
_for(i, 1, n)
if(Map[x][i] && !vis[i])
{
int t = dfs(i);
if(ma < t) ma = t, r[x] = i;
}
vis[x] = 0;
return v[x] + ma;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &v[i]);
_for(i, 1, n)
_for(j, i + 1, n)
scanf("%d", &Map[i][j]);
_for(i, 1, n)
{
memset(r, 0, sizeof(r));
int t = dfs(i);
if(ans < t)
{
ans = t, st = i;
_for(i, 1, n) k[i] = r[i];
}
}
for(int i = st; i; i = k[i]) printf("%d ", i);
printf("\n%d\n", ans);
return 0;
}
10.18 周日
编程才是我真正热爱的东西
真正的享受在其中
加油
P2196 挖地雷
这道题用搜索做出,后来开始想动规的做法
一开始想到f[i][j]表示前i个点,以j为终点的状态
但是发现如果用j为终点这样更新有问题
然后就懵逼了
因为这道题已经做出,就懒得再想了,所以看了别人的动规写法
发现就是我原来想的这样,此外i这一维没有用,可以省掉(写出方程可以发现)
为什么可行呢
后来我发现是因为这个题目的奇葩设定
这里的边一定是单向边
而且一定是从标号小的到标号大的
这种图我还是第一次见
就是这种标号的单向性使得这种做法可行
不过我看好像评论区没有人说到这点
这个做法完全是得益于这个奇葩数据,正常一点的数据这个做法是不行的
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 25;
int v[MAXN], Map[MAXN][MAXN], r[MAXN], f[MAXN], n;
void print(int x)
{
if(!x) return;
print(r[x]);
printf("%d ", x);
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &v[i]);
_for(i, 1, n)
_for(j, i + 1, n)
scanf("%d", &Map[i][j]);
_for(i, 1, n)
{
f[i] = v[i];
_for(j, 1, i - 1)
if(Map[j][i] && f[i] < f[j] + v[i])
{
f[i] = f[j] + v[i];
r[i] = j;
}
}
int ans = 0, end;
_for(i, 1, n)
if(ans < f[i])
{
ans = f[i];
end = i;
}
print(end);
printf("\n%d\n", ans);
return 0;
}
P1434 [SHOI2002]滑雪
代码写完后干两件事 一从头到尾检查一遍,不要犯低级错误 二试一下极限小和极限大的数据
这道题我写了个记忆化搜索过了
还有另外一种动态规划解法
为了保持无后效性,将高度排序,然后就用类似最长不下降子序列n方的解法做就可以
动规中要考虑无后效性
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int MAXN = 110;
int a[MAXN][MAXN], f[MAXN][MAXN], n, m, ans;
int dir[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
int dfs(int x, int y)
{
int t = 0;
REP(i, 0, 4)
{
int xx = x + dir[i][0], yy = y + dir[i][1];
if(xx < 1 || yy < 1 || xx > n || yy > m || a[xx][yy] >= a[x][y]) continue;
if(f[xx][yy]) t = max(t, f[xx][yy]);
else t = max(t, dfs(xx, yy));
}
ans = max(ans, 1 + t);
return f[x][y] = 1 + t;
}
int main()
{
scanf("%d%d", &n, &m);
_for(i, 1, n)
_for(j, 1, m)
scanf("%d", &a[i][j]);
_for(i, 1, n)
_for(j, 1, m)
if(!f[i][j])
dfs(i, j);
printf("%d\n", ans);
return 0;
}
训练不要着急,不要忙着赶进度,衡量训练效果的不是题目数量,而是你究竟学到了什么。耐心一道题一道题踏踏实实独立做出做好总结,速度慢一点也没关系