【高手训练】数位(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 imes 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)都在后面,但是我写不动了。
鸽了吧。