【高手训练】数位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)\)具有单调性。
感性理解:
而我们通过前缀和的方式求解\(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) \]
(不想手打了浪费时间)
然后就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\)都在后面,但是我写不动了。
鸽了吧。