【高手训练】数位DP系列(暂完)

【高手训练】数位\(dp\)学习笔记

<前言>

也就是不停讲题写题,没讲什么实质性数位\(dp\),甚至只有后面三题有点点味道。

前面就是用到了数位的相关知识而已。

<正文>

【高手训练】【动态规划】最大值

题目大意

多组数据,每组几个操作\(opt\)与n个数,\(opt\)可能是\(and、xor、or\),求任意两数\(opt\)运算后的最大值。

Solution

显然我们得分操作进行。


\(Xor\)

\(opt=xor\)时,你会发现这题十分熟悉。

The XOR Largest Pair(随手网上找的OJ)

我们发现对于一个0/1,我们需要找到和它相对的方向走

比如当一位为1,我们要有最大代价,就应该找0与之形成贡献。

然而对于找不到相对值的位置,没有办法,只能找相同数了

直接一个\(Trie\)树板子套上去就行了。

\(\mathrm{Code:}\)

struct trie
{
    int tr[N << 1][2];
    int cnt = 0;
    inline void inc(int x)
    {
        int now = 0;
        for(int i = 30; i >= 0; --i)
        {
            int t = x >> i & 1;
            if(!tr[now][t])tr[now][t] = ++cnt;
            now = tr[now][t];
        }
    }
    inline int ask(int x)
    {
        int now = 0, ans = 0;
        for(int i = 30; i >= 0; --i)
        {
            int t = x >> i & 1;
            if(tr[now][t ^ 1])
                ans += 1 << i, now = tr[now][t ^ 1];
            else now = tr[now][t];
        }
        return ans;
    }
    inline void clear()
    {
        memset(tr, 0, sizeof(tr));
        cnt = 0;
    }
} tr;

\(And\)

(直接念题解:)

从高位到低位贪心。毕竟and的限制性还是比较强的。

  • 对于某一位,如果在没被删除的数中1的个数超过2个,那么上下那些不合法的数随便删,因为就算那些数接下来的1再多,造成的贡献也不及这一次保留(\(2^{i-1}+2^{i-2}...+2^0=2^i-1<2^i\))。

    然后累计当前二进制位的贡献(\(ans+=(1<<i)\))。

  • 如果当前位是1的数的个数不到两个,那么删掉只会造成贡献变小,则不删,直接考虑下一位。

\(\mathrm{Code:}\)

int vis[N] = {};
for(int i = 30; i >= 0; --i){
    int cnt = 0, op = 0;
    for(int j = 1; j <= n; ++j)
        if(!vis[j]){
            ++op;
            cnt += a[j] >> i & 1;
        }
    if(op <= 2)break;
    if(cnt > 1){
         for(int j = 1; j <= n; ++j)
            if(!vis[j] && ((a[j] >> i & 1 ) == 0))
                vis[j] = 1;
    }
}
int ans = (1 << 30) - 1;
for(int i = 1; i <= n; ++i)
    if(!vis[i])ans &= a[i];

\(Or\)

or比较烦,因为限制少,答案难求,但我们依然从高位到低位贪心。

考虑每次都尽量取优。枚举某个数的不同位。

  • 若当前枚举的位为1,那么另一数可以是0或1。
  • 若当前枚举的位位0,那么另一数尽量取1。

我们对于每个数,找到满足最优条件(如上规则)的情况下,最小的一个值,记为\(val\)。可以理解为:当某一位可选可不选的时候,让它为0.

这么搞出来的\(val\)一定是某个可行解的子集。

  • 处理子集,设\(F[x]\)表示是否存在一个\(a_i\)满足\(x∈a_i\)

  • 当某一位\(i\)满足存在某个\(a_i\)使\(val|2^i∈a_i\),则第\(i\)位可以为1.这个相当于存在这么一种方案使第i位为1.

    贡献加上。

最后拼出的答案即为所求。

状压\(dp\)\(\mathrm{Code:}\)

memset(f, 0, sizeof(f));
for(int i = 1; i <= n; ++i)f[a[i]] = 1;
for(int i = (1 << 20) - 1; i >= 0; --i)
    for(int j = 0; j <= 20; ++j)
        f[i] |= f[i | (1 << j)];

Code

完整代码:

#include<bits/stdc++.h>
#define N 200010
#define int long long

int n, a[N] = {};
int k;

inline int read()
{
    int s = 0, w = 1;
    char c = getchar();
    while((c < '0' || c > '9') && c != '-')
        c = getchar();
    if(c == '-')w = -1, c = getchar();
    while(c <= '9' && c >= '0')
        s = (s << 3) + (s << 1) + c - '0', c = getchar();
    return s * w;
}
void write(int x)
{
    if(x > 9)write(x / 10);
    putchar(x % 10 + 48);
}
                    //IO
