导图
智乃酱的子集与超集(SOSdp)
题意描述 (
)个物品做为一个全集,第
个物品价值
,一个集合的价值为集合物品价值的异或和。
次询问,询问选择其中一些物品 {
},它的所有子集价值之和 与 所有全集价值之和。
分析
前置芝士:高维前缀和
以二维为例理解计算方法:
第一步:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
sum[i][j] = a[i][j]; 
就是单个点,相当于是0维前缀和
第二步:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
sum[i][j] += sum[i - 1][j] 
此时存了当前列的前缀和,即计算了1维前缀和
第三步:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
sum[i][j] += sum[i][j - 1]; 
此时已经完成了2维前缀和的计算
因此可以推广一下这个做法,**求维前缀和**,则从0维开始,每次对一个维度做一次前缀和,相当于起到了一个降维的作用,做了
次就可以求解出
维前缀和了。时间复杂度
代码:
for (i : 空间元素)
sum[i] = a[i];
for (i = 1; i <= k; i++)
for (j : 空间元素)
sum[j] += sum[k] [k的第i维 + 1 = j] 下面就是这题的具体思路了
高维前缀和的一个应用就是 (
),常与
结合使用。
在这题中,可以构建一个维空间,每一维长度都是
,取值
和
。每个物品都是一个维度,不选取
,选了取
。
因此维空间中的某一个点都表示一种物品集合,它的
维前缀和就是它所有子集的价值之和,它的
维后缀和就是它所有超集的价值之和。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 30, S = 1 << 20;
int n, m, k;
ll a[N], presum[S], sufsum[S];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%lld", &a[i]);
for (int i = 0; i < 1 << n; i++)
{
ll sum = 0;
for (int j = 0; j < n; j++)
if (i & (1 << j))
sum ^= a[j];
presum[i] = sum;
sufsum[i] = sum;
}
for (int i = 0; i < n; i++)
for (int j = 0; j < 1 << n; j++)
if (j & (1 << i)) presum[j] += presum[j ^ (1 << i)];
else sufsum[j] += sufsum[j ^ (1 << i)];
while (m--)
{
scanf("%d", &k);
int q = 0;
for (int i = 1; i <= k; i++)
{
int x;
scanf("%d", &x);
q |= 1 << x - 1;
}
printf("%lld %lld\n", presum[q], sufsum[q]);
}
return 0;
}
智乃酱的前缀和与差分(高阶前缀和)
题意描述
给长度是 的序列
,求其
次前缀和,前缀和对
取模。
分析
如果序列做前缀和取的模数大与序列的长度,那么前缀和序列会以长度为模数的循环节循环出现。
这里由于模数大于序列长度,可以对负数的 (差分) 取模后转化为前缀和。
NTT卷积求解:
首先对于序列 [] 列出做前缀和后的表格:
| 序号 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 原序列 | |||||
| 一次前缀和 | |||||
| 二次前缀和 | |||||
| 三次前缀和 | |||||
| … |
只保留其中的系数得到:
| 序号 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 原序列 | 1 | 0 | 0 | 0 | 0 |
| 一次前缀和 | 1 | 1 | 1 | 1 | 1 |
| 二次前缀和 | 1 | 2 | 3 | 4 | 5 |
| 三次前缀和 | 1 | 3 | 6 | 10 | 15 |
| … |
首先容易得到一个递推式,第次前缀和序号
的系数
这个递推式是有实际含义的:在网格中从(1,0)出发,每次只能向下或向右移动一格,求移动到(k,i)的方案数。
答案就是
因此可以通过组合数递推的方式 得到做
次前缀和后
的系数序列
如果把 都考虑进来,做
次前缀和的结果像下面这样计算(以序列长度为3,做3次前缀和为例):

