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.}\)

posted @ 2024-09-18 20:29  Saltyfish6  阅读(32)  评论(1)    收藏  举报
Document