动态规划专题

Codeforces Round 890 (Div. 2) supported by Constructor Institute

E1

题目链接:

Problem - E1 - Codeforces

题目大意:

给的一棵根为1的树,节点数为n,边数为n-1, 给定一个大小为n的排列,重新对n个节点赋值a1,a2....an,其中a1,a2……an是大小为n的排列。对于每一个节点,设节点被赋赋值为x,求节的两个子树中节点分别为yi和zi,其中(yi>x&&zi<x)或者(yi<x&&zi>x),也就是子树中存在y,z,使得lca(y,z)=x。对树重新赋值,出现最多的这样的序列对的最大个数。

数据范围:

n<=5e3

思路:

看到数据范围,发现O(n²)能过,会往dp方向思考。而对于一个节点,它的子树重新排列后的结果,对答案的贡献为:x*(siz-x-1),这里siz表示节点的树的大小,x表示比根节点小的数,那么贡献为比根节点小的数的个数*比根节点大的数的个数。也就是这个是一个二元一函数,发现其对称轴x=-b/2a为x=(siz-1)/2,在这个点上可以获得贡献的最大值。所以问题就转换:所有子树的大小来凑一个(siz-1)/2,很明显这可以用一个背包来实现,将大小看做体积,同时价值也是大小,使用滚动优化节省一维空间即可。而其它节点也是可以递归的实现。

做法:

用一个dfs预处理出所有的子树大小,然后再用一个dfs递归地跑背包,累加上对答案的最大贡献即可。

标签:

背包、动态规划,dfs、数学

Rating

1800

代码实现:

void solve() {
	int n;
	cin >> n;
	vector<vector<int>>e(n + 1);
	for (int i = 2; i <= n; ++i) { //建图
		int x;
		cin >> x;
		e[x].push_back(i);
	}
	vector<int>siz(n + 1);
	int res = 0;
	function<void(int)>dfs1 = [&](int u)->void { //预处理出所有的子树大小
		siz[u]++;
		for (auto& v : e[u]) {
			dfs1(v);
			siz[u] += siz[v];
		}
		};
	function<void(int)>dfs2 = [&](int u)->void {
		vector<int>dp(n + 1);
		int m = siz[u] - 1 >> 1;
		for (auto& v : e[u]) { //跑背包
			for (int i = m; i >= siz[v]; --i) {
				dp[i] = MAX(dp[i], dp[i - siz[v]] + siz[v]);
			}
		}
		int s = dp[m] * (siz[u] - 1 - dp[m]);
		res += s;
		for (auto& v : e[u]) { //递归下一个点
			dfs2(v);
		}
		};
	dfs1(1);
	dfs2(1);
	cout << res << endl;
}

时间复杂度:

O(n²)

Codeforces Round 881 (Div. 3)

D

题目链接:

Problem - D - Codeforces

题目大意:

给定一棵大小为n的树,边为n-1,给定q个询问,每次询问给出一个x,y,表示树的两个节点。两个节点可以按任意顺序往他们的叶子方向移动,当x,y同时移动到叶子节点使,得到一个二元序列(x',y'),求可以获得的二元序列最大值。

数据范围 :

2≤n≤2e5,1≤q≤2e5

思路:

容易发现,这题是一个离线查询问题,对于2e5的数据,每次查询的时间复杂度最差不能超过O(sqrt(n)),于是会想到预处理所有每个子树包含的叶子节点个数。设x子树的叶子节点个数为a,y子树的叶子结点的个数为b,那么易得所有二元组的个数为a*b。 所以这道题最重要的就是预处理出所有子树的的叶子结点个数,而这个预处理可以通过一个树形dp(递归)来实现。

做法:

先建图,写一个dfs来预处理出所有子树的叶子结点个数,然后就可以进行q次O(1)查询。

标签:

树形dp、dfs、回溯

Rating

1200

代码实现:

void solve() {
	int n;
	cin >> n;
	vector<vector<int>>e(n + 1);
	for (int i = 1; i <= n - 1; ++i) { //建图
		int x, y;
		cin >> x >> y;
		e[x].push_back(y);
		e[y].push_back(x);
	}
	vector<int>lef(n + 1);
	function<void(int, int)>dfs = [&](int u, int fa)->void { //树形dp
		int tt = 1;
		for (auto& v : e[u]) {
			if (fa == v) continue;
			tt = 0;
			dfs(v, u);
			lef[u] += lef[v]; //回溯递推出所有的子树的叶子结点个数
		}
		if (tt == 1) lef[u] = 1; //tt=1表示当前节点为叶子节点
		};
	dfs(1, -1);
	int q;
	cin >> q;
	while (q--) { //O(1)查询
		int x, y;
		cin >> x >> y;
		cout << lef[x] * lef[y] << endl;
	}
}

时间复杂度:

O(n+q)

洛谷P2602

数字计数

题目链接:

Problem - A - Codeforces[ZJOI2010] 数字计数 - 洛谷Problem - A - Codeforces

题目大意:

给定一个区间[a , b] 求区间内每个数位出现的总个数。

数据范围 :

1≤a≤b≤1e12

思路:

暴力O(n)是不可能的了。所以这题要么用dp,要么用记忆化搜索(本质上记忆化搜索也算一种dp吧)

做法:

首先对于一个数,我们需要枚举它的每一个位,这里选择从高位向低位枚举,比较方便。具体来说,对于每个位,我们需要预处理一个 dp[i]=i*pow(10,i-1),这里的i表示第i位。对于每一个数字,都有贡献i*pow(10,i-1)。然后我们需要吧数位分三类讨论,数位大于当前位,数位等于当前位,数位小于当前位。

① 数位大于当前位:贡献增加pow(10,i-1)

②数位等于当前位:贡献增加 sum(a[i]*pow(10,i-1)+a[i-1]*pow(10,i-2)....)

③数位小于当前位 没有额外贡献

注意,枚举数位小于当前位的时候不要枚举0,因为会产生额外贡献,即会产生前导0

最后,用前缀和的思想来求一下区间内的个数即可

标签:

数位dp,前缀和

Rating

普及+

代码实现:

int dp[20],num[20]; //dp[i]表示第i位的预处理结果
void init(){
	//预处理dp函数,如0-9每个数位都有1个,0-99每个数位有20个,0-999每个数位有300个 
	for(int i=1;i<=15;++i){
		dp[i]=i*pow(10LL,(int)(i-1));
	}
}
void query(int x,vector<int>&cnt){
	int len=0,res=0;
	while(x){
		num[++len]=x%10;
		x/=10;
	}
	for(int i=len;i>=1;--i){ //从高到低枚举x的数位 
		for(int j=0;j<=9;++j) cnt[j]+=dp[i-1]*num[i]; //放在当前位后面,全部数都雨露均沾 
		for(int j=1;j<num[i];++j) cnt[j]+=pow(10LL,(int)(i-1)); //判断小于当前位置的数位
		int num2=0;
		for(int j=i-1;j>=1;--j) num2=num2*10+num[j]; //特判当前位置等于的数位 
		cnt[num[i]]+=num2+1;  
	}
}
void solve(){
	init();
	int x,y;
	cin>>x>>y;
	vector<int>cnt1(10),cnt2(10);
	query(x-1,cnt1);
	query(y,cnt2);
	for(int i=0;i<=9;++i) cout<<cnt2[i]-cnt1[i]<<" ";
	cout<<endl;
}

时间复杂度:

O(1) 只与数位长度有关

Codeforces Round 875 (Div. 1)

A

题目链接:

Problem - A - Codeforces

题目大意:

一颗大小为n的树,有n-1条边,按照给出的边的顺序建树,每次建边按顺序从上到下,只能找已有的点建边。问最少需要从上到下建边几次。

数据范围 :

2≤n≤2⋅1e5

思路:

这题模拟去做肯定会TLE,因为当树退化成一条链时,最差结果是O(n^2)。所以这题需要使用dp来做。由于这是一棵树,所以我们可以使用bfs或者dfs来遍历树,同时使用dp记录每个节点的最少操作。dfs的代码较少,而且思路很简单,这里故使用dfs遍历树。

做法:

