NOIP2024集训 Day32 总结
前言
当坚冰还盖着北海的时候,我看到了怒放的梅花。
停课了,对于每天的题也是终于有时间写总结了。
我不会告诉你,以前没空写是因为颓废。
今天是,愉快的,数位DP专题~
淘金
乍一看,这个 \(n^2\) 怎么感觉跟数位 \(\texttt{DP}\) 没啥关联呢。
手玩一下小数据,我们可以发现,对于每个变动到的坐标 \((x,y)\),要使其合法,即 \(x,y\in [1,n]\),\(x,y\) 质因数分解之后必然为:\(x,y=2^{p_1}\times 3^{p_2} \times 5^{p_3} \times 7^{p_4}\)。观察到合法的 \(x,y\) 均 \(\in [1,10^{12}]\),故 \(p_{1,2,3,4}\) 都不是很大,经过计算,\(p_1 \times p_2 \times p_3 \times p_4\) 大概在 \(2\times 10^5\) 级别。
由于 \(x,y\) 本质上的变动情况是相同的,故我们可以先只考虑一维然后在进行两维的合并。
考虑对于一维,有我们刚刚的推导可以得到,每一维变动之后最终的合法状态在 \(2\times 10^5\) 级别。于是我们可以直接枚举最终状态的 \(p_{1,2,3,4}\),通过数位 \(\texttt{DP}\) 计算每种状态的点上有多少个点。
这个应该是不难的,所以先直接放代码。
//num1,2,3,4是分别对应的2,3,5,7的次数,如6,num1=1,num2=1
long long dfs(int x, bool limit, int num1, int num2, int num3, int num4, bool flg)
{
if(num1 < 0 || num2 < 0 || num3 < 0 || num4 < 0) return 0;
if(x == 0) return (num1 + num2 + num3 + num4) == 0 && flg;
if(~dp[x][num1][num2][num3][num4][limit][flg]) return dp[x][num1][num2][num3][num4][limit][flg];
int up = limit ? a[x] : 9;
long long now = 0;
for (int i = ((!flg) ? 0 : 1); i <= up; ++i)
now += dfs(x - 1, limit & (i == a[x]), num1 - ::num1[i], num2 - ::num2[i], num3 - ::num3[i], num4 - ::num4[i], (flg | bool(i)));
return dp[x][num1][num2][num3][num4][limit][flg] = now;
}
然后样我们就可以得到每个最终状态上有多少个点。
问题就转化成了:有 \(n\) 个数 \(a_i\),每次可以选一个数对 \((i,j)\),每次选的不能相同,选择一个数对的代价是 \(a_i\times a_j\),要求选择 \(k\) 个数对的最大代价之和。
一个简单的堆就解决了,先对于每一个 \(i\),将 \(a_i \times \max_{j=1}^n a_j\) 放入堆中,每次取出一个,就将 \(a_j\) 变为次大再放进去,一直取直到限制即可。
代码写的非常难评,是时间正常人的 \(10\) 倍,还是别看了(((
#include <bits/stdc++.h>
using namespace std;
#define maxn 1000005
const int mod = 1e9 + 7;
long long n;
int k;
int a[20], cnt;
long long dp[15][42][30][20][15][2][2];
map<long long, int> ans;
int num1[10], num2[10], num3[10], num4[10];
long long dfs(int x, bool limit, int num1, int num2, int num3, int num4, bool flg)
{
if(num1 < 0 || num2 < 0 || num3 < 0 || num4 < 0) return 0;
if(x == 0) return (num1 + num2 + num3 + num4) == 0 && flg;
if(~dp[x][num1][num2][num3][num4][limit][flg]) return dp[x][num1][num2][num3][num4][limit][flg];
int up = limit ? a[x] : 9;
long long now = 0;
for (int i = ((!flg) ? 0 : 1); i <= up; ++i)
now += dfs(x - 1, limit & (i == a[x]), num1 - ::num1[i], num2 - ::num2[i], num3 - ::num3[i], num4 - ::num4[i], (flg | bool(i)));
return dp[x][num1][num2][num3][num4][limit][flg] = now;
}
void solve(long long x)
{
while(x) a[++cnt] = x % 10, x /= 10;
memset(dp, -1, sizeof(dp));
for (int i = 0; i <= 40; ++i)
{
for (int j = 0; j < 30; ++j)
{
for (int k = 0; k < 20; ++k)
{
for (int l = 0; l < 15; ++l)
ans[dfs(cnt, true, i, j, k, l, 0)]++;
}
}
}
}
unordered_map<long long, int> now, nxt;
int main()
{
num1[2] = 1, num2[3] = 1, num3[5] = 1, num4[7] = 1;
num1[4] = 2;
num1[6] = 1, num2[6] = 1;
num1[8] = 3;
num2[9] = 2;
cin >> n >> k;
solve(n);
long long maxx = (*(--ans.end())).first;
priority_queue<pair<long long, int> > p;
for (auto i = ans.begin(); i != ans.end(); ++i) now[(*i).first] = maxx;
for (auto i = ans.begin(); i != ans.end(); ++i) if(i != ans.begin())
{
long long now = (*i).first;
--i;
nxt[now] = (*i).first;
++i;
}
for (auto i = ans.begin(); i != ans.end(); ++i) p.push(make_pair((*i).first * maxx, (*i).first));
int sum = 0;
while(!p.empty())
{
int x = p.top().second;
long long y = p.top().first;
p.pop();
if(ans[x] * ans[now[x]] >= k)
{
sum += 1ll * y % mod * k % mod, sum %= mod;
break;
}
sum += 1ll * y % mod * ans[x] % mod * ans[now[x]] % mod, sum %= mod;
k -= ans[x] * ans[now[x]];
if(nxt[now[x]])
{
now[x] = nxt[now[x]];
p.push(make_pair(x * 1ll * now[x], x));
}
}
cout << sum << endl;
return 0;
}
数数
有一说一,这个题还是很难的,至少我一开始就走上了错误的思路。
首先把这个题转化为 \([0,r]-[0,l-1]\) 是基本思路。
看到这种子串的问题,我们先明确一个中心思路。就是说,我们可以考虑去枚举处于 \([l,r]\) 之间每个字符串的前缀的长度,然后通过对于每个字符串的前缀的后缀的答案计算来得到最终答案。注意,我们的长度对应的贡献计算 都默认表示的是在含有数位 DP 的字典序即首位限制的情况下所对应的贡献,当然这一位也就对应的是长度,不是指的字串长度!!!
其实也就是变相的对字串转化了一下,使其更贴近于数位 \(\texttt{DP}\) 字典序,方便我们统计答案。这两句话还是有点抽象的,只是我自己总结出来的,可以考虑先看后面的比较直白的解法。
我们定义 \(\text{num}_i\) 表示的是从首位开始进行到第 \(i\) 位时,有多少种不会被 \(\text{lim}\) 所影响的字符串。
显然有 \(\text{num}_i=\text{num}_{i-1}\times b+a_i+(b-1)\)
首先可以直接在上一位之后乱填,也可以在前面全部首位都被顶到的情况下填写 \([0,a_i-1]\),也可以前面全部是前导 \(0\),这一位填 \([1,b-1]\)。
接下来我们定义 \(\text{len}_{i,0/1}\) 表示前 \(i\) 位,\(0\) 表示被首位所限制,\(1\) 表示没有限制的满足条件的所有字符串的长度之和。
显然有 \(\text{len}_{i,0}=len_{i-1,0} + 1\),毕竟你都被限制了显然只有一种。
\(\text{len}_{i,1}=\text{len}_{i-1,1}\times b+\text{len}_{i-1,0}\times a_i+\text{num}_{i-1}\times b\)
其实挺显然的,没限制就是 \(b\) 种情况,有限制就只能填 \([0,a_i-1]\),还是好理解的。
然后我们定义 \(\text{now}_{i,0/1}\) 表示前 \(i\) 位,\(0/1\) 含义相同,其对应合法的所有字符串的后缀的拼起来的数值之和。
简单的 \(\text{now}_{i,0}=\text{now}_{i-1,0}\times b+\text{len}_{i,0}\times a_i\)
显然这一位只能填 \(a_i\),而之前的答案总和要进位,故乘 \(b\),\(len\) 本质上就是所有字符串的后缀个数之和,和所有字符串的长度相同,很合理。
那么有了 \(0\) 的铺垫,\(1\) 应该也是相对好理解的,这里就不写了,可以看后面代码。
然后我们考虑定义一个 \(\text{dp}\) 来统计答案,由于我们只关心了前面 \(i\) 位的贡献,而后面的位仍存在乱填导致的方案数对答案的贡献,也需要通过这个 \(\text{dp}\) 加上,这里也不写了。
更多细节参见代码:
//牛的,看题解硬控我2h
#include <bits/stdc++.h>
using namespace std;
#define maxn 100005
const int mod = 20130427;
int n, b;
int dp[maxn][2], num[maxn], now[maxn][2], len[maxn][2];
int get(int x) {return 1ll * x * (x + 1) / 2 % mod;}
int solve(int n, int a[])
{
for (int i = 1; i <= n; ++i)
{
int j = (i > 1) * b - 1;
num[i] = (1ll * num[i - 1] * b % mod + a[i] + j) % mod;
len[i][0] = len[i - 1][0] + 1;
len[i][1] = (len[i][0] * 1ll * a[i] % mod + (num[i - 1] + len[i - 1][1]) * 1ll * b % mod + j) % mod;
now[i][0] = (now[i - 1][0] * 1ll * b % mod + len[i][0] * 1ll * a[i] % mod) % mod;
now[i][1] = (get(j) + now[i - 1][0] * 1ll * b % mod * a[i] % mod + len[i][0] * 1ll * get(a[i] - 1) % mod) % mod;
now[i][1] += (now[i - 1][1] * 1ll * b % mod * b % mod + (len[i - 1][1] + num[i - 1]) * 1ll * get(b - 1) % mod) % mod;
now[i][1] %= mod;
dp[i][0] = (dp[i - 1][0] + now[i][0]) % mod;
dp[i][1] = (dp[i - 1][0] * 1ll * a[i] % mod + dp[i - 1][1] * 1ll * b % mod + now[i][1]) % mod;
}
return (dp[n][0] + dp[n][1]) % mod;
}
int l, r;
int a[maxn], c[maxn];
int main()
{
scanf("%d", &b);
scanf("%d", &l);
for (int i = 1; i <= l; ++i) scanf("%d", &a[i]);
scanf("%d", &r);
for (int i = 1; i <= r; ++i) scanf("%d", &c[i]);
a[l]--;
for (int i = l; a[i] < 0; --i) a[i] += b, a[i - 1]--;
if(!a[1])
{
for (int i = 2; i <= l; ++i) a[i - 1] = a[i];
--l;
}
cout << (solve(r, c) - solve(l, a) + mod) % mod << endl;
return 0;
}
Beautiful numbers
前面两个题写的太认真了,这个题也没什么水平,所以写的稍微水一点。
由于所有我们需要考虑的因子都是个位数。观察到 \(\text{lcm}(1,2,3,4,5,6,7,8,9)=2520\)
剩下的就很简单了,我们只在乎这个数模 \(2520\) 的值是否被填了的数整除。
所以数位DP的时候,打个取模,打个对于 \([1,9]\) 的状压,也是华丽结束。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 20;
const int mod = 2520;
int T, cur, a[mod + 1];
ll l, r, f[20][mod + 1][50];
vector<int> dim;
int gcd(int x, int y) { return x % y ? gcd(y, x % y) : y; }
int lcm_(int x, int y)
{
if (!y) return x;
return x / gcd(x, y) * y;
}
ll dfs(int x, int mode, int lcm, bool op)
{
if (!x) return mode % lcm == 0 ? 1 : 0;
if (!op && f[x][mode][a[lcm]]) return f[x][mode][a[lcm]];
int maxx = op ? dim[x] : 9;
ll ret = 0;
for (int i = 0; i <= maxx; i++) ret += dfs(x - 1, (mode * 10 + i) % mod, lcm_(lcm, i), op & (i == maxx));
if (!op) f[x][mode][a[lcm]] = ret;
return ret;
}
ll solve(ll x)
{
dim.clear();
dim.push_back(-1);
ll t = x;
while (t) dim.push_back(t % 10), t /= 10;
return dfs(dim.size() - 1, 0, 1, 1);
}
main()
{
for (int i = 1; i <= mod; i++) if (mod % i == 0) a[i] = ++cur;
scanf("%d", &T);
while (T--) scanf("%lld%lld", &l, &r), printf("%lld\n", solve(r) - solve(l - 1));
return 0;
}
New Year and Binary Tree Paths
挺有意思的题目,虽然是黑色,但是我实际做下来还好,至少结论都推出来了。
感觉在草稿纸上认真画一画就有了,只需要注意二进制的低位怎么加都加不到高位的性质。
首先我们先考虑这个路径是一条链的情况。
假设深度最浅的节点为 \(x\),路径长度是 \(h\)。
对于这条路径上的所有右儿子,假设他们在路径上的深度(假设路径最后一个节点的深度为 \(1\))分别为 \(d_1,d_2...d_m\)。
于是我们的路径权值是 \(x\times (2^{h+1}-1) + \sum _{i=1}^{m}2^{d_i}-1\)
我们发现,在知道最终路径权值的多少的情况下,我们可以通过枚举 \(h\),而此时的 \(x\) 是一定的。
其实是比较显然的,因为你后面的右儿子无论怎么选,都无法达到 \(2^{h+1}\) 级别。感性理解即可。
而在 \(x\) 一定的情况下,对于右儿子的分布二进制拆解一下显然也是固定的。
然后我们考虑这条路径是一条分叉的情况,也就是有两条链。
假设最浅的节点为 \(x\),左链长度为 \(h_1\),右链长度为 \(h_2\)。
对于左链上的所有右儿子,假设他们在路径上的深度(假设路径最后一个节点的深度为 \(1\))分别为 \(d_1,d_2...d_n\)。
对于右链上的所有右儿子(不算 \(x\) 的右儿子),假设他们在路径上的深度(假设路径最后一个节点的深度为 \(1\))分别为 \(e_1,e_2...e_m\)。
显然,这条路径的权值就是 \(x\times (2^{h_1+1} + 2^{h_2+1}-3) + (2^{h_2} - 1)+\sum_{i=1}^{n}2^{d_i}-1 +\sum_{i=1}^{m}2^{e_i}-1\)
推导一下,发现我们在枚举 \(h_1,h_2\) 之后,\(x\) 还是唯一的,与上面同理。
问题简化成了:
我们有 \(n+m\) 个整数 \(2^0-1,2^1-1,...,2^n-1,2^0-1,2^1-1,...2^m-1\),问有多少种选择子集的方案,使得选出来的子集的元素之和为 \(S\)。
我们发现这个 \(-1\) 比较恶心,考虑把他去掉。即我们枚举要选多少个数,然后这个 \(-1\) 就被去掉了。
问题转化为:
我们有 \(n+m\) 个整数 \(2^0,2^1,...,2^n,2^0,2^1,...2^m\),可以从中选择 \(k\) 个数,使得选出来的数的元素之和为 \(S\)。
其实是一个简单的 \(\text{dp}\)。
定义 \(dp_{i,j,k}\) 表示前 \(i\) 位选择了 \(j\) 个数,\(k\) 表示进不进位。
这个转移是真心简单,注意一下要等于 \(S\) 的细节就行了,具体可以看代码。
复杂度比较显然,即 \((\log n) ^5\),可过。
感觉细节还是很多的,不知道为什么 \(\texttt{PYT}\) 会觉得很好写。
#include <bits/stdc++.h>
using namespace std;
long long n;
long long dp[65][120][2], qpow[65];
long long solve(long long x, int l, int r, int m)
{
long long lim = __lg(x);
memset(dp, 0, sizeof(dp));
dp[1][0][0] = 1;
for (int i = 1; i <= lim + 1; ++i)
{
for (int j = 0; j <= 2 * i - 2; ++j)
{
for (int k = 0; k <= 1; ++k)
{
if(!dp[i][j][k]) continue;
for (int a = 0; a <= 1; ++a)
{
if(a && i >= l) continue;
for (int b = 0; b <= 1; ++b)
{
if(b && i >= r) continue;
if((k + a + b & 1) == bool(x & (1ll << i)))
dp[i + 1][j + a + b][(a + b + k) / 2] += dp[i][j][k];
}
}
}
}
}
return dp[lim + 2][m][0];
}
int main()
{
long long ans = 0;
cin >> n;
qpow[0] = 1;
for (int i = 1; i <= 60; ++i) qpow[i] = qpow[i - 1] * 2;
for (int i = 1; i <= 60; ++i)
{
if(qpow[i] > n) break;
long long now = n / (qpow[i] - 1);
if(now == 0) continue;
long long x = n - 1ll * now * (qpow[i] - 1);
for (int j = i - 1; j >= 0; --j) if(x >= qpow[j] - 1) x -= qpow[j] - 1;
if(!x) ++ans;
}
for (int i = 1; i <= 60; ++i)
{
if(qpow[i] > n) break;
for (int j = 1; j <= 60; ++j)
{
if(qpow[j] > n) continue;
long long now = (n - qpow[j] + 1) / (qpow[i + 1] + qpow[j + 1] - 3);
if(now <= 0) continue;
long long x = (n - qpow[j] + 1) - now * 1ll * (qpow[i + 1] + qpow[j + 1] - 3);
if(!x)
{
++ans;
continue;
}
if(i == 1 && j == 1)
{
ans += (x == 5ll * now + 1);
continue;
}
for (int k = 1; k <= i + j; ++k)
{
if(~(x + k) & 1)
ans += solve(x + k, i, j, k);
}
}
}
cout << ans << endl;
return 0;
}
方伯伯的商场之旅
还是比较有意思的一道题目。
我们发现对于合并石子中的终点,从最高位到最低位一定是单峰的。
故我们考虑去枚举这个终点,从高到低,如果在数位 DP 转移的过程中他优于之前的状态,那就更新,否则不更新。
具体来说,考虑先求出在最高位的答案,然后我们依次向最低位走,每次计算要减少的量。
感觉还是挺简单的(?
#include <bits/stdc++.h>
#define int long long
using namespace std;
int l, r, k;
int a[100], f[105][10005];
int dfs(int now, int sum, int p, int lim)
{
if (!now) return max(sum, 0LL);
if (!lim && ~f[now][sum]) return f[now][sum];
int ans = 0;
int num = lim ? a[now] : k - 1;
for (int i = 0; i <= num; i++) ans += dfs(now - 1, sum + (p == 1 ? i * (now - 1) : (now < p ? -i : i)), p, lim && (i == num));
if (!lim) f[now][sum] = ans;
return ans;
}
int solve(int x)
{
int n = 0;
while (x)
{
a[++n] = x % k;
x /= k;
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
memset(f, -1, sizeof(f));
ans += (i == 1 ? 1 : -1) * dfs(n, 0, i, 1);
}
return ans;
}
signed main()
{
cin >> l >> r >> k;
cout << solve(r) - solve(l - 1) << endl;
return 0;
}
Number with Bachelors
这个题我是真不想评价啊,什么时候 \(\texttt{ICPC}\) 的题也能这么烂了啊,真的 \(\texttt{QOJ}\) 上的评分都要负了。
题意即求在 \([l,r]\) 满足条件的数的个数和第 \(k\) 小的满足条件的数。
满足条件即每个数位上的数不重复出现。
额,第 \(k\) 大直接转化为二分。至于不重复出现,随便打一个 \(2^{10}\) 的状压不就有了吗。
这不是最关键的,关键是你的那一坨输入输出一会十六进制一会十进制又是什么鬼。。。。
不想写这个题了,直接奉上代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = (1 << 16) + 10;
typedef unsigned long long ull;
ull dp[2][22][maxn];
int typ, a[100];
ull dfs(int x, int sta, int limit)
{
if (x == -1) return 1;
int ty = (typ == 10) ? 0 : 1;
if (dp[ty][x][sta] != -1 && !limit) return dp[ty][x][sta];
int up = limit ? a[x] : typ - 1;
ull ans = 0;
for (int i = 0; i <= up; i++)
{
if ((sta >> i) & 1) continue;
if (sta == 0 && i == 0) ans += dfs(x - 1, sta, limit && i == up);
else ans += dfs(x - 1, sta | (1 << i), limit && i == up);
}
if (!limit) dp[ty][x][sta] = ans;
return ans;
}
ull solve(ull x)
{
int num = 0;
while (x)
{
a[num++] = x % typ;
x /= typ;
}
return dfs(num - 1, 0, true);
}
void input(ull &x)
{
x = 0;
char s[22];
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int i = 1; i <= len; i++)
{
if (s[i] <= '9' && s[i] >= '0') x = x * typ + s[i] - '0';
else x = x * typ + s[i] - 'a' + 10;
}
}
void print(ull x)
{
if (x == 0)
{
printf("0\n");
return;
}
if (typ == 10) printf("%llu\n", x);
else
{
vector<int> ans;
while (x)
{
int p = x % typ;
x /= typ;
ans.push_back(p);
}
for (int i = ans.size() - 1; i >= 0; i--)
{
if (ans[i] >= 10) printf("%c", ans[i] - 10 + 'a');
else printf("%c", ans[i] + '0');
}
printf("\n");
}
}
void test()
{
int t;
scanf("%d", &t);
while (t--)
{
int x;
scanf("%d%d", &x, &typ);
printf("%llu", solve(x));
}
}
int main()
{
memset(dp, -1, sizeof(dp));
int T;
scanf("%d", &T);
while (T--)
{
char op[10];
scanf("%s", op);
if (op[0] == 'd') typ = 10;
else typ = 16;
int flg;
scanf("%d", &flg);
if (!flg)
{
ull a, b;
input(a), input(b);
ull ans = solve(b);
if (a > 0) ans -= solve(a - 1);
print(ans);
}
else
{
ull x;
input(x);
if (x < 10)
{
printf("%llu\n", x - 1);
continue;
}
ull l = 0, r = 0, ans = 0;
r--;
if (solve(r) < x)
{
printf("-\n");
continue;
}
while (l <= r)
{
ull mid = l + (r - l) / 2;
if (solve(mid) >= x) r = mid - 1, ans = mid;
else l = mid + 1;
}
print(ans);
}
}
return 0;
}
后记
写完了,今天也要结束了。
\(\text{All tragedy erased. I see only wonders.}\)

浙公网安备 33010602011771号