目录
典例类
洛谷P1273 有线电视网(树形dp——分组背包)
题目描述
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入
输入文件的第一行包含两个用空格隔开的整数 N 和 M,其中2≤N≤3000,1≤M≤N−1,N 为整个有线电视网的结点总数,M 为用户终端的数量。
第一个转播站即树的根结点编号为 1,其他的转播站编号为 2 到 N−M,用户终端编号为N−M+1 到 N。
接下来的 N−M 行每行表示—个转播站的数据,第 i+1 行表示第 i 个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K 表示该转播站下接 K 个结点(转播站或用户),每个结点对应一对整数 A 与 C ,A 表示结点编号,C 表示从当前转播站传输信号到结点 A 的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。单次传输成本和用户愿意交的费用均不超过 10。
思路
是树形dp中的树形分组背包dp。
节点的每个子树都可以选择不同用户数,和分组背包一样(分组背包:每组只能选择一种重量)
dp[i][j][k]表示第i个节点,选择前j个子树,获得k个用户的最大盈利(最小亏损)。
本题选择三维dp会MLE,因此使用二维滚动数组。
dp[i][j]表示第i个节点,选择j个用户的最大盈利。
dp[i][j] = dp[i][j-k]+dp[child][k]
因为是滚动数组,因此要逆序遍历。
代码
#include<bits/stdc++.h>
using namespace std;
vector<pair<int,int>>tree[3001];
int weight[3001];
int siz[3001];
int dp[3010][3010];
int n,m;
void dfs(int cur){
if(tree[cur].size()==0){
siz[cur] = 1;
return ;
}
for(int i = 0;i<tree[cur].size();i++){
dfs(tree[cur][i].first);
siz[cur]+=siz[tree[cur][i].first];
}
}
void f(int cur){
dp[cur][0] = 0;
if(tree[cur].size()==0){
dp[cur][1] = weight[cur];
return ;
}
int sum = 0;
for(int i = 0;i<tree[cur].size();i++){
int ch = tree[cur][i].first,cost = tree[cur][i].second;
f(ch);
sum+=siz[ch];
for(int j = sum;j>0;j--){
for(int k = 0;j-k>=0&&k<=siz[ch];k++){
dp[cur][j] = max(dp[cur][j],dp[cur][j-k]+dp[ch][k]-cost);
}
}
}
}
int main() {
memset(dp,0x8f,sizeof(dp));
cin>>n>>m;
for(int i = 1;i<=n-m;i++){
int k;cin>>k;
for(int j = 0;j<k;j++){
int a,w;cin>>a>>w;
tree[i].push_back({a,w});
}
}
for(int i = m-1;i>=0;i--){
cin>>weight[n-i];
}
dfs(1);
f(1);
//cout<<dp[1][3];
for(int i = m;i>=0;i--){
if(dp[1][i]>=0){
cout<<i;
return 0;
}
}
}
平均数对(dp的状态优化)
题意
给定 n 对数 (ai,bi) 和参数k,你需要选出一些对使得在满足 bi 的平均值不超过 k 的同时,ai 的和最大,求出这个最大值。
思路1
首先排除贪心,本题基本能确定是背包问题。
直接按题意来设计出的dp应该是三维的(只记录b的平均值不行)
dp[i][j][k]//i为选前i个数,j为b的总和,k为选择数对的数量,保存a的最大和
可以优化成二维。
直接枚举会达到n*sum(b)*n,即O(n^4),超时。
考虑map优化背包,可以把一对j和k映射成一个数,作为map的key。
这种思路在时间和空间要求不严的情况下可以使用。
代码
#include <bits/stdc++.h>
using namespace std;
struct s{
int a, b;
bool operator <(const s &x)const{
return b < x.b;
}
} ab[510];
unordered_map<int, int> um; // b的和,数量为key,value为a的sum
int decode(int sum, int num)
{
return sum * 1000 + num;
}
pair<int, int> huifu(int x) // b的和,数量
{
return {(x - x % 1000) / 1000, x % 1000};
}
int main()
{
int ans = 0;
int n, k;
scanf("%d %d", &n, &k);
um[0] = 0;
for (int i = 0; i < n; i++)
{
scanf("%d %d", &ab[i].a, &ab[i].b);
}
sort(ab, ab + n);
for (int i = 0; i < n; i++)
{
int a=ab[i].a ,b=ab[i].b;
vector<pair<int, int>> tmp;
for (auto j : um)
{
pair<int, int> t = huifu(j.first);
double res = (double)(t.first + b) / (double)(t.second + 1);
if (res <= k)
{
ans = max(ans, j.second + a);
tmp.push_back({decode(t.first + b, t.second + 1), j.second + a});
}
}
for (int j = 0; j < tmp.size(); j++)
{
um[tmp[j].first] = max(um[tmp[j].first], tmp[j].second);
}
}
cout << ans;
}
思路2
思路2是本题正解,如果直接按题意来,空间上必须要三维,时间上则是四维。
但我们可以转化一下,与其通过维护两个维度(b的和、(a,b)对的数量)来确定k,直接用一个维度也可以达到相同的效果,如下:
dp[i][j]//i代表选前i个数,j代表sum(b-k)
凡是j>=0的情况都是可行的
这样写时间上和空间上都会减少,同时代码也更简洁。
(由于比较简单就不写了)
其他
二叉树的字符匹配(KMP)
题目描述:
给你一颗二叉树和一个字符串,树上有很多一路向下的路径,如果某一个路径经过的节点形成的字符串等与给出的字符串,打印“Yes”,否则打印“No”(暴力方法TLE)。
思路:
一路向下的路径,自然是dfs;同时用KMP匹配字符串(字符匹配问题套路相当固定,就是原封不动的KMP+其他算法)。
具体过程:
DFS深搜每一个路径,同时在形成每个路径时,以该路径为大字符串,匹配题目给出的字符串。
一路向下,不需要回溯(可以把树想象成一个总路径分成许多分路径,总路径已经匹配好了,因此只需要依次查看每个分路径)。
#include<bits/stdc++.h>
using namespace std;
#define N 1000000
struct s{
int lc = -1,rc = -1;
char c;
};
s tree[N];
string str;
int nexts[N];
void fnext(){
nexts[0] = -1;
nexts[1] = 1;
int i = 2,cnt = 0;
while(i<str.size()){
if(str[i-1]==str[cnt]){
nexts[i++] = ++cnt;
}else{
if(cnt==0){
nexts[i++] = 0;
}else{
cnt = nexts[cnt];
}
}
}
}
bool f(int i,int j){
//i代表现在是数上哪一个节点,j代表对比到哪一个字符
if(j == str.size()){
return true;
}
if(i==-1){
return false;
}
while(j>=0&&str[j]!=tree[i].c){
j = nexts[j];
}
return f(tree[i].lc,j+1)||f(tree[i].rc,j+1);
}
int main(){
//忽略建树过程
fnext();
bool ans = f(0,0);
if(ans){
cout<<"Yes";
}else{
cout<<"No";
}
}
P1040 [NOIP2003 提高组] 加分二叉树
题意:
设一个 n 个节点的二叉树 treetree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 treetree 本身)的加分计算方法如下:
subtree 的左子树的加分 × subtree 的右子树的加分 + subtree 的根的分数。
若某个子树为空,规定其加分为 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n) 且加分最高的二叉树 tree。要求输出
-
tree 的最高加分。
-
tree的前序遍历。
输入格式
第 1 行 1 个整数 n,为节点个数。
第 2 行 n 个用空格隔开的整数,为每个节点的分数
输出格式
第 1 行 1 个整数,为最高加分(Ans≤4,000,000,000)。
第 2 行 n 个用空格隔开的整数,为该树的前序遍历。
数据规模与约定
对于全部的测试点,保证 1≤n<30,节点的分数是小于 100 的正整数,答案不超过 4×10^9。
思路:
树本质是图,是一个连通、无环、顶点数为边数+1的图。
一种中序遍历序列可以还原成许多不同结构的二叉树,但是只对应一种结构的图。
中序遍历序列相同,而结构不同的二叉树区别在于“根”的选择,是一个递归的结构,不仅是整棵树的根的选择,还包括子树的根,子树的子树的根……
代码:
1.严格位置依赖的dp
#include<bits/stdc++.h>
using namespace std;
const int MAX = 50;
typedef long long ll;
ll n;
ll f[MAX][MAX], root[MAX][MAX];
void print(ll l, ll r) {
if (l > r)return;
printf("%lld ", root[l][r]);
if (l == r)return;
print(l, root[l][r] - 1);
print(root[l][r]+1,r);
}
int main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++)scanf("%lld", &f[i][i]),f[i][i-1]=1, root[i][i] = i;
for (int len = 1; len < n; ++len) {
for (int i = 1; i + len <= n; ++i) {
int j = i + len;
f[i][j] = f[i + 1][j] + f[i][i];
root[i][j] = i;
for (int k = i + 1; k < j; ++k) {
if (f[i][j] < f[i][k - 1] * f[k + 1][j] + f[k][k]) {
f[i][j] = f[i][k - 1] * f[k + 1][j] + f[k][k];
root[i][j] = k;
}
}
}
}
cout << f[1][n] << endl;
print(1, n);
return 0;
}
2.记忆化搜索
#include<bits/stdc++.h>
using namespace std;
const int MAX = 50;
int n,v[MAX],dp[MAX][MAX],root[MAX][MAX];
int f(int l,int r){
if(dp[l][r]>0)return dp[l][r];
if(l==r)return v[l];
if(r<l)return 1;
for(int i=l;i<=r;i++){
int p=f(l,i-1)*f(i+1,r)+dp[i][i];
if(p>dp[l][r]){
dp[l][r]=p;root[l][r]=i;
}
}
return dp[l][r];
}
void print(int l,int r){
if(r<l)return;
if(l==r){printf("%d ",l);return;}
printf("%d ",root[l][r]);
print(l,root[l][r]-1);
print(root[l][r]+1,r);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]),dp[i][i]=v[i];
printf("%d\n",f(1,n));
print(1,n);
return 0;
}