这时已经可以意识到 序列
次 前缀和的结果就是
序列 与做
次前缀和的 系数序列
的卷积。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int mod = 998244353;
const int N = 100010;
const int G = 3;
int a[N << 2], b[N << 2], rev[N << 2];
int inv[N];
void getb(ll k, int n)
{
k = (k % mod + mod) % mod;
b[0] = 1;
for (int i = 1; i < n; i++)
b[i] = (ll)b[i - 1] * (i + k - 1) % mod * inv[i] % mod;
}
int qmi(int a, int b, int p)
{
int ans = 1;
for (; b; b >>= 1)
{
if (b & 1) ans = (ll)ans * a % p;
a = (ll)a * a % p;
}
return ans;
}
void ntt(int a[], int tot, int sgn, int p)
{
for (int i = 0; i < tot; i++)
if (i < rev[i])
swap(a[i], a[rev[i]]);
int g = sgn == 1 ? G : qmi(G, p - 2, p);
for (int mid = 1, t = 1; mid < tot; mid <<= 1, t++)
{
int wn = qmi(g, (p - 1) >> t, p);
for (int i = 0; i < tot; i += mid << 1)
{
int w = 1;
for (int j = 0; j < mid; j++, w = (ll)w * wn % p)
{
int x = a[i + j], y = (ll)w * a[i + mid + j] % p;
a[i + j] = (x + y) % p;
a[i + mid + j] = ((x - y) % p + p) % p;
}
}
}
if (sgn == -1)
{
int invtot = qmi(tot, p - 2, p);
for (int i = 0; i < tot; i++)
a[i] = (ll)a[i] * invtot % p;
}
}
int main()
{
inv[1] = 1;
for (int i = 2; i < N; i++)
inv[i] = (ll)(mod - mod / i) * inv[mod % i] % mod;
int n; ll k;
scanf("%d%lld", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
getb(k, n);
n--;
int tot = 1, bit = 0;
while (tot < n + n + 1) tot <<= 1, ++bit;
for (int i = 0; i < tot; i++)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
ntt(a, tot, 1, mod), ntt(b, tot, 1, mod);
for (int i = 0; i < tot; i++) a[i] = (ll)a[i] * b[i] % mod;
ntt(a, tot, -1, mod);
n++;
for (int i = 0; i < n; i++)
printf("%d%c", a[i], i == n - 1 ? '\n' : ' ');
return 0;
} 智乃酱的静态数组维护问题多项式(多项式前缀和)
题意描述:
对于一个序列,每次选择 [l, r] 加上一个多项式
(即 )
最后询问 [l, r] 的元素之和。
分析:
数学定理:最高次项为 n 次的 n 阶多项式做 n + 1 阶差分后余项为常数(常数0)。
可以将原数组做 次差分,然后对差分数组做有限次修改,最后前缀和求回来即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pli pair<ll,int>
#define Min(a,b,c) min(a,min(b,c))
#define Max(a,b,c) max(a,max(b,c))
typedef long long ll;
typedef unsigned long long ull;
const double pi = 3.141592653589793;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
const int N = 100010;
const ll mod = 1000000007;
ll a[N], p[10], f1[10], f2[10];
void S(ll a[], int n, int t)
{
while (t--)
{
for (int i = 1; i <= n; i++)
a[i] = (a[i - 1] + a[i]) % mod;
}
}
void D(ll a[], int n, int t)
{
while (t--)
{
for (int i = n; i >= 1; i--)
a[i] = (a[i] - a[i - 1]) % mod;
}
}
ll f(ll x, ll p[], int k)
{
ll base = 1, ans = 0;
for (int i = 0; i <= k; i++)
{
ans = (ans + p[i] * base % mod) % mod;
base = base * x % mod;
}
return ans;
}
int main()
{
int n, m, q;
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
D(a, n, 6);
while (m--)
{
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
for (int i = k; i >= 0; i--) scanf("%lld", &p[i]);
for (int i = 1; i <= 6; i++)
{
f1[i] = f(i, p, k);
f2[i] = (mod - f(i + r - l + 1, p, k)) % mod;
}
D(f1, 6, 6);
D(f2, 6, 6);
for (int i = 1; i <= 6; i++)
{
a[l + i - 1] = (a[l + i - 1] + f1[i]) % mod;
a[r + i] = (a[r + i] + f2[i]) % mod;
}
}
S(a, n, 7);
while (q--)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", ((a[r] - a[l - 1]) % mod + mod) % mod);
}
return 0;
} 智乃酱的双塔问题(DP、前缀和、矩阵)
题意描述
左右有两个高度为n个塔,对于每个塔可以从下一层直接到上一层。对于每层(不包括顶层)额外有一个楼梯要么是从左塔上到右塔,要么是从右塔往上走一层到左塔。
每次询问从左边或右边塔的第 i 层走到左边或右边的第 j 层的方案数(i < j)。
分析
首先想到,
表示左边的塔从第一层走到第
层的方案数,
表示右边的塔从第一层走到第
层的方案数。那么就有转移方程:
$$
因此从某一层到某一层的方案数,就是这一段矩阵的连乘积。
因此可以预处理出矩阵的前缀和,因为两种可能出现的矩阵都是可逆矩阵,最后为了消除左半部分影响,乘一下左半部分矩阵的逆即可。(要注意是左乘)
代码
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pli pair<ll,int>
#define Min(a,b,c) min(a,min(b,c))
#define Max(a,b,c) max(a,max(b,c))
typedef long long ll;
typedef unsigned long long ull;
const double pi = 3.141592653589793;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
const int N = 100010;
const ll mod = 1000000007;
struct mat
{
ll a[2][2];
mat() {memset(a, 0, sizeof(a));}
mat(int a1, int a2, int a3, int a4)
{
a[0][0] = a1;
a[0][1] = a2;
a[1][0] = a3;
a[1][1] = a4;
}
};
int n, m;
char s[N];
mat sum[N], A(1, 1, 0, 1), B(1, 0, 1, 1);
ll qmi(ll a, ll b)
{
ll ans = 1;
for ( ; b; b >>= 1)
{
if (b & 1) ans = ans * a % mod;
a = a * a % mod;
}
return ans;
}
ll inv(ll x)
{
return qmi(x, mod - 2);
}
void mul(ll a[][2], ll b[][2])
{
ll c[2][2];
memset(c, 0, sizeof(c));
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
for (int k = 0; k < 2; k++)
c[i][j] = (c[i][j] + a[i][k] * b[k][j] % mod) % mod;
memcpy(a, c, sizeof(c));
}
void inv(ll a[][2])
{
int n = 2, is[2], js[2];
memset(is, 0, sizeof(is));
memset(js, 0, sizeof(js));
for (int k = 0; k < n; k++)
{
for (int i = k, j; i < n; i++)
{
for (j = k; j < n && !a[i][j]; j++);
is[k] = i, js[k] = j;
}
for (int i = 0; i < n; i++)
swap(a[k][i], a[is[k]][i]);
for (int i = 0; i < n; i++)
swap(a[i][k], a[i][js[k]]);
if (!a[k][k])
{
puts("No Solution");
exit(0);
}
a[k][k] = inv(a[k][k]);
for (int j = 0; j < n; j++)
if (j != k)
(a[k][j] *= a[k][k]) %= mod;
for (int i = 0; i < n; i++)
if (i != k)
{
ll temp = a[i][k];
a[i][k] = 0;
for(int j = 0; j < n; ++j)
(a[i][j] += mod - a[k][j] * temp % mod) %= mod;
}
}
for (int k = n - 1; k >= 0; k--)
{
for (int i = 0; i < n; i++)
swap(a[js[k]][i], a[k][i]);
for (int i = 0; i < n; i++)
swap(a[i][is[k]],a[i][k]);
}
}
int main()
{
scanf("%d%d", &n, &m);
scanf("%s", s + 1);
sum[0] = mat(1, 0, 0, 1);
for (int i = 1; i < n; i++)
{
sum[i] = sum[i - 1];
if (s[i] == '/') mul(sum[i].a, A.a);
else mul(sum[i].a, B.a);
}
while (m--)
{
int l, r, pl, pr;
scanf("%d%d%d%d", &l, &r, &pl, &pr);
mat ans = sum[l - 1];
inv(ans.a); mul(ans.a, sum[r - 1].a);
printf("%d\n", ans.a[pl][pr]);
}
return 0;
} 牛牛的猜球游戏(置换前缀和)
题意描述
给定1~10的排列,有一个长度是的操作序列,序列中每个操作是交换排列中的两个元素。接下来有m次询问,每次询问,对于
,运用 l ~ r 的操作序列,排列是什么样的。
分析
一系列操作,每次操作后会得到一个新的排列,维护前缀排列即可。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double pi = 3.141592653589793;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
const int N = 100010;
int a[N][10];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < 10; i++) a[0][i] = i;
for (int i = 1; i <= n; i++)
{
int x, y;
scanf("%d%d", &x, &y);
for (int j = 0; j < 10; j++)
a[i][j] = a[i - 1][j];
swap(a[i][x], a[i][y]);
}
while (m--)
{
int l, r;
scanf("%d%d", &l, &r);
int t[10];
for (int i = 0; i < 10; i++)
t[a[l - 1][i]] = i;
for (int i = 0; i < 10; i++)
printf("%d ", t[a[r][i]]);
puts("");
}
return 0;
} [NOIP2013]积木大赛(差分,经典题)
题意描述
给定长度是n的序列a,序列元素。还有一个同样长度全0的b序列,每次可以选择b序列一段区间 [l, r],让区间中每个元素加1。问最少做多少次操作让b=a。
分析
考虑用差分数组实现区间修改,如果要让 [l, r] 加1,就是在差分数组中 d[l]+=1, d[r] -= 1。
那么对序列a做一次差分,序列中正数之和是等于负数之和的绝对值的。
例如:
a 3 5 2 1 4
d 3 2 -3 -1 3 -4
那么最后的答案就等于差分序列中正数之和或者负数之和的绝对值。
代码
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pli pair<ll,int>
#define Min(a,b,c) min(a,min(b,c))
#define Max(a,b,c) max(a,max(b,c))
typedef long long ll;
typedef unsigned long long ull;
const double pi = 3.141592653589793;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
const int N = 100010;
int a[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int ans = 0;
for (int i = n; i >= 1; i--)
{
a[i] = a[i] - a[i - 1];
ans += a[i] > 0 ? a[i] : 0;
}
printf("%d\n", ans);
return 0;
} 
京公网安备 11010502036488号