刷题笔记(总)

目录

典例类

洛谷P1273 有线电视网(树形dp——分组背包)

题目描述

输入

思路

代码

平均数对(dp的状态优化)

题意

思路1

代码

思路2

其他

二叉树的字符匹配(KMP)

题目描述:

思路:

具体过程:

P1040 [NOIP2003 提高组] 加分二叉树

 题意:

输入格式

输出格式

数据规模与约定

思路:

代码:


典例类

洛谷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。要求输出

  1. tree 的最高加分。

  2. 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;
} 

"Labuladong"是一个著名的算法题解博主,他的刷题笔记非常受欢迎。他的笔记具有以下几个特点: 1. 思路清晰:Labuladong的刷题笔记是能够很清晰地阐述解题思路。他善于将复杂的问题简化为易于理解的小问题,并通过逐步引入关键概念和方法,帮助读者理解并掌握解题思路。 2. 逻辑严谨:Labuladong的刷题笔记经过深思熟虑,逻辑严谨。他会从问题的定义开始,逐步引入相关的概念和解题思路,循序渐进地解决问题。这种严谨的逻辑结构有助于读者理解和消化算法的核心思想。 3. 举例详细:Labuladong的刷题笔记通常会通过具体的例子来说明解题思路。这种举例的方式不仅能够帮助读者更好地理解解题方法,还可以帮助读者更好地应用这些方法解决其他类似的问题。 4. 知识点整合:Labuladong的刷题笔记不仅仅是一个题解,而是将相关的算法知识点整合起来,构建出一个完整的学习体系。他会引入一些底层的算法原理,将不同的解题方法进行比较和结。这种整合的方式能够帮助读者更好地理解和掌握算法的本质。 之,Labuladong的刷题笔记以其思路清晰、逻辑严谨、举例详细和知识点整合等特点,为广大读者提供了一种深入学习和理解算法的有效途径。通过阅读他的刷题笔记并进行实践,读者能够提高解题能力,并在面对各种算法问题时能够找到正确、高效的解决方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值