int f[1 << 21] = {};
struct trie
{
    int tr[N << 1][2];
    int cnt = 0;
    inline void inc(int x)
    {
        int now = 0;
        for(int i = 30; i >= 0; --i)
        {
            int t = x >> i & 1;
            if(!tr[now][t])tr[now][t] = ++cnt;
            now = tr[now][t];
        }
    }
    inline int ask(int x)
    {
        int now = 0, ans = 0;
        for(int i = 30; i >= 0; --i)
        {
            int t = x >> i & 1;
            if(tr[now][t ^ 1])
                ans += 1 << i, now = tr[now][t ^ 1];
            else now = tr[now][t];
        }
        return ans;
    }
    inline void clear()
    {
        memset(tr, 0, sizeof(tr));
        cnt = 0;
    }
} tr;
              //trie树

void work()
{
    n = read();
    k = read();
    for(int i = 1; i <= n; ++i)
        a[i] = read();
    if(k == 1)
    {
        int vis[N] = {};
        for(int i = 30; i >= 0; --i)
        {
            int cnt = 0, op = 0;
            for(int j = 1; j <= n; ++j)
                if(!vis[j])
                {
                    ++op;
                    cnt += a[j] >> i & 1;
                }
            if(op <= 2)break;
            if(cnt > 1)
            {
                for(int j = 1; j <= n; ++j)
                    if(!vis[j] && ((a[j] >> i & 1 ) == 0))
                        vis[j] = 1;
            }
        }
        int ans = (1 << 30) - 1;
        for(int i = 1; i <= n; ++i)
            if(!vis[i])ans &= a[i];
        write(ans);
        putchar(10);
        return ;
    }
    if(k == 2)
    {
        tr.clear();
        for(int i = 1; i <= n; ++i)
            tr.inc(a[i]);
        int ans = 0;
        for(int i = 1; i <= n; ++i)
            ans = std::max(ans, tr.ask(a[i]));
        write(ans);
        putchar(10);
        return ;
    }
    if(k == 3)
    {
        memset(f, 0, sizeof(f));
        for(int i = 1; i <= n; ++i)f[a[i]] = 1;
        for(int i = (1 << 20) - 1; i >= 0; --i)
            for(int j = 0; j <= 20; ++j)
                f[i] |= f[i | (1 << j)];
        int sum = 0, val = 0;
        for(int i = 1; i <= n; ++i)
        {
            val = 0;
            for(int j = 20; j >= 0; --j)
                if(!((a[i] >> j) & 1) && f[val | (1 << j)])
                    val |= 1 << j;
            sum = std::max(sum, a[i] | val);
        }
        write(sum);
        putchar(10);
    }
}
main()
{
    freopen("maxium.in", "r", stdin);
    freopen("maxium.out", "w", stdout);
    int T = read();
    while(T--)
        work();
    return 0;
}

【高手训练】【动态规划】寻找整数

题目大意

给定整数\(m,k\),求出正整数\(n\)使得\(n+1,n+2,...,2n\)中恰好有个\(m\)数在二进制下恰好有\(k\)\(1\)。有多组数据。

Solution

这种题一上来就没头没脑的,该怎么做?

稍微手推几组相邻答案,尝试寻找关联。

  • \(Num(x)\)为区间\([x+1,2x]\)之间的二进制数中1的个数为\(k\)的个数
  • 我们通过观察发现\(Num(x)\)具有单调性。

感性理解:

image-20200525182531104

而我们通过前缀和的方式求解\(Num(n)\)

  • \(S(x)\)\([1,x]\)中二进制位\(k\)的个数。
  • \(Num(x)=S(2x)-S(x)\)

\(S(x)\)

  • 从高位到低位考虑,每个小于(等于)x的数。钦定当前位为1,则前面几位都和\(x+1\)一样.那么对于后面的位置可以计算方案。
  • 记录剩下位数\(n\)和前面1的个数\(m\),方案为\(\mathrm{C(k-m,n)}\)

