[CodeForces 915C] dfs+字典序+贪心+离散化 妙用

给定两个正整数 a 和 b。对 a 的数字作出排列 (更改顺序),以构建一个不超过 b 的最大数。输入输出中的任何数,都不含前导数字 0。

允许保持 a 的原样。

第一行包含整数 a (1 ≤ a ≤ 10^18)。第二行包含整数 b (1 ≤ b ≤ 10^18)。两个整数均不含前导 0。保证有解。

打印不超过 b 的最大数,此数是 a 的某个数字排列。答案不含前导 0。保证有解。

 训练的时候没做出来,想到用暴力dfs必然超时,然后想到贪心和剪枝;

a的位数若小于b位数,将a排序后逆序输出即可,所以要考虑的只有a,b位数相同这种情况。

这里一个贪心思路,就是将a以 字典序降序排序,然后从a的最高位开始寻找,每一位严格满足小于b每一位的最大,

然后如果找不到就dfs中回溯,这是典型的贪心and dfs。

这种找法,因为dfs中树的每一层都是优先找大的,树自上而下是对应位数从高到低,高位数影响更大,因此第一个找到的,

符合条件(小于b)排列,必然是最大的,贪心策略是没问题的。

那么,我就每层dfs遍历一遍a的降序排列,用vis数组标记每一位数,然后输出第一个结果。

结果,在test+ tl了....

做了一些优化,还是无果,看了wa的数据,20000....1,和20000.....0;然后发现这个数据从最后一位不合法,一直回溯,tl

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <stack>
#include <queue>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=100000;
bool cmp(const char &a,const char &b)
{
	return a>b;
}
int flag,flagg,k1,k2;
string s1;string s2;
bool vis[100];
vector <int> v;
void dfs(int cnt,int flag)
{
	if(cnt==k1){
		for(int i=0;i<v.size();i++){
			cout<<v[i];
		}
		printf("\n");
		flagg=1;
		return ;
	} 
	for(int i=0;i<k1;i++){
		if(!vis[i]){
			if(s1[i]>s2[cnt]&&!flag) continue;
			if(s1[i]<s2[cnt]) flag=1;
			v.push_back(s1[i]-'0');
			vis[i]=1;
			dfs(cnt+1,flag);
			if(flagg) return ;
			vis[i]=0;
			v.pop_back();
 		}
	}
}
int main()
{
	
	memset(vis,0,sizeof(vis));
	cin>>s1>>s2;
	k1=s1.size(),k2=s2.size();
	sort(s1.begin(),s1.end(),cmp);
	if(k1<k2) cout<<s1<<endl;
	else{
		dfs(0,0);
	}
	return 0;
}

然后想,用a[i]记录每个位数的数的个数会不会不一样?然而没深入想,直接看了别人的题解,发现做法和我差不多,

然后换了dfs的写法,ac了。。。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <stack>
#include <queue>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=100000;
bool cmp(const char &a,const char &b)
{
	return a>b;
}
int flag,flagg,k1,k2,a[11];
string s1;string s2;
bool vis[100];
vector <int> v;
void dfs(int cnt,int flag)
{
	if(cnt==k1){
		for(int i=0;i<v.size();i++){
			cout<<v[i];
		}
		cout<<endl;
		flagg=1;
		return ;
	}
	for(int i=9;i>=0;i--){
		if(a[i]&&(s2[cnt]-'0'>=i||flag)){
			a[i]--;
			v.push_back(i);
			if(s2[cnt]-'0'>i) dfs(cnt+1,1);
			else dfs(cnt+1,flag); 
			if(flagg) return ;
			v.pop_back();
			a[i]++; 
		}
	}
}
int main()
{
	
	memset(vis,0,sizeof(vis));
	cin>>s1>>s2;
	k1=s1.size(),k2=s2.size();
	for(int i=0;i<k1;i++){
		a[s1[i]-'0']++;
	}
	sort(s1.begin(),s1.end(),cmp);
	if(k1<k2) cout<<s1<<endl;
	else{
		dfs(0,0);
	}
	return 0;
}

搜了下,发现这个可以称作离散化。

为什么两者大致一样,但时间复杂度却有着天大的差距?主要原因还是,前者从位数来考虑,而后者是从数来考虑;

而这种排列本身和位数无关;用a数组记录数的个数,,那么,每次判断排列的某个位置放什么数字

如果是前者那么还要用数位来表示,每个数位dfs后后面还有dfs,而后者直接每一步都是从0~9来判断,大大简化了树。

举例,a=2000000000001,b=2000000000000;//前一种写法,生成的子序列可能会重复,像这个就重复了超多次。算剪枝。

对a进行dfs,2000000000001前面一直合法,但到最后一位发现不合法,于是回溯,回溯到某一位,某一位

只需判断0~9是否有其他合法数,显然中间的0都不会dfs,到最前一位2,才会进行dfs,得出结果;

如果是开始那种写法,那么回溯到某一位(树的某一层),后面若干0依旧合法,实际上0在这一位是不合法的,

但是程序没记录下0是不合法的数,它只是记录了之前某一位的某个数不合法,这一点十分关键,于是会进行超多次的dfs搜索

最后就超时乃至爆栈了。。。

其实这对应就是离散化:先排序,再删除重复元素,然后就是索引元素离散化后对应的值 (来自别人博客一句话),

此处是用a数组记录了下来。

其实这点我觉得十分微妙,恰是离散化的精妙;贪心的精妙;

再者,字典序,我弄清了字典序怎么回事,

两个字符串,从最左比较ascii码大小,相同就比下一位,直到一个没有,长的是更大的;

还有dfs的剪枝真的很重要,几乎剪枝和不剪是两个数量级的时间复杂度

还是有收获的一题啊。

ps:还看到一种用排列函数的做法,没太看明白,有点像字典序算法:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int main()
{
   string a;
   LL b;
   cin>>a>>b;
   priority_queue<int,vector<int>,greater<int> >Q;
   for(int i=0;i<a.length();i++)
   {
       sort(a.begin()+i,a.end(),greater<char>());
       sort(a.begin()+i+1,a.end());
       while(stoll(a)>b)
       {
           prev_permutation(a.begin()+i,a.end());
           sort(a.begin()+i+1,a.end());
       }
   }
   cout<<a<<endl;
 
   return 0;
}
--------------------- 
作者:Pei_1997 
来源:CSDN 
原文:https://blog.csdn.net/pei_1997/article/details/79067541 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值