天梯赛补题
目录
DP
1、01背包+逆向
思路
可以发现背包的容量很大,但是获利比较少,所以可以采用01背包的逆过程
可以把获利看作背包容量,把原来的背包容量当作获利
也就是我们要求,在当前获利下,最少需要的背包容量是多少?
最后按照获利的总数逆序遍历,找到第一个体积容量小于等于背包容量的获利,就是答案
1 、未优化空间做法MLE
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, M = 1010 * 30;
int f[N][M], n, m;
int v[N], w[N];
int sum;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i];
for(int i = 1; i <= n; i ++ ) cin >> w[i], sum += w[i];
// cout << "sum: " << sum << endl;
//正向:f[i][j]在前i个物品中选,体积为j时所放的最大价值
//逆向:f[i][j]在前i个物品中选,价值为j时所需要的最小体积
//由于是求最小值,初始化为无穷大
memset(f, 0x3f, sizeof f);
for(int i = 1; i <= n ; i++ ) f[i][0] = 0;
for(int i = 1; i <= n; i ++ )
for(int j = 0; j <= sum; j ++ )
{
//当前物品(第i个)不选
//在前i个物品中选,价值为j的体积
//就等于在前i-1个物品中选,价值为j的体积
f[i][j] = f[i - 1][j];
//选当前物品
//在前i个物品中选,并选择第i个物品,价值为j
//相当于在前i-1个物品中选,价值为j-w[i]
if(j >= w[i]) f[i][j] = min(f[i][j], f[i - 1][j - w[i]] + v[i]);
}
// for(int i = sum; i >= 0; i -- )
// cout << f[i] << ' ';
// cout << endl;
//从最大价值开始往前找到第一个满足体积小于等于背包容量
for(int i = sum; i >= 0; i -- )
if(f[n][i] <= m)
{
cout << i << endl;
break;
}
return 0;
}
// 120 MB
2、优化空间做法AC
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, M = 1010 * 30;
int f[M], n, m;
int v[N], w[N];
int sum;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i];
for(int i = 1; i <= n; i ++ ) cin >> w[i], sum += w[i];
// cout << "sum: " << sum << endl;
//正向:f[i][j]在前i个物品中选,体积为j时所放的最大价值
//逆向:f[i][j]在前i个物品中选,价值为j时所需要的最小体积
//由于是求最小值,初始化为无穷大
memset(f, 0x3f, sizeof f);
f[0] = 0;
for(int i = 1; i <= n; i ++ )
for(int j = sum; j >= w[i]; j -- )
{
//当前物品(第i个)不选
//在前i个物品中选,价值为j的体积
//就等于在前i-1个物品中选,价值为j的体积
//选当前物品
//在前i个物品中选,并选择第i个物品,价值为j
//相当于在前i-1个物品中选,价值为j-w[i]
if(j >= w[i]) f[j] = min(f[j], f[j - w[i]] + v[i]);
}
//从最大价值开始往前找到第一个满足体积小于等于背包容量
for(int i = sum; i >= 0; i -- )
if(f[i] <= m)
{
cout << i << endl;
break;
}
return 0;
}
// 120 MB
2、背包DP+去重
思路 :
DP:
状态表示f[i][j]:
集合:在前i个字符中选,删掉j个
属性:不同字符串个数
状态计算:
当前字符删:f[i][j] += f[i - 1][j]
当前字符不删:f[i][j] += f[i - 1][j - 1]
DP的大题思路容易想到,难点在于去重。如果我们要删除重复的字符串,那么我们就要思考两个问题:
- 那些元素导致了出现重复?
- 重复的个数是多少?
我们先看问题1:那些元素导致出现了重复。
举例:
(1)例如字符串:abcded
假如我们当前遍历到最后一个字符d,并且删除了两个字符:ed,就就与在前5个字符中删除de就相同的,剩下的字符串都是abcd。
(2)再例如字符串:abcdded
假如我们当前遍历到最后一个字符d,并且删除了两个字符ed,那么这与在前6个字符当中删除de是一样的。如果还在这个位置但我们删除了三个字符ded,这与在前6个字符当中删去dde是一样的,并且对于删除{aed,ade},{bed,bde},{cde,ced}都是重复的。并且我们可以把上面三个删除都抽象为删除了{ed,de}。
规律:
(1)如果在删除下标为i,字符是ch 处有重复,那么肯定是因为在位置i 的前面也有一个位置j的字符也是ch,导致删除{ch---}与删除{---ch}是同样的效果。
其实就是这么一个字符串:{***ch---ch***},其中我们删除{ch---}和{---ch}是同样的效果,剩下的都是***ch***(注意上述*和-都代指很多个字符,而不是单个字符,但是他们的效果是一样的,只是为了区分要删除的字符(-)和不删除的字符(*),我们规定了不同的符号表示)。
(2)上述字符j 的位置并不是随意的。
我们上面也说了,必须删除{ch---}和{---ch}才会有重复,其中{---}是两个ch之间的全部字符,也就是说,我们可以删除的字符的数量一定要大于等于我们必须要删除的字符的数量(ch和ch之间的全部字符)。
(3)可能有多个位置导致重复。
在举例(2)中,我们发现删除三个字符时,不仅仅只有一种重复的情况,在位置i 前面的为一个与ch 相同字符的位置j 都会导致重复,但我们有必要都去重吗?
答案是没有必要,我们只需要去重离i 最近的位置j 导致的重复即可。
证明:反证法
如果我们在操作过程中遍历到每一个字符都会有去重操作,那么在遍历到字符i之前的i-1个字符,肯定是没有重复的。此时我们加入了一个字符ch,假设在它的前面还有两个ch(从左到右编号为1,2),那么位置i 的ch只可能与2号ch产生重复。因为如果该ch与1号ch产生重复,说明1号ch与2号ch也能产生重复,因为该ch与1号ch产生的重复是由于2号ch与1号ch产生的重复间接导致的!这与2号ch肯定与1号无重复就矛盾了
我们再从局面(状态)的角度深入理解一下为什么不需要考虑之前的:
例如下图,在前三个字符中删去两个字符,我们不需要考虑情况①②导致的重复,因为在它(在前3个字符中删除两个)的子局面1(前两个字符删两个字符)和子局面2(前两个字符删一个字符)中不会给它留下重复的字符串,即在子局面2中,它只会留下(删除第一个字符或者删除第二个字符)中的一个的局面,而不会把这两个局面都留下来,导致我们删除第一个a和第3个a与删除第2个a和第3个a产生重复,因为删除第一个a和删除第二个a这两个局面本质上已经不可能同时出现了! 所以说既删除第1个a和第3个a,又删除第2个a和第3个a也就不可能发生了!
问题2:重复的元素个数是多少
如下图,我们在删除位置i 的d时会与位置k 的d产生重复,此时需要减去的重复个数为f(3,0),因为我们已经把两个字符{e,d}都删完了,此时{ded}肯定是{d},那么可能变化的就是前面的{abc}了,由于此时只能再删2-2=0个字符,所以重复元素个数为f(3,0)
即:f[k-1][j - (i - k)]:k之前的字符中再删除(j - (i - k))个字符
上面我的理解可能有点混乱和繁琐,下面值一些人写的比较精简的理解:
(1)对于样例ababcc来看,比如子串abab,删除一个的时候不会重复,删除两个的时候删除ab和ba等效。删除三个的时候aab aba…也等效。
发现对于任何一个字符s[i],假设i-1对应的dp已经确定好不重复(这样就不会造成后续影响)
找到最近的前面的s[x]=s[i]。删除【x+1,i】和【x,i-1】等效的。那么只要是[x,i]元素个数减一小于等于j,就需要去重。也就是减掉重复的dp[x-1][j-(i-x)]。i-x选择在可以重复的地方。这时候只是需要关心见加进来的i元素造成的重复,之前的元素在之前的dp中已经解决。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1000100;
long long res, n;
char s[N];
long long f[N][4];//第二维开到4!
int main()
{
cin >> s + 1;
n = strlen(s + 1);
f[0][0] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = 0; j <= 3; j ++ )
{
if(j == 0)
{
f[i][j] = 1;
continue;
}
//不删
f[i][j] += f[i - 1][j];
//删掉
f[i][j] += f[i - 1][j - 1];
//去重,(i-k)为删除的元素的个数,当然要小于j了
for(int k = i - 1; k >= 1 && (i - k) <= j; k -- )
{
if(s[k] == s[i])
{
f[i][j] -= f[k - 1][j - (i - k)];
break;//找到第一个就break掉
}
}
}
}
for(int i = 0; i <= 3; i ++ ) res += f[n][i];
cout << res << endl;
return 0;
}
双指针+简单推式子
思路:
- 由已知条件:任意两边之和大于第三遍,现在我们已知第一条边,枚举第二条点,然后二分查找第三条边
- 假设三条边:a,b,c,已知a,b。那么,有a+b>c,b+c>a,a+c>b,我们可以得出c的范围为{c<a+b, c>a-b, c>b-a},即(abs(a-b}, a+b),现在我们按照c的两个边界分别二分c的最大和最小取值
- 最后c的左右边界有三种情况,我们需要分别讨论:(1)如果左右边界相等,此时可能有解也可能无解(2)如果左边界>有边界,此时一定无解(3)如果左边界小于右边界,有解,解的结束为(右边界-左边界+1)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 100010;
int n, a[N];
long long p = 0;
bool check(int a, int b, int c)
{
if(a + b <= c || a + c <= b || b + c <= a) return false;
return true;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, p;
cin >> n >> p;
for(int i = 0; i < n; i ++ ) cin >> a[i];
sort(a, a + n);
long long res = 0;
for(int i = 0; i < n - 1; i ++ )
{
int maxn = a[i] + p, minx = abs(a[i] - p);
int l = i + 1, r = n - 1;
while(l < r)
{
int mid = (l + r) >> 1;
if(a[mid] > minx) r = mid;
else l = mid + 1;
}
int last = r;
l = i + 1, r = n - 1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[mid] < maxn) l = mid;
else r = mid - 1;
}
// cout << last << ' ' << r << endl;
if(r == last)
{
if(check(a[last], a[i], p)) res ++ ;
}
else if(last > r) continue;
else res += (r - last + 1);
}
cout << res << endl;
return 0;
}
记忆化搜索
本题有一个小坑点,那就是终点不一定必须是叶子节点,例如下图,求1->2的逻辑自洽路径,有一条, 并且是合法的。
1、队列bfs(MLE)
由于这题的边数很大,最后一个测试点数组开小了会段错误,开大了会内存超限
28分
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
const int N = 510, M = 250000;
int n, m, st, en;
int h[N], e[M], ne[M], idx;
int q[M], hh, tt = -1;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m;
for(int i = 0; i < m; i ++ )
{
int a, b;
cin >> a >> b;
add(a, b);
}
cin >> st >> en;
int cnt = 0;
bool flag = true;
q[++ tt] = st;
while(hh <= tt)
{
int t = q[hh ++ ];
if(h[t] == -1 && t != en)
{
flag = false;
continue;
}
if(t == en)
{
cnt ++ ;
continue;
}
for(int i = h[t]; i != -1; i = ne[i])
q[ ++ tt ] = e[i];
}
if(!cnt) flag = false;//特判
cout << cnt << ' ';
if(flag) cout << "Yes";
else cout << "No";
return 0;
}
2、记忆化搜索bfs
bfs由于必须使用队列会导致内存超限,所以我们使用记忆化搜索bfs
深度优先搜索,在搜索的过程中用一个num数组来记录从这个点出发,到终点的路径条数
初始化num=-1,当num!=-1时,说明这个点已经遍历过,不用再遍历了
其中num数组就是记忆化
这样我们减少了搜索次数,也不用额外的队列空间
知识点:链式前向星存图+记忆化搜索(dfs)
每个节点到终点的路径为他的所有子节点到终点路径的数量和,
边界即为该节点就是终点,此时这个节点到终点的路径条数为1;
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N= 550, M = N * N;
int n,m;
int st,en;
int e[M],ne[M],h[N],idx;
int deg[N],num[N];
bool f=false;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//x为起点 y为终点
int dfs(int u)
{
if(u==en) return num[u]=1;//目标节点
if(h[u]==-1)//叶子结点并且非目标节点
{
f=true;
return num[u]=0;
}
if(num[u]!=-1) return num[u];//已经判断过了
//这里的三个判断包括了子节点的三种情况
//1.已经判断过了; 2.这个节点就是目标节点; 3.这个节点是一个死节点,即出度为0;
int sum=0;
for(int i=h[u];i!=-1;i=ne[i])
{
int j = e[i];
sum += dfs(j);
}
return num[u] = sum;//如果这个节点的所有子节点做完判断了,就向上返回
}
int main()
{
memset(num,-1,sizeof num);
memset(h,-1,sizeof h);
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
scanf("%d%d",&st,&en);
int ans=dfs(st);
if(f||!ans) printf("%d No\n",ans);
else printf("%d Yes\n",ans);
return 0;
}
二叉树
1、二叉树搜索树
对于搜索树,需要注意当两个点相等的时候,是在左子树还是右子树
//用map实现
//定义
map<int, vector<int> > m;
//添加
m[depth].push_back(v);
//获得最后两层节点总数
res += m.rbegin()->second.size();
m.erase(m.rbegin()->first);//删除map的第一个关键字,就删除了该信息
res += m.rbegin()->second.size();
#include <iostream>
#include <algorithm>
#include <cstring>
#include <map>
#include <vector>
using namespace std;
const int N = 1010;
int n, w[N], max_depth;
int l[N], r[N], idx;
vector<int> g[N];
void Insert(int &u, int v, int depth)
{
if(!u)
{
u = ++ idx;
w[idx] = v;
g[depth].push_back(v);
max_depth = max(max_depth, depth);
return ;
}
else if(v <= w[u]) Insert(l[u], v, depth + 1);
else Insert(r[u], v, depth + 1);
}
int main()
{
cin >> n;
if(n <= 1)//x小于两层
{
cout << n << endl;
return 0;
}
int root = 0;
for(int i = 0; i < n; i ++ )
{
int x;
cin >> x;
Insert(root, x, 1);
}
// cout << "depth: " << max_depth << endl;
int res = g[max_depth].size() + g[max_depth - 1].size();
cout << res << endl;
return 0;
}
2、完全二叉树
思路:后序遍历二叉树,编号为u的节点的值就是当前后序列遍历到的值
然后按编号顺序(就是层次顺序)输出就行
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n, w[N], a[N], idx;
void dfs(int u)
{
if(u > n) return ;
dfs(u * 2);
dfs(u * 2 + 1);
{
// cout << u << ' ';
a[u] = w[++ idx];
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ ) cin >> w[i];
dfs(1);
// cout << endl;
for(int i = 1; i <= n; i ++ )
{
cout << a[i];
if(i != n) cout << ' ';
}
cout << endl;
return 0;
}
图论
1、最短路
打印路径,维护状态,映射,打印路径
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
using namespace std;
typedef pair<int, int> PII;
const int N = 300, M = 1e5 + 10, INF = 0x3f3f3f3f;
int n, m, hh, tt, id;
int h[N], e[M], ne[M], w[M], idx;
int enemy[N], kill[N], city[N], path[N];
int dist[N], pre[N];
bool st[N];
map<int, string> its; //输出的时候将编号转换为字符串
map<string ,int> sti; //输入的时候将字符串转换为编号
void add(int a, int b, int c)
{
w[idx] = c;
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void djikstra(int u)
{
memset(st, false, sizeof st);
memset(dist, 0x3f, sizeof dist);
priority_queue<PII, vector<PII>, greater<PII> > q;
dist[u] = 0;
city[u] = 1; kill[u] = enemy[u]; path[u] = 1;
q.push({dist[u], u});
while(q.size())
{
auto t = q.top(); q.pop();
int ver = t.second, distance = t.first;
if(st[ver]) continue;
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
pre[j] = ver;
q.push({dist[j], j});
city[j] = city[ver] + 1;
kill[j] = kill[ver] + enemy[j];
path[j] = path[ver];
}
else if(dist[j] == distance + w[i])
{
path[j] += path[ver];
if(city[j] < city[ver] + 1)
{
pre[j] = ver;
city[j] = city[ver] + 1;
kill[j] = kill[ver] + enemy[j];
}
else if(city[j] == city[ver] + 1)
{
if(kill[j] < kill[ver] + enemy[j])
{
pre[j] = ver;
kill[j] = kill[ver] + enemy[j];
}
}
}
}
}
}
void out(int u)
{
if(pre[u] == -1)
{
cout << its[u];
return ;
}
out(pre[u]);
cout << "->" << its[u];
}
int main()
{
string s, ss;
int v;
memset(pre, -1, sizeof pre);
memset(h, -1, sizeof h);
cin >> n >> m >> s >> ss;
sti[s] = id; its[id] = s; hh = id ++ ;
sti[ss] = id; its[id] = ss; tt = id ++ ;
for(int i = 0; i < n - 1; i ++ )
{
cin >> s >> v;
if(!sti.count(s))
{
sti[s] = id;
its[id ++ ] = s;
}
enemy[sti[s]] = v;
}
while(m -- )
{
cin >> s >> ss >> v;
if(!sti.count(s))
{
sti[s] = id;
its[id ++ ] = s;
}
if(!sti.count(ss))
{
sti[ss] = id;
its[id ++ ] = s;
}
add(sti[s], sti[ss], v);
add(sti[ss], sti[s], v);
}
djikstra(hh);
out(tt);
cout << endl << path[tt] << " " << dist[tt] << " " << kill[tt] << endl;
return 0;
}