NOIP2024集训 Day32 总结

前言

当坚冰还盖着北海的时候,我看到了怒放的梅花。

停课了,对于每天的题也是终于有时间写总结了。

我不会告诉你,以前没空写是因为颓废。

今天是,愉快的,数位DP专题~

淘金

乍一看,这个 n2 怎么感觉跟数位 DP 没啥关联呢。

手玩一下小数据,我们可以发现,对于每个变动到的坐标 (x,y),要使其合法,即 x,y[1,n]x,y 质因数分解之后必然为:x,y=2p1×3p2×5p3×7p4。观察到合法的 x,y[1,1012],故 p1,2,3,4 都不是很大,经过计算,p1×p2×p3×p4 大概在 2×105 级别。

由于 x,y 本质上的变动情况是相同的,故我们可以先只考虑一维然后在进行两维的合并。

考虑对于一维,有我们刚刚的推导可以得到,每一维变动之后最终的合法状态在 2×105 级别。于是我们可以直接枚举最终状态的 p1,2,3,4,通过数位 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 个数 ai,每次可以选一个数对 (i,j),每次选的不能相同,选择一个数对的代价是 ai×aj,要求选择 k 个数对的最大代价之和。

一个简单的堆就解决了,先对于每一个 i,将 ai×maxj=1naj 放入堆中,每次取出一个,就将 aj 变为次大再放进去,一直取直到限制即可。

代码写的非常难评,是时间正常人的 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,l1] 是基本思路。

看到这种子串的问题,我们先明确一个中心思路。就是说,我们可以考虑去枚举处于 [l,r] 之间每个字符串的前缀的长度,然后通过对于每个字符串的前缀的后缀的答案计算来得到最终答案。注意,我们的长度对应的贡献计算 都默认表示的是在含有数位 DP 的字典序即首位限制的情况下所对应的贡献,当然这一位也就对应的是长度,不是指的字串长度!!!

其实也就是变相的对字串转化了一下,使其更贴近于数位 DP 字典序,方便我们统计答案。这两句话还是有点抽象的,只是我自己总结出来的,可以考虑先看后面的比较直白的解法。

我们定义 numi 表示的是从首位开始进行到第 i 位时,有多少种不会被 lim 所影响的字符串。

显然有 numi=numi1×b+ai+(b1)

首先可以直接在上一位之后乱填,也可以在前面全部首位都被顶到的情况下填写 [0,ai1],也可以前面全部是前导 0,这一位填 [1,b1]

接下来我们定义 leni,0/1 表示前 i 位,0 表示被首位所限制,1 表示没有限制的满足条件的所有字符串的长度之和。

显然有 leni,0=leni1,0+1,毕竟你都被限制了显然只有一种。

leni,1=leni1,1×b+leni1,0×ai+numi1×b

其实挺显然的,没限制就是 b 种情况,有限制就只能填 [0,ai1],还是好理解的。

然后我们定义 nowi,0/1 表示前 i 位,0/1 含义相同,其对应合法的所有字符串的后缀的拼起来的数值之和

简单的 nowi,0=nowi1,0×b+leni,0×ai

显然这一位只能填 ai,而之前的答案总和要进位,故乘 blen 本质上就是所有字符串的后缀个数之和,和所有字符串的长度相同,很合理。

那么有了 0 的铺垫,1 应该也是相对好理解的,这里就不写了,可以看后面代码。

然后我们考虑定义一个 dp 来统计答案,由于我们只关心了前面 i 位的贡献,而后面的位仍存在乱填导致的方案数对答案的贡献,也需要通过这个 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

前面两个题写的太认真了,这个题也没什么水平,所以写的稍微水一点。

由于所有我们需要考虑的因子都是个位数。观察到 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)分别为 d1,d2...dm

于是我们的路径权值是 x×(2h+11)+i=1m2di1

我们发现,在知道最终路径权值的多少的情况下,我们可以通过枚举 h,而此时的 x 是一定的。

其实是比较显然的,因为你后面的右儿子无论怎么选,都无法达到 2h+1 级别。感性理解即可。

而在 x 一定的情况下,对于右儿子的分布二进制拆解一下显然也是固定的。

然后我们考虑这条路径是一条分叉的情况,也就是有两条链。

假设最浅的节点为 x,左链长度为 h1,右链长度为 h2

对于左链上的所有右儿子,假设他们在路径上的深度(假设路径最后一个节点的深度为 1)分别为 d1,d2...dn

对于右链上的所有右儿子(不算 x 的右儿子),假设他们在路径上的深度(假设路径最后一个节点的深度为 1)分别为 e1,e2...em

显然,这条路径的权值就是 x×(2h1+1+2h2+13)+(2h21)+i=1n2di1+i=1m2ei1

推导一下,发现我们在枚举 h1,h2 之后,x 还是唯一的,与上面同理。

问题简化成了:

我们有 n+m 个整数 201,211,...,2n1,201,211,...2m1,问有多少种选择子集的方案,使得选出来的子集的元素之和为 S

我们发现这个 1 比较恶心,考虑把他去掉。即我们枚举要选多少个数,然后这个 1 就被去掉了。

问题转化为:

我们有 n+m 个整数 20,21,...,2n,20,21,...2m,可以从中选择 k 个数,使得选出来的数的元素之和为 S

其实是一个简单的 dp

定义 dpi,j,k 表示前 i 位选择了 j 个数,k 表示进不进位。

这个转移是真心简单,注意一下要等于 S 的细节就行了,具体可以看代码。

复杂度比较显然,即 (logn)5,可过。

感觉细节还是很多的,不知道为什么 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

这个题我是真不想评价啊,什么时候 ICPC 的题也能这么烂了啊,真的 QOJ 上的评分都要负了。

题意即求在 [l,r] 满足条件的数的个数和第 k 小的满足条件的数。

满足条件即每个数位上的数不重复出现。

额,第 k 大直接转化为二分。至于不重复出现,随便打一个 210 的状压不就有了吗。

这不是最关键的,关键是你的那一坨输入输出一会十六进制一会十进制又是什么鬼。。。。

不想写这个题了,直接奉上代码:

#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;
}

后记

写完了,今天也要结束了。

All tragedy erased. I see only wonders.

posted @   Saltyfish6  阅读(23)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
Document
点击右上角即可分享
微信分享提示