一. 并查集
并查集总共有两种操作:
1.查找
2.合并
基础版
int n;
vector<int> parent;
void Init(int n) {
for (int i = 0; i < n; i++) {
int a;
cin >> a;
parent.push_back(a);
}
}
int find(int x) {//用来找祖先结点
if (parent[x] == x) {
return x;
}
else {
return find(parent[x]);
}
}
void merge(int x, int y) {
//注意!想让y合并到x,要先找到x和y各自的祖先结点,再进行合并
parent[find(y)] = find(x);;
}
signed main() {
cin >> n;
Init(n);
return 0;
}
路径压缩
int find(int x) {
if (parent[x] == x) {
return x;
}
else {
parent[x] = find(parent[x]);
//边查找变修改,使当前结点直属于其祖先结点
//也就是让祖先结点成为其父结点
return parent[x];
}
}
可简化为
int find(int x) {
return parent[x] == x ? x : (parent[x] = find(parent[x]));
}
按秩合并
void merge(int x, int y) {
//先查看哪边的深度更深,然后把浅的并到深的里面
//注意!用于比较的深度是一整棵树的深度
//所以要找到x和y的祖先结点,那里存着一整个树的深度
int rx = find(x);
int ry = find(y);
if (rx != ry) {
//先判断一下x和y的祖先结点是不是同一个
//如果是同一个结点,当然就没必要操作了
if (Rank[rx] < Rank[ry]) {
swap(rx, ry);//让rx成为较深的那个
//硬要用if else也行,但是不利于后面的操作
}
parent[ry] = rx;//ry并到rx里
if (Rank[rx] == Rank[ry]) {
//在合并前,两颗树的深度是一样的
//在ry合并到rx后,rx的深度会+1
Rank[rx] += 1;
}
}
}
通用模板
int n;
vector<int> parent;
vector<int> Rank;//在每个结点记录当前深度
void Init(int n) {
for (int i = 0; i < n; i++) {
int a;
cin >> a;
parent.push_back(a);
Rank.push_back(1);//一开始自己是自己的老大,深度为1
}
}
int find(int x) {
return parent[x] == x ? x : (parent[x] = find(parent[x]));
}
void merge(int x, int y) {
//先查看哪边的深度更深,然后把浅的并到深的里面
//注意!用于比较的深度是一整棵树的深度
//所以要找到x和y的祖先结点,那里存着一整个树的深度
int rx = find(x);
int ry = find(y);
if (rx != ry) {
//先判断一下x和y的祖先结点是不是同一个
//如果是同一个结点,当然就没必要操作了
if (Rank[rx] < Rank[ry]) {
swap(rx, ry);//让rx成为较深的那个
//硬要用if else也行,但是不利于后面的操作
}
parent[ry] = rx;//ry并到rx里
if (Rank[rx] == Rank[ry]) {
//在合并前,两颗树的深度是一样的
//在ry合并到rx后,rx的深度会+1
Rank[rx] += 1;
}
}
}
signed main() {
cin >> n;
Init(n);
return 0;
}
题型归纳:找是否有公共祖先!没了!
1.修复公路
Sample 1
Inputcopy | Outputcopy |
---|---|
4 4 1 2 6 1 3 4 1 4 5 4 2 3 | 5 |
分析:
各个村庄都是独立不相关的点,我理解成村庄在一条线性的线段上,以为村庄1到村庄5修了路,村庄234都可以到5。
因此我们可以知道,想要任意村庄能通车,每个村庄间都至少得修一条路,比如说有n个村庄,就要修n-1条路。
然后输入数据的时候总想着查重,但其实有些题没必要(因为稍后的操作可以自动避开),直接结构体一股脑输入就行了。(我记得之前做过有一题也是这样)
我们求的是最短通行时间,这是由要修的n-1条路中,时间耗费最久的那条路决定(因为所有路都是同时开始修)
因为求的时间尽可能短,可以想到结构体按时间 t 排序(额总之没什么思路就想想要不要预排序,可能有惊喜)
接下来构建并查集,如果当前遍历到的祖先结点是之前没出现过的,才并入(这不就去重了吗),也意味着新路出现了,cnt++
解释一下
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int n, m, k,cnt = 0;
int f[200000];
map<pair<int, int>, int> path;//默认初始化为0
int find(int x) { return f[x] == x ? x : (f[x] = find(f[x])); }
struct Node {
int x, y, t;
}node[200000];
bool cmp(Node a, Node b) {
return a.t < b.t;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
f[i] = i;
}
for (int i = 1; i <= m; i++) {
cin >> node[i].x >> node[i].y >> node[i].t;
}
sort(node + 1, node + 1 + m,cmp);
for (int i = 1; i <= m; i++) {
int rx = find(node[i].x);
int ry = find(node[i].y);
if (rx != ry) {
f[rx] = ry;
cnt++;
}
if (cnt==n-1) {
cout << node[i].t;
return 0;
}
}
cout << -1;
return 0;
}
2.一中校运会之百米跑
Sample 1
Inputcopy | Outputcopy |
---|---|
10 6 Jack Mike ASDA Michel brabrabra HeHe HeHE papapa HeY Obama Jack Obama HeHe HeHE brabrabra HeHe Obama ASDA papapa Obama Obama HeHE 3 Mike Obama HeHE Jack papapa brabrabra | No. Yes. Yes. |
基本上模板题,只是由int改成string。
需要注意的是,注意要想到可以用map,别用vector嵌套pair,不好用。
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int n, m, k;
map<string, string> mp;
string find(string s) {
if (mp[s] == s) return s;
mp[s] = mp[find(mp[s])];//注意这里,别写成find(s)陷入死循环
return mp[s];
}
void merge(string s1, string s2) {
string x1 = find(s1);
string x2 = find(s2);
if (x1 != x2) {
mp[x1] = x2;
}
}
signed main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
string s;
cin >> s;
mp[s] = s;
}
for (int i = 0; i < m; i++) {
string s1, s2;
cin >> s1 >> s2;
merge(s1, s2);
}
cin >> k;
while (k--) {
string s1, s2;
cin >> s1 >> s2;
if (find(s1) == find(s2)) cout << "Yes." << endl;
else cout << "No." << endl;
}
return 0;
}
3.The Suspects
Sample
Inputcopy | Outputcopy |
---|---|
100 4 2 1 2 5 10 13 11 12 14 2 0 1 2 99 2 200 2 1 5 5 1 2 3 4 5 1 0 0 0 | 4 1 1 |
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int n, m, k,cnt = 0;
int f[N];
int a[N];
int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
void merge(int x, int y) {
int rx = find(x), ry = find(y);
if (rx != ry) {
f[rx] = ry;
}
}
signed main() {
while (cin >> n >> m && (n != 0 || m != 0)) {
int cnt = 0;//0本身就是
memset(f, 0, sizeof(f));
for (int i = 0; i < n; i++) {
f[i] = i;
}
while (m--) {
cin >> k;
int x;
cin >> x;
for (int i = 1; i < k; i++) {
int y;
cin >> y;
merge(x, y);
}
}
for (int i = 0; i < n; i++) {
if (find(i) == find(0)) {//这里需要注意,因为0可能被并到别的数下面
cnt++; //除非上面并数时,限定了把大的数并到小的数下面
}
}
cout << cnt << endl;
}
return 0;
}
二. 线段树
基本模板(不用结构体版)
int a[] = { 1,3,5,7,9,11 };
int sz = 6;
int tree[N];
//以前学过,给定一个数组,将数组的数据排成大根堆或小根堆
//这个也类似,给个数组,构造一个线段树。但不同于堆,
//构造出来的线段树元素会比数组的元素多,而堆本质上只是将一组数据排序而已
void build_tree(int root, int start, int end) {
//都是下标。root是树的根结点下标。start和end属于a数组的下标
if (start == end) {
//找到叶结点了
tree[root] = a[start];
}
else {
int mid = (start + end) / 2;
int left_node = root * 2 + 1;
int right_node = root * 2 + 2;
build_tree(left_node, start, mid);
build_tree(right_node, mid + 1, end);
//排序左右子树。注意根结点的左孩子和右孩子的表达方式
tree[root] = tree[left_node] + tree[right_node];
}
}
void update_tree(int root, int start, int end, int index, int val) {
//a[index]要被修改成val
if (start == end) {
//走到最底层了,也就是叶子结点
a[index] = val;
tree[root] = val;
}
else {
int mid = (start + end) / 2;
int left_node = root * 2 + 1;
int right_node = root * 2 + 2;
if (index >= start && index <= mid) {
update_tree(left_node, start, mid, index, val);
}
else {
update_tree(right_node, mid + 1, end, index, val);
}
tree[root] = tree[left_node] + tree[right_node];
}
}
int query_tree(int root, int start, int end, int L, int R) {
cout << "start:" << start << endl;
cout << "end:" << end << endl << endl;
if (R<start || L>end) {
//没有交集
return 0;
}
else if (start >= L && end <= R) {
//完全被包含
return tree[root];
}
else if (start==end) {
//已经到叶节点了,且这个叶结点也在要求的范围内
return tree[root];
}
else {
//既不是叶结点,又有交集,又不是被包含
//所以还要再细分
int mid = (start + end) / 2;
int left_node = root * 2 + 1;
int right_node = root * 2 + 2;
int sum_left = query_tree(left_node, start, mid, L, R);
int sum_right = query_tree(right_node, mid + 1, end, L, R);
return sum_left + sum_right;
}
}
signed main() {
//每次进行操作,都必须告知函数,树的根结点以及数组的开头结尾。为了递归能实现。
build_tree(0, 0, sz - 1);
update_tree(0, 0, sz - 1, 4, 6);
//for (int i = 0; i < 15; i++) cout << tree[i] << endl;
cout << query_tree(0, 0, sz - 1, 2, 5);
return 0;
}
1.线段树
Sample 1
Inputcopy | Outputcopy |
---|---|
5 5 1 5 4 2 3 2 2 4 1 2 3 2 2 3 4 1 1 5 1 2 1 4 | 11 8 20 |
简单模板。涉及区间查询和区间修改和懒标记
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
#define lson x << 1
#define rson x << 1 | 1
const int N = 1e5 + 10;
struct node {
ll l, r, sum, lazy;
}tree[N << 2];
int a[N];
//void push_up()
void build(int x, int l, int r) {
tree[x] = { l,r,0,0 };
if (l == r) {
tree[x].sum = a[l];
return;
}
int mid = (l + r) / 2;
build(lson, l, mid);
build(rson, mid + 1, r);
tree[x].sum = tree[lson].sum + tree[rson].sum;
}
void push_down(int x) {
if (tree[x].lazy) {
tree[lson].lazy += tree[x].lazy;
tree[rson].lazy += tree[x].lazy;
//如果传进来根节点的x区间是[4,7],说明4到7的区间下的子树都需要打上懒标记
tree[lson].sum += tree[x].lazy * (tree[lson].r - tree[lson].l + 1);
tree[rson].sum += tree[x].lazy * (tree[rson].r - tree[rson].l + 1);
tree[x].lazy = 0;//懒标记已经传给下一层了,父节点清零
}
}
void add(int x, int l, int r, int val) {
//区间修改
if (tree[x].l >= l && tree[x].r <= r) {
tree[x].sum += val * (tree[x].r - tree[x].l + 1);
tree[x].lazy += val;
//只有完全覆盖时才将区间打上懒标记,
//打上后不一定马上就将该区间下的子树都更新,
//而是先标记,后续遍历到有懒标记的结点才将懒标记传给左右子树,
//并更新左右子树的值。
//同样的,左右子树也不一定会将懒标记一次性传到底层,
//主要是看当前遍历到哪个结点,才把懒标记传到该节点的下一层
return;
}
push_down(x);
if (tree[lson].r >= l) add(lson, l, r, val);
if (tree[rson].l <= r) add(rson, l, r, val);
tree[x].sum = tree[lson].sum + tree[rson].sum;
}
//区间查询
ll query(int x, int l, int r) {
if (tree[x].l >= l && tree[x].r <= r)
{
return tree[x].sum;
}
if (tree[x].l > r || tree[x].r < l) {
return 0;
}
push_down(x);//为什么查询也要下沉
ll sum = 0;
if (tree[lson].r >= l) sum += query(lson, l, r);
if (tree[rson].l <= r) sum += query(rson, l, r);
return sum;
}
//void add_point(int x,int )
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
build(1, 1, n);
while (m--)
{
int flag, x, y, k;
cin >> flag;
if (flag == 1)
{
cin >> x >> y >> k;
add(1, x, y, k);
}
else
{
cin >> x >> y;
cout << query(1, x, y) << endl;
}
}
return 0;
}
三. 树状数组
功能:
1.求前缀和 2.求区间和 3.单点修改 4.区间更新(可以但不好用,要用线段树)
有个特点:数组c的下标i,可以用来表示c[i]的区间长度。
模板
int a[N];
int c[N];
int lowerbit(int i) {
//求c[i]的区间长度
return (-i) & i;
}
int sum(int i) {
//求前缀和,将各个直接前驱相加
//怎么找到i的直接前驱呢?
//因为c[i]的区间长度是lowerbit(i)
//所以 i - lowerbit(i) 就是i的直接前驱
int ans = 0;
for (; i > 0; i -= lowerbit(i)) {
ans += c[i];
}
return ans;
}
int query(int i, int j) {
//求区间和
return sum(j) - sum(i - 1);
}
void add(int i, int val) {
//点更新,将i及其所有后驱都加上val
for (; i <= n ; i += lowerbit(i)) {
c[i] += val;
}
}
signed main() {
//建树
for (int i = 1; i <= n; i++) {
//注意数组c要从1开始
cin>>a[i];
add(i, a[i]);
}
return 0;
}
1. 逆序对
Sample 1
Inputcopy | Outputcopy |
---|---|
6 5 4 2 6 3 1 | 11 |
如果数字在1e6左右,直接这样做就行了
总结的来说,本质上是哈希表,就是先将树状数组初始化为0,代表所有数都还没出现过。
每出现了一个数x,就在hash[x]++。
那么逆序对怎么计算?
假设是从数组后面往前遍历(从前往后同理),遍历的下标是递减的,因此如果hash中有值为1,一定是在这之前出现过的(下标大于当前的)。这样就满足了其中一个逆序对的条件。然后再看看这个值是不是小于当前遍历的数,如果是,满足了逆序对的两个条件。
假设我们有一个数组 nums = [7, 5, 6, 4]
,现在我们要统计该数组中的逆序对个数。
首先,我们定义一个长度为 5 的树状数组 bit
,初始化所有元素为 0:bit = [0, 0, 0, 0, 0]
。这是因为数组 nums
的最大值是 7,所以树状数组的长度需要至少是 8(比最大值多 1)。
然后,我们从后向前遍历数组 nums
。
-
当处理到
nums[3] = 4
时:- 我们查询在树状数组
bit
中比 4 小的元素个数,即query(4)
,由于此时bit[4] = 0
,表示在 4 之前没有比它小的元素。 - 然后,我们将 4 加入树状数组
bit
中,即update(4, 4)
。这会将bit[4]
的值自增 1,结果变为 1。 - 继续下一个元素。
- 我们查询在树状数组
-
当处理到
nums[2] = 6
时:- 我们查询在树状数组
bit
中比 6 小的元素个数,即query(6)
,由于此时bit[6] = 1
,表示在 6 之前有一个比它小的元素,即 4。 - 然后,我们将 6 加入树状数组
bit
中,即update(6, 4)
。这会将bit[6]
的值自增 1,结果变为 2。 - 继续下一个元素。
- 我们查询在树状数组
-
当处理到
nums[1] = 5
时:- 我们查询在树状数组
bit
中比 5 小的元素个数,即query(5)
,由于此时bit[5] = 1
,表示在 5 之前有一个比它小的元素,即 4。 - 然后,我们将 5 加入树状数组
bit
中,即update(5, 4)
。这会将bit[5]
的值自增 1,结果变为 2。 - 继续下一个元素。
- 我们查询在树状数组
-
当处理到
nums[0] = 7
时:- 我们查询在树状数组
bit
中比 7 小的元素个数,即query(7)
,由于此时bit[7] = 2
,表示在 7 之前有两个比它小的元素,分别是 4 和 5。 - 然后,我们将 7 加入树状数组
bit
中,即update(7, 4)
。这会将bit[7]
的值自增 1,结果变为 3。 - 继续下一个元素。
- 我们查询在树状数组
完成整个遍历后,我们统计到树状数组 bit
的状态为 [0, 0, 0, 1, 2]
。最后,我们将查询到的前缀和累加起来,得到逆序对的个数:0 + 0 + 0 + 1 + 2 = 3。
所以,数组 nums
中的逆序对个数为 3。
但是!序列中每个数字最大是1e9,不能开一个这么大的数组。
所以要离散化。
具体操作为先按val优先排升序,然后把val改成1-n。这样就可以开数组了(最大占空间1e5)
然后再按事先存的原下标一个个插入ranks数组。
有个排序的注意点
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
//#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int tree[500010], ranks[500010], n;
long long ans;
struct point
{
int num, val;
}a[500010];
int c[N];
inline bool cmp(point q, point w)
{
if (q.val == w.val)
return q.num < w.num;
return q.val < w.val;
}
int lowerbit(int i) {
//求c[i]的区间长度
return (-i) & i;
}
void add(int i, int val) {
//点更新,将i及其所有后驱都加上val
for (; i <= n; i += lowerbit(i)) {
c[i] += val;
}
}
int sum(int i) {
//求前缀和,将各个直接前驱相加
//怎么找到i的直接前驱呢?
//因为c[i]的区间长度是lowerbit(i)
//所以 i - lowerbit(i) 就是i的直接前驱
int ans = 0;
for (; i > 0; i -= lowerbit(i)) {
ans += c[i];
}
return ans;
}
int query(int i, int j) {
//求区间和
return sum(j) - sum(i - 1);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i].val), a[i].num = i;
sort(a + 1, a + 1 + n, cmp);//升序
for (int i = 1; i <= n; i++) {
a[i].val = i;
ranks[a[i].num] = a[i].val;//离散化
}
for (int i = 1; i <= n; i++) {
add(ranks[i], 1);
ans += i-query(1, ranks[i]);//或ans += query(ranks[i]+1,n);
}
cout << ans;
return 0;
}
四. 递推
1.火车站
分析:读完题目发现,唯一一个未知量是第二站上车人数,已知量是最后一站下车m人,也就是倒数第二站发车的时候有m人。本题的递推公式题目也很清楚的说明了。所以遍历b的值,找到答案。
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int dp[N];
int getin[N],getoff[N];//每站上车人数
const int mod = 10007;
signed main()
{
int a, n, m, x;
cin >> a >> n >> m >> x;
dp[1] = a, dp[2] = a;
getin[1] = a;
getoff[1] = 0;
//关键在于第二站上车多少个人不知道,因此要逆推
//设为b吧,b<=a
for (int b = 0; b <= 10005; b++) {
getin[2] = b, getoff[2] = b;
for (int i = 3; i <= n; i++) {
getin[i] = getin[i - 1] + getin[i - 2];
getoff[i] = getin[i - 1];
dp[i] = dp[i - 1] + getin[i] - getoff[i];
}
if (dp[n - 1] == m) {
cout << dp[x];
return 0;
}
}
return 0;
}
2. 平面分割
分析:
当线交于一点时,每增加一条线,增加两块区域
当线不交于一点时,每增加一条线,最多增加n块区域(n是加上新加的线后,总共n条线)
int dp[N];
const int mod = 10007;
signed main()
{
int n, p;
cin >> n >> p;
int ans = 2 * p;
for (int i = p + 1; i <= n; i++) {
ans += i;
}
cout << ans;
return 0;
}
3.兔子繁殖
分析:兔子出生的那月算第一个月 ,那么要第三个月才开始生兔宝宝。
递推公式:当前月兔子 = 上个月兔子 + 这个月成年兔生的幼兔
上个月兔子好知道,就是dp[i-1]
但是这个月成年兔子是多少?毕竟只有知道了成年兔,才知道生了多少幼兔。
那么dp[i-2]就代表这个月的成年兔。
因为 i-2 月的兔子有成年兔,也有幼兔,但是两个月过后,那些幼兔都已经长成成年兔了,所以i月的成年兔是dp[i-2]。
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int a[100][100];
int dp[1005];
signed main()
{
int n;
cin >> n;
dp[1] = 1, dp[2] = 1;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
cout << dp[n];
return 0;
}
4.昆虫繁殖
傻逼题目,那句话应该是:每对成虫过x月,每月产y对卵。
这题根上一题略有不一样,因为上一题求的是所有兔子的数量(也包括幼兔)
但这题求的是成虫数量(不包括卵),相当于求的是上一题成年兔的数量
注意!卵要过两个月长到成虫,然后成虫要再过x个月才能开始产卵
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int c[N], r[N];//每月成虫数和卵数
signed main()
{
int x, y, z;
//间隔,每次产卵数,z月后(注意是z月后,不是第z月。本题把第0月当成第一个月)
cin >> x >> y >> z;
for (int i = 0; i <= x - 1; i++) {
c[i] = 1;
r[i] = 0;
}
c[x] = 1;
r[x] = y;
for (int i = x + 1; i <= z; i++) {
r[i] = c[i - x] * y;
c[i] = c[i - 1] + r[i - 2];
}
cout << c[z];
return 0;
}
五. 动态规划
较常见的动态规划有三种题型:
可选的物品数量有限:01背包,多重背包
可选的物品数量无限:完全背包
注意!对于背包容量/预算金额这样的元素
如果是01背包和多重背包,背包容量要从后往前遍历
如果是完全背包,背包容量要从前往后遍历
还会有背包问题的变式,需要具体问题具体分析
至于其他诸多类型的dp,很多跟背包问题类似或者能利用背包问题的思维来解决
1.Bone Collector
骨头数,袋子体积
骨头价值
骨头体积
求能带走最大价值是多少
分析:设一个dp[i][j],i代表若有i个骨头,j代表若有j大小的空间。
跟一般的一维dp不同,比如说求的是“第n天最多有多少只兔子”,那么i就代表第i天,dp[i]代表第i天最多的兔子。我们输入的只有一个变量——天数
但是这题不一样,我们输入的有两个属性,骨头数和袋子体积。因此要用二维dp记录这两个变量各种变化的时候所代表的最大价值。
设一个dp[i][j],i代表若有i个骨头,j代表若有j大小的空间。dp[i][j]代表当骨头有i个,袋子体积为j时能带走的最大价值
本题属性:骨头数,袋子体积
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int dp[1010][1010];//前i个物品,目前背包容量
struct Bone {
int val, v;
}bone[1010];
signed main()
{
int T;
cin >> T;
while (T--) {
int num, bag;
cin >> num >> bag;
for (int i = 1; i <= num; i++) {
cin >> bone[i].val;
}
for (int i = 1; i <= num; i++) {
cin >> bone[i].v;
}
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= num; i++) {
for (int j = 0; j <= bag; j++) {
//骨头体积有可能为0,很坑。所以背包容量为0时也有可能装骨头
if (bone[i].v > j) {
//当前骨头体积肯定装不下,没有选择余地
dp[i][j] = dp[i - 1][j];
}
else {
//可以选择要不要装当前这块骨头
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - bone[i].v] + bone[i].val);
}
}
}
cout << dp[num][bag] << endl;
}
return 0;
}
二维dp可以压缩成一维dp。
这是二维dp的表格(虽然不是这题的)。
再看二维dp公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - bone[i].v] + bone[i].val);
假设i是行,j是列。发现只用到了第i-1行,而1---(i-1)行已经没用了,到后面也没用,所以可以一直覆盖来压缩成一维。
公式:dp[j] = max (dp[j] , dp[j-c[i].v] + c[i].w);
这个dp[j]就相当于dp[i-1][j]
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[1111];
struct node
{
int v,w;
}c[1111];
int main()
{
int u;
int n,v;
scanf ("%d",&u);
while (u--)
{
scanf ("%d %d",&n,&v);
memset (dp,0,sizeof (dp));
for (int i = 1 ; i <= n ; i++) //读题要注意,这俩别反了
scanf ("%d",&c[i].w);
for (int i = 1 ; i <= n ; i++)
scanf ("%d",&c[i].v);
for (int i = 1 ; i <= n ; i++)
{
for (int j = v ; j >= c[i].v ; j--)
{
dp[j] = max (dp[j] , dp[j-c[i].v] + c[i].w);
}
}
printf ("%d\n",dp[v]);
}
return 0;
}
2. Coin Change
思路跟上一题一模一样,有两个限制条件,二维dp
但这题是完全背包问题,即可选的硬币有无限个
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
//只用前i(5)个硬币,金额(250)
int dp[1000][1000];//1,5,10,25,50
int coin[6] = { 0,1,5,10,25,50 };
signed main()
{
int n;
memset(dp, 0, sizeof(dp));
for (int i = 0; i <= 5; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 251; j++) {
if (coin[i] > j)
{
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] += dp[i - 1][j];
dp[i][j] += dp[i][j - coin[i]];
}
}
}
while (cin >> n) {
cout << dp[5][n] << endl;
}
return 0;
}
一维写法。
注意!这题跟上一题不一样,这题求的是最大方案数, 因此选和不选的方案要加起来然后放到dp[j]
//前i(5)个硬币,金额(250)
int dp[1000];//1,5,10,25,50
int coin[6] = { 0,1,5,10,25,50 };
signed main()
{
int n;
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 251; j++) {
//如果写成for (int j = coin[i]; j <= 251; j++),这个if的判断就可以省略了
if (coin[i] > j)
{
continue;//不操作,dp[j]没变,也就是dp[i-1][j]
}
else {
//有得选择
dp[j] = dp[j] + dp[j - coin[i]];
//注意,题目求的是方案数,所以当前最大方案数 = 选的方案数+不选的方案数
}
}
}
while (cin >> n) {
cout << "答案:" << dp[n] << endl;
}
return 0;
}
但如果加上个限制条件,选的硬币不能超过100枚,总共有三个限制条件,就一定要用上面的一维dp进行优化,再加个变量,即已选择的硬币数k,然后变成二维dp。
其本质上是三维dp!!!
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int coin[5] = { 1,5,10,25,50 }/*下标从零开始,前面加个0*/;
int dp[110][260];//最多用上k个硬币,金额j
signed main() {
int n;
while (cin >> n) {
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for (int i = 0; i < 5; i++) {//物种货币
for (int k = 1; k <= 100; k++) {
for (int v = coin[i]; v <= n; v++) {
dp[k][v] += dp[k - 1][v - coin[i]];
}
}
}
int ans = 0;
for (int k = 0; k <= 100; k++) {//切记!n可能为0,一枚硬币都不用选!所以k=0不能漏
ans += dp[k][n];
}
cout << ans << endl;
}
return 0;
}
3.庆功会
#include <vector>
#include<string>
#include<algorithm>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//#define int long long
#include<math.h>
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
struct Node {
int price, val, num;
//价格,价值,可购买数量
}node[7000];
int dp[7000];//预算,购买数量
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> node[i].price >> node[i].val >> node[i].num;
}
for (int i = 1; i <= n; i++) {//物品
for (int j = m; j >= node[i].price; j--) {//一定要从后往前
for (int k = 0; k <= node[i].num && k * node[i].price <= j; k++) {
//第i件物品可购买的数量
//预算
dp[j] = max(dp[j], dp[j - k * node[i].price] + k * node[i].val);
}
}
}
cout << dp[m];
return 0;
}
3. Frog
Sample
Inputcopy | Outputcopy |
---|---|
1 4 1 2 2 1 2 3 4 | 8 |
题意:从头开始只能往后跳,最多跳k下,求能吃最大的分数
分析:跟上一题的变式一样,本题是本质上三维。
本题的属性:池塘长度,可跳跃步数
dp[i][j],i代表池塘的长度为i(i<=n),j代表可以跳j步(j<=k),dp[i][j]代表当池塘长度为i,可以跳j步时,青蛙能吃的最多的分数。
还有一个地方要强调,例如图中的j的循环,因为要用到i,所以一定要嵌套在i的循环里面
#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 105
using namespace std;
int dp[N][N];//池塘长度,跳跃步数
int book[N];
int main()
{
int n,A,B,K,T;
scanf("%d",&T);
while(T--)
{
int sum=0;
scanf("%d%d%d%d",&n,&A,&B,&K);
for(int i=0;i<n;i++)
{
scanf("%d",&book[i]);
}
dp[0][0]=book[0];//别忘了一开始的初始化!
for(int i=A;i<n;i++)//池塘最短要为A(青蛙跳跃最短的距离),否则青蛙跳不了
{
for(int k=1;k<=K;k++)//k=0相当于青蛙没跳,可以从1开始(从0开始也无所谓)
{
for(int j=A;j<=i&&j<=B;j++)//每一步青蛙可以跳的距离是[A,B]
{
dp[i][k]=max(dp[i-j][k-1]+book[i],dp[i][k]);
}
}
}
for(int i=1;i<=K;i++)
{
sum=max(dp[n-1][i],sum);
}
printf("%d\n",sum);
}
return 0;
}
4.Common Subsequence
最长公共子序列(LCS)模板题
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
//int dp[2000][2000];//属性:s1长度,s2长度
int longestCommonSubsequence(string text1, string text2) {
int dp[1005][1005] = { 0 };
//属性:test1的长度,test2的长度
for (int i = 1; i <= text1.size(); i++) {
for (int j = 1; j <= text2.size(); j++) {
//比较两个字符串末尾
if (text1[i - 1] == text2[j - 1]) {
//如果相等,公共子序列长度肯定加1
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.size()][text2.size()];
}
signed main() {
string s1, s2;
while (cin >> s1 >> s2) {
//求最长公共子序列
cout << longestCommonSubsequence(s1, s2) << endl;
}
return 0;
}
5.最少拦截系统
简单的贪心
他妈的,真是气死我了,题目看错了两个地方
1.多组数据
2.每组数据开头是n枚导弹
跟个脑残一样不知道哪里错了浪费一堆时间,真的脑残
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int dp[30005];
int a[30005];
int state[30005] = { 0 };
//已发射导弹的目前最高发射高度,
//因为新添加的导弹一定是为了解决之前导弹解决不了的高度
//所以state里一定是递增的
signed main() {
int n = 0;
while (cin >> n) {
for (int i = 0; i < n; i++) {
cin >> a[i];
}
memset(state, -1, sizeof(state));
int cnt = 0;
for (int i = 0; i < n; i++) {
int k = 0;
while (k <= cnt && state[k] < a[i]) {
k++;
//贪心地找到了state里能解决当前导弹的最小高度系统,然后跳出循环
//如果没找到,说明目前的所有系统都不能解决当前的导弹
//要加一个系统
}
state[k] = a[i];
if (k > cnt) cnt++;
}
cout << cnt << endl;
}
return 0;
}
6.Super Jumping! Jumping! Jumping!
分析:求最长严格上升子序列的和
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
int a[N];
int state[30005] = { 0 };
int dp[N];
signed main() {
int n = 0;
while (cin >> n && n != 0) {
for (int i = 1; i <= n; i++) {
cin >> a[i];
dp[i] = a[i];//这里就要初始化
}
int ans = -1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (a[i] > a[j]) {
dp[i] = max(dp[j] + a[i], dp[i]);
}
}
ans = max(ans, dp[i]);
}
cout << ans << endl;
}
return 0;
}
7.ACM排名
#include <vector>
#include<string>
#include<algorithm>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//#define int long long
#include<math.h>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
const int N = 1e6 + 10;
int a[N];
int dp[N];//预算
struct Node {
int score, cost;
}node[10005];
int main()
{
int t, n;
cin >> t >> n;
for (int i = 1; i <= n; i++) {
cin >> node[i].score >> node[i].cost;
}
for (int i = 1; i <= n; i++) {
//完全背包问题(无限个),是从前往后遍历
//01背包和多重背包都是有限个,是从后往前遍历
for (int j = node[i].cost; j <= t; j++) {//时间
dp[j] = max(dp[j], dp[j - node[i].cost] + node[i].score);
}
}
cout << dp[t];
return 0;
}
8.分组问题
样例输入 Copy
10 6 3 2 1 1 3 3 1 4 8 2 6 9 2 2 8 3 3 9 3
样例输出 Copy
20
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
//#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Node {
vector<int> w;//重量
vector<int> v;//价值
}node[1000];
int dp[300];//容量
signed main()
{
int bag, n, t;
cin >> bag >> n >> t;
for (int i = 1; i <= n; i++) {
int w, v, num;
cin >> w >> v >> num;
node[num].w.push_back(w);
node[num].v.push_back(v);
}
//多重背包+01背包,有限个数,容量从后往前
for (int i = 1; i <= t; i++) {//组
for (int k = bag; k >= 0; k--) {
for (int j = 0; j < node[i].w.size(); j++) {//遍历组内的操作必须在内层
//一组里只能选一个
int weight = node[i].w[j];
int val = node[i].v[j];
if (weight <= k) {
dp[k] = max(dp[k], dp[k - weight] + val);
}
}
}
}
cout << dp[bag];
return 0;
}
9.打包
样例输入 Copy
6 5 4 10 2 2 20 3 2 40 4 3 30 3 3
样例输出 Copy
50
二维费用背包模板题
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#include<iostream>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
//#include<bits/stdc++.h>
const int N = 1e6 + 10;
//#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Node {
int score, w, v;
}node[1000];
int dp[1000][1000];//体积,重量
signed main()
{
int V, W;
cin >> W >> V;
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
int a, b, c;
cin >> a >> b >> c;
node[i] = { a,b,c };
}
//01背包,每个物品只有一件,从后往前遍历
for (int i = 1; i <= n; i++) {//物品
for (int j = V; j >= node[i].v; j--) {//体积
for (int k = W; k >= node[i].w; k--) {//重量
dp[j][k] = max(dp[j][k], dp[j - node[i].v][k - node[i].w] + node[i].score);
}
}
}
cout << dp[V][W];
return 0;
}
六. 字符串操作和哈希表
1.Post Robot
Sample
Inputcopy | Outputcopy |
---|---|
Apple bananaiPad lemon ApplepiSony 233 Tim cook is doubi from Apple iPhoneipad iPhone30 is so biiiiiiig Microsoft makes good App. | MAI MAI MAI! MAI MAI MAI! MAI MAI MAI! SONY DAFA IS GOOD! MAI MAI MAI! MAI MAI MAI! MAI MAI MAI! |
#include<set>
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#define inf 0x3f3f3f3f
#define int long long
const int N = 20017;
//#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#include<iostream>
using namespace std;
string word[5] = { "Apple","iPhone","iPod","iPad","Sony" };
signed main()
{
string str;
while (getline(cin, str)) {
istringstream s(str);
string s2;
while (s >> s2) {
int flag = 0;
for (int i = 0; i < 5; i++) {
if (s2.find(word[i]) != -1) {
if (word[i] == "Sony") {
cout << "SONY DAFA IS GOOD!" << endl;
}
else {
cout << "MAI MAI MAI!" << endl;
}
s2.erase(s2.find(word[i]), word[i].size() - 1);
//消除已查询过的关键词
}
}
}
}
return 0;
}
2.Four Operations
3.Remove Two Letters
Sample 1
Inputcopy | Outputcopy |
---|---|
7 6 aaabcc 10 aaaaaaaaaa 6 abcdef 7 abacaba 6 cccfff 4 abba 5 ababa | 4 1 5 3 3 3 1 |
分析:将相邻的两个字母去掉,然后统计不重复的字符串有多少个。
一开始是用erase和map暴力做的,超时了。
正确的解法应该是找规律。
拿abacaba举例,
如果去掉ab,结果是acaba
如果去掉ba,结果是acaba
稍加验证得知,如果 str[i] == str[i+2],那么去掉str[i],str[i+1]和去掉str[i+1],str[i+2]产生的结果一定是一样的,是有重复的。
因为最大不重复字符串的个数是 n-1个(n是字符串长度),然后统计有重复的情况共cnt种,
答案就是 n-1-cnt
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#define inf 0x3f3f3f3f
#define int long long
const int N = 20017;
//#include <bits/stdc++.h>
typedef long long ll;
#include<iostream>
using namespace std;
signed main()
{
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
string str;
cin >> str;
int ans = n - 1;
for (int i = 0; i < n - 2; i++) {
if (str[i] == str[i + 2]) ans--;
}
cout << ans << endl;
}
return 0;
}
4.A-B
方法1:map映射法,哈希
简单分析:数据太大了,2的30次方,不能用数组哈希。所以要用map哈希
这是我自己写的,有一个点wal不知道为什么
#include <bits/stdc++.h>
using namespace std;
map<int, int> a;//num,times
int main()
{
int n, c;
cin >> n >> c;
for (int i = 0; i < n; i++) {
int input;
cin >> input;
a[input]++;
}
int cnt = 0;
for (auto it = a.begin(); it != a.end(); it++) {
if (it->second == 0) continue;
cnt += it->second * a[c + it->first];
}
cout << cnt;
return 0;
}
这是题解的方法,更简洁
#include<set>
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#define inf 0x3f3f3f3f
#define int long long
const int N = 1e6+10;
//#include <bits/stdc++.h>
typedef long long ll;
#include<iostream>
using namespace std;
int a[N];
map<int, int> mp;
signed main()
{
//A-B = C
//A-C = B
int n, c;
cin >> n >> c;
for (int i = 0; i < n; i++) {
cin >> a[i];
mp[a[i]]++;
//a[i] 是A,所以要找一个B才能满足公式
a[i] -= c;//使A变成B
}
int ans = 0;
for (int i = 0; i < n; i++) {
ans += mp[a[i]];
}
cout << ans << endl;
return 0;
}
方法2:stl库的lower_bound和upper_bound
#include<cstdio>
#include<set>
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#define inf 0x3f3f3f3f
#define int long long
const int N = 1e6+10;
//#include <bits/stdc++.h>
typedef long long ll;
#include<iostream>
using namespace std;
int a[N];
bool cmp(int b, int c) {
return b < c;//升序
}
signed main()
{
//A-B = C
//A-C = B
int n, c;
cin >> n >> c;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n, cmp);
int ans = 0;
for (int i = 0; i < n; i++) {
int target = a[i] - c;//A-C=B
auto lowerIt = lower_bound(a, a + n, target);
auto upperIt = upper_bound(a, a + n, target);
ans += upperIt - lowerIt;
}
cout << ans;
return 0;
}
方法3:双指针
也是预处理排序,然后正常双指针搜索,本质上和方法2一样。
5.小美的01串翻转
分析:
如何求一个01串的最小权值?
假设头为0或1,可以得到两种假设的操作数,取较小的一个就行
于是我用暴力,wa了
正确做法是双重循环遍历,边遍历边统计。
#include<set>
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>
#define inf 0x3f3f3f3f
#define int long long
const int N = 1e6+10;
//#include <bits/stdc++.h>
typedef long long ll;
#include<iostream>
using namespace std;
int a[N];
signed main()
{
string s;
cin >> s;
int n = s.size();
int ans = 0;
for (int i = 0; i < n; i++) {
//从i开始
int cnt0 = 0, cnt1 = 0;
for (int j = 0; j < n - i; j++) {
if (j % 2 == 0) {
//偶数位
if (s[i + j] != '0') {
cnt0++;
}
if (s[i + j] != '1') {
cnt1++;
}
}
else {
//奇数位
if (s[i + j] != '1') {
cnt0++;
}
if (s[i + j] != '0') {
cnt1++;
}
}
ans += min(cnt0, cnt1);
//在内层。相当于每种情况都有加上
//但如果起点不变,cnt0和cnt1是可以继续累加的
}
}
cout << ans;
return 0;
}