D - Lunlun Number
题意:
求第k小的相邻两位绝对值小于等于1的数。
数据范围:
1 ≤ K ≤
找规律+构造:
思路:
设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得到的状态转移方程,还有一些特判要注意。