对于答案

  • 二分极值,然后相减。
  • 特判:\(m=0\)\(2^k-1\)(恰好\(k\)\(1\)
  • \(m=1且k=1\)时特判\(-1\)(无数组解)

\(\mathrm{Code:}\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m;

int read() {
    int s = 0, w = 1;
    char c = getchar();
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') w = -1, c = getchar();
    while (c <= '9' && c >= '0')
        s = (s << 3) + (s << 1) + c - '0', c = getchar();
    return s * w;
}
void write(int x) {
    if (x > 9) write(x / 10);
    putchar(x % 10 + 48);
}
int c[85][85];
inline int ask(int x) {
    int ans = 0, r = 0;
    ++x;
    for (int i = 63; ~i; --i)
        if (x >> i & 1LL) {
            if (m >= r) ans += c[i][m - r];
            ++r;
        }
    return ans;
}

inline bool check(int x) { return ask(x << 1) - ask(x) >= n; }

int find() {
    int l = 0, r = 2e18, mid, ans = 0;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (check(mid))
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }
    return ans;
}

void work(void) {
    n = read();
    m = read();
    if (n == 0) {
        write(1);
        putchar(32);
        write((1LL << m - 1) - 1);
        putchar(10);
        return void();
    }
    if (n == 1 && m == 1) {
        puts("1 -1");
        return void();
    }
    int k1 = find();
    ++n;
    int k = find();
    write(k1);
    putchar(32);
    write(k - k1);
    putchar(10);
    return void();
}
void pre(void) {
    for (int i = 0; i <= 64; ++i) c[i][0] = 1;
    for (int i = 1; i <= 64; ++i)
        for (int j = 1; j <= i; ++j) c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
}
main() {
    freopen("num.in", "r", stdin);
    freopen("num.out", "w", stdout);
    pre();
    int T = read();
    while (T--) work();
    return 0;
}

【高手训练】【动态规划】好数字

题目大意

一个数字被称为好数字需满足下列条件:

①它有个\(2 \times n\)数位,\(n\)是正整数(允许有前导\(0\))。

②构成它的每个数字都在给定的数字集合\(S\)中。

③它前\(n\)位之和与后\(n\)位之和相等或者它奇数位之和与偶数位之和相等

例如,对于\(n=2\)\(S=\{1,2\}\),合法的好数字有\(8\)个:

\(1111\)\(1122\)\(1212\)\(1221\)\(2112\)\(2121\)\(2211\)\(2222\)

已知,求合法的好数字个数\(mod\ 999983\)

Solution

我们可以通过一些操作得出一些奇怪结论。

  • \(前n位=后n位\)\(奇数位=偶数位\) == \(前n位=后n位\) + \(奇数位=偶数位\) - \(前n位=后n位\)\(奇数位=偶数位\)

  • \(f(n,m)\)为用给定数字集拼出\(m\)的方案数,可以用01背包\(mb\)预处理。

  • 我们发现两者本质其实相同,答案都为

  • \[\sum_{i=1}^{max}f^2(n,i) \]

    (不想手打了浪费时间)

    image-20200525210302388

然后就tm瞎算。

\(\mathrm{Code:}\)

#include <bits/stdc++.h>
#define N 1010
#define mod 999983
#define int long long
using namespace std;
int n, m;

inline int add(int a, int b) { return a + b >= mod ? a + b - mod : a + b; }
inline int del(int a, int b) { return a - b <= 0 ? a - b + mod : a - b; }

int read() {
    int s = 0, w = 1;
    char c = getchar();
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') w = -1, c = getchar();
    while (c <= '9' && c >= '0')
        s = (s << 3) + (s << 1) + c - '0', c = getchar();
    return s * w;
}
void write(int x) {
    if (x > 9) write(x / 10);
    putchar(x % 10 + 48);
}
int a[11] = {}, cnt = 0;
int f[N][N * 10] = {};
void work() {
    n        = read();
    int maxn = 0;
    char c   = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c <= '9' && c >= '0') a[++cnt] = c - '0', c = getchar();
    for (int i = 1; i <= cnt; ++i) maxn = max(maxn, a[i]);
    for (int i = 1; i <= cnt; ++i) f[1][a[i]] = 1;
    for (int i = 2; i <= n; ++i)
        for (int j = 0; j <= i * maxn; ++j)
            for (int k = 1; k <= cnt; ++k)
                if (j >= a[k]) f[i][j] = add(f[i][j], f[i - 1][j - a[k]]);
    int ans = 0;
    for (int i = 0; i <= n * maxn; ++i)
        f[n][i] ? ans = add(ans, 2 * f[n][i] % mod * f[n][i] % mod) : 0;
    int mid = n >> 1, res = n - mid, s1 = 0, s2 = 0;
    for (int i = 0; i <= mid * maxn; ++i)
        f[mid][i] ? s1 = add(s1, f[mid][i] * f[mid][i] % mod) : 0;
    for (int i = 0; i <= res * maxn; ++i)
        f[res][i] ? s2 = add(s2, f[res][i] * f[res][i] % mod) : 0;
    ans = del(ans, s1 ? 1LL * s1 * s2 % mod : s2);
    write(ans);
    putchar(10);
}
main() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);
    work();
    return 0;
}

<后记>

后面的题都没写了

虽然真正的数位\(dp\)都在后面,但是我写不动了。

鸽了吧。

posted @ 2020-05-25 15:31  云烟万象但过眼  阅读(56)  评论(0编辑  收藏  举报