dp[i]表示第i个节点所需最少操作数。考虑转移,从根节点开始,每次转移都是从根节点的子树中来转移,我们选择一个最大的来转移,因为每一个节点的操作次数取决于子节点中最大的那一个。我们需要记录一开始给出边的顺序,这里用一个map来记录,然后去跑dfs,从叶子结点开始转移上来,如果发现子节点的顺序在当前节点之前,那么就需要从头开始操作一次,此时dp[u]=dp[v]+1,否则dp[u]=dp[v]

标签:

树形dp、dfs、回溯

Rating

1400

代码实现:

void solve() {
	int n;
	cin>>n;
	map<pii,int>mp;
	vector<vector<int>>e(n+1);
	for(int i=1;i<=n-1;++i){
		int x,y;
		cin>>x>>y;
		mp[{x,y}]=i;
		mp[{y,x}]=i;
		e[x].push_back(y);//存建边顺序
		e[y].push_back(x);	
	}
	vector<int>dp(n+1); 
	function<void(int,int)>dfs=[&](int u,int fa)->void{ 
		for(auto &v:e[u]){
			if(v==fa) continue;
			dfs(v,u);//一直递归到叶子结点
			dp[u]=max(dp[u],dp[v]+(mp[{u,v}]<mp[{fa,u}])); //dp转移
		}
	};
	dfs(1,-1);
	cout<<dp[1]+1<<endl;//次数至少为1
}

时间复杂度:

O(nlog2n)

Codeforces Round 863 (Div. 3)

E

题目链接:

Problem - A - CodeforcesProblem - E - CodeforcesProblem - A - Codeforces

题目大意:

给定一个无限长的序列,该序列是1~inf去掉含有4的序列。给出一个k,求该序列第k个数是什么。

数据范围 :

1≤k≤1e12,1≤t≤1e4

思路:

看到这个数据范围,大概可以知道这题不是数位dp就是二分了,事实上这题是二分+数位dp,即二分答案,数位dp来check。虽然有更好的方法,但是这题的思路这样很容易就能想到。

做法:

首先我们需要预处理一下dp,计算一下 i 在 1- i 中排在第几位,这里dp[i][j]表示长度为i,最后一位是j的数字排在第几位,换一句话说,也就是1-i*10+j中去除掉4,还有多少个数。接下来,我们在二分答案的时候,使二分的答案尽量靠左,因为存在例如13,14,它们在这个序列中都排在一个位置,然而14不存在于序列中,故取最小的。

标签:

数位dp、二分。

Rating

1500

代码实现:

int dp[20][10]; //dp[i][j]表示长度为i的最后一位是j的数在序列中的位置
void init(){
	for(int i=0;i<=9;i++) if(i!=4) dp[1][i]=1;
	for(int i=2;i<20;i++){
		for(int j=0;j<=9;j++){
			if(j==4) continue; //当前位是4,跳过
			for(int k=0;k<=9;k++){ 
				if(k==4) continue; //不转移有4的方案数
				dp[i][j]+=dp[i-1][k];
			} 
		} 
	}
} 
void solve(){
	int n;
	cin>>n;
	auto check=[&](int x){
		int num[20];
		int len=0,sum=-1,lst=0;
		while(x){
			num[++len]=x%10;
			x/=10;
		}
		for(int i=len;i>=1;i--){ //长度为i
			for(int j=0;j<num[i];j++){  //最后一位为num[i],要加上长度为i,最后一位小于num[i]的方案数
				if(j==4) continue; //不加上4
				sum+=dp[i][j]; 
			}
			if(num[i]==4) break; //当前位是4,直接跳过这一位
			if(i==1) sum++; //长度为1,没得转移,只有它本身
		}
		return sum;
	};
	int l=1,r=1e18,res=0;
	while(l<=r){ //二分答案
		int mid=l+r>>1;
		int x=check(mid);
		if(x>=n){ //答案靠左
			res=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	cout<<res<<endl;
}
signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	init(); //记得初始化
	int t = 1;
	cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

时间复杂度:

接近(O(t*log2(1e18)*100)),t组,100是数位dp的复杂度,log2(1e18)是二分的复杂度,总复杂度接近1e7,能过

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值