AtCoder - ABC 159 - D(找规律+构造 / dp+构造)

D - Lunlun Number

题意:

求第k小的相邻两位绝对值小于等于1的数。

数据范围:

1 ≤ K ≤ 10^{5}

找规律+构造:

思路:

设x为Lunlun数,x的最后一位为y,那么在x的末尾追加y-1,y,y+1,得到的数也一定是Lunlun数.

再往后写写观察下,一位数满足的是1~9;然后是10 11 12  21 22 23 …… 98 99;三位数是100 101  110 111 112 …… 998 999。
然后就阔以发现这一位数是由上一位的数演变来的,上一位为0/9时特判得出两个一组外,其他是3个一组(x-1,x,x+1)出现的。
用队列queue,由1~9得出之后的数,边弹出边得新数,直到得到第K次弹出的数。

Code:

#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
#include<queue>
using namespace std;

//#define x first
//#define y second

#define IOS ios::sync_with_stdio(false);cin.tie(0);
#define int long long

typedef pair<int, int>PII;

const int N = 3010, mod = 998244353;

void solve()
{
	int k, ans;
	cin >> k;

	queue<int>q;
	for (int i = 1; i < 10; i++)q.push(i);   //将1~9先入队
	while (k--)
	{
		int x = q.front();       //取队头
		q.pop();                 //队头出队

		ans = x;
		
		int r = x % 10;           //判断个位是0/9/其他
		x *= 10;
		x += r;                   //得出x

		if (r > 0)q.push(x - 1);  //如果r=0,没有x-1
		q.push(x);                //添加x
		if (r < 9)q.push(x + 1);  //如果r=9,没有x+1
	}
	cout << ans << endl;
}

signed main()
{
	IOS;

	int t = 1;
	//int t;
	//cin >> t;
	while (t--)
	{
		solve();
	}

	return 0;
}

dp+构造:

思路:

状态表示:f[i,j] 表示以 i 开头位数为 j 的所有数中lunlun数的个数。根据题目样例给出的 k=1e5 时的答案是10位数,说明 0 ≤ j ≤ 10;0 ≤ i ≤ 9。

状态转移:以 i 的下一位的数为划分依据,因为要找位数为 j 的lunlun数,开头数已定是 i,等价于找位数为 j - 1 且开头数与 i 相差不超过 1 的lunlun数的个数。两相邻数最多差1,所以i的下一位数可能为 i - 1 或 i 或 i + 1 ,不过需要对 0,9 特判——
1.i != 0 && i != 9时,无需特判,即f[i,j] = f[i-1,j-1] + f[i,j-1] + f[i+1,j-1]
2.i == 0,去掉开头数为i-1的情况,即f[i,j] = f[i,j-1] + f[i+1,j-1]
3.i == 9,去掉开头数为i+1的情况,即f[i,j] = f[i-1,j-1] + f[i,j-1]
根据dp过程我们得到了开头数为i,位数为 j 的所有lunlun数的个数 f[i,j]。我们用数组 sum[j] 记录位数为j的lunlun数的个数,即 sum[i] = f[i,j] (i从1~9)。

确定第 k 个数的位数:根据 sum[j] 数组,j 从1~10枚举,得到第 k 个数的位数 pos,也就是看sum[j] 的前缀和到哪个 j 时大于 k 。

确定第k个数的每一位:
我们已知位数为 pos,从左往右确定每一位的数。先确定第一位的数 i,枚举 1~9,根据开头数为 i位数为 pos 的lunlun数的个数 f[i,pos] 确定开头数,也就是看 f[i,pos] 的前缀和到哪个 i 时大于 k;
根据确定的开头数 i,确定下一位的数 alter 为 i-1 或 i 或 i + 1 的一种,同理我们这时需要确定的是低 2~pos 位的数,用 f[alter,pos-2+1],进而确定第二位的数。
以此类推,递归从左往右顺次确定pos位每一位的数(因为位数越多lunlun数肯定越多,所以从高位开始确定)

Code:

#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
#include<queue>
using namespace std;

//#define x first
//#define y second

#define IOS ios::sync_with_stdio(false);cin.tie(0);
#define int long long

typedef pair<int, int>PII;

const int N = 3010, mod = 998244353;

int k;
int f[11][11];
int sum[11], pos;

//找出第k个lunlun数
void dfs(int num, int alter)
{
	if (num > pos)return;                    //num表示在dfs中此时已确定数到的位数
	if (num == 1)                            //对于pos位的数,num=1表示先确定第k个数的开头数
	{
		for (int i = 1; i < 10; i++)         //开头数1~9枚举
		{
			if (k > f[i][pos])				 //判断开头为i时的位数为pos的lunlun数的个数f[i][pos]与k的大小
				k -= f[i][pos];
			else                             //当k<=f[i][pos]时确定开头为i时的pos位数中包含第k个数
			{
				cout << i;                   //输出开头数
				dfs(num + 1, i);             //继续确定下一位的数,第一个参数表示将要确定的是第几位;第二个参数表示将要确定的该位的前一位的数i
				break;
			}
		}
	}
	else                                     //开头数确定之后位的确定都是进入else
	{
		for (int i = alter - 1; i <= alter + 1; i++)   //pos位中第num位(从左往右数的,因为num-1位的数已经确定为i,所以像开头数是第1位)最多3中可能:i-1/i/i+1
		{
			if (i < 0 || i > 9)continue;           //特判,如果list=-1或者10时,不符合
			if (k > f[i][pos - num + 1])		   //将第num位看做看透位,则开头为i的位数为pos - num + 1的lunlun数的个数f[i][pos - num + 1](这里的pos - num + 1是因为第k个数的位数是pos, 而我们已经确定了前num - 1位,需要找的是位从num~pos的lunlun数的个数,位数为pos - num + 1)
				k -= f[i][pos - num + 1];
			else
			{
				cout << i;                         //同理依次得到第num位的数
				dfs(num + 1, i);                   //继续推下一位的数
				break;
			}
		}
	}
}

void solve()
{
	cin >> k;
	for (int i = 0; i < 10; i++)
		f[i][1] = 1;						//初始化:开头是i的一位数的lunlun数都是i本身,个数为1

	sum[1] = 9;								//数组sum[i]记录位数为i的lunlun数的个数;一位数中lunlun数的个数为9(1~9)
	for (int j = 2; j <= 10; j++)			//枚举位数2~10
		for (int i = 0; i < 10; i++)		//枚举开头数0~9
		{
			if (i != 0 && i != 9)f[i][j] = f[i - 1][j - 1] + f[i][j - 1] + f[i + 1][j - 1];  //开头数既不是0,也不是9时
			else if (i != 0)f[i][j] = f[i - 1][j - 1] + f[i][j - 1];                         //开头数是9时特判
			else if (i != 9)f[i][j] = f[i][j - 1] + f[i + 1][j - 1];                         //开头数是0时特判
			if (i != 0)sum[j] += f[i][j];	 //sum[j]记录位数为j开头数为1~10的所有lunlun数的个数
		}

	//枚举不同位数拥有的lunlun数的个数,判断第k个lunlun数的位数
	for (int i = 1; i <= 10; i++)
	{
		if (k > sum[i])
			k -= sum[i];
		else
		{
			pos = i;      //pos记录第k个lunlun数的位数
			break;
		}
	}
	//dfs找到第k个lunlun数
	dfs(1, 0);            //1表示从pos位数的开头数从开始枚举;第二个参数用于枚举后面位的值
}

signed main()
{
	IOS;

	int t = 1;
	//int t;
	//cin >> t;
	while (t--)
	{
		solve();
	}

	return 0;
}

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:呼,总算把两种方法都看懂啦。本题注意点:

1.方法一找规律如何构造,开始写的时候能看出来有规律,但是吧,前后没联系到一起,无了。

2.方法二数位dp,dp真是变化万千啊,不过这题的dp部分还是很好理解的,不过dfs部分又让我有种写不出来的赶脚。整体思路是先确定位数,对这么些位头到尾顺次确定,除开头数外,后面位的确定都是与前一位有联系的。dfs中用到dp得到的状态转移方程,还有一些特判要注意。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值