字首和的n個神奇操作

扇與她盡失發表於2021-09-05

前情回顧

字首和的基礎用法戳這裡—>傳送門

眾所周知,簡單的字首和解決的一般都是靜態查詢的問題,例如區間和、區間積等

操作的時候也很簡單,就是根據需要來維護一個陣列,每次查詢的時候就用到tr[r] 與 tr[l - 1]這兩個值來得出答案

例如:

A 智乃醬的區間乘積

題目描述:

給你一個長度為n的陣列tr,m次查詢,每次查詢給你 l 和 r ,問tr[l] 乘到 tr[r] 後對1e9+7取模是多少

思路:

用到了取模,且查詢的時候還涉及到了除法,所以要用逆元,而又因為模數是素數,所以可以用費馬小定理求逆元

維護一個mul的字首積陣列,查詢的時候就直接輸出mul[r] * getniyuan(mul[l - 1])即可,getniyuan()是求逆元的函式

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%lld",&n)
#define sdd(n,m) scanf("%lld %lld",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n, m;
ll x, l, r;
ll mul[MAX];

ll q_pow(ll a, ll b, ll MOD){
    ll ans = 1;
    while(b > 0){
        if(b & 1)ans = ans * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return ans;
}

inline ll getniyuan(ll a, ll p){
    return q_pow(a, p - 2, p);
}

int main(){
    cin>>n>>m;
    mul[0] = 1;
    sd(mul[1]);
    for(int i = 2; i <= n; ++i){
        sd(x);
        mul[i] = mul[i - 1] * x % mod;
    }
    for(int i = 1; i <= m; ++i){
        sdd(l, r);
        ll ans = mul[r] * getniyuan(mul[l - 1], mod) % mod;
        cout<<ans<<endl;
        
    }
    return 0;
}


這只是字首和的基礎用法,接下來要講的才是重頭戲

“字首和”的再定義!

廣義的“字首和”運算是指連續的進行若干次操作,產生一個疊加影響,且這種影響可以通過某種反向操作“撤銷”。

比較常見的有滿秩矩陣的乘積、字首置換、卷積等

先講解一下字首置換是怎麼回事

F牛牛的猜球遊戲

題目描述:

給從0到9編號的十個球,原始狀態是0,1,2……9,現在有n個交換操作,m次查詢,查詢的時候會給出l和r,每次查詢的是球從最開始的狀態,經過 l 到 r 的操作後的狀態是什麼

思路:

仔細想想,其實是符合廣義的“字首和”的定義的,我們要做的就是消除前 l 個球產生的影響

舉個例子:

1 2 3 經過若干次操作得到3 1 2

而要消除3 1 2之前的操作,也就是要將3 1 2變成1 2 3,那就逆過來搞,這樣如果我們要消掉3 1 2之前的操作,也就是相當於最開始是2 3 1的順序。1 2 3對應r的順序是可以得到的,那我們就可以根據2 3 1與1 2 3 的對應關係推出2 3 1開頭時r的順序,這就是我們要求得順序,這就是廣義的“字首和”的思路,通過某些方法消除前 l 個產生的影響

在這裡插入圖片描述

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%d",&n)
#define sdd(n,m) scanf("%d %d",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n, m, a, b, l, r;
int pos[MAX][10];
int tr[MAX][10];

int main(){
    sdd(n, m);
    for(int i = 0; i < 10; ++i)pos[0][i] = tr[0][i] = i;
    for(int i = 1; i <= n; ++i){
        sdd(a, b);
        for(int j = 0; j < 10; ++j){
            tr[i][j] = tr[i - 1][j];
            pos[i][j] = pos[i - 1][j];
        }
        swap(pos[i][tr[i][a]], pos[i][tr[i][b]]);
        swap(tr[i][a], tr[i][b]);
    }
    for(int i = 1; i <= m; ++i){
        sdd(l, r);
        for(int j = 0; j < 10; ++j){
            if(!j)cout<<pos[l - 1][tr[r][j]];
            else cout<<" "<<pos[l - 1][tr[r][j]];
        }
        cout<<endl;
    }
    return 0;
}

再講講“字首和”和“滿秩矩陣的乘積”的聯絡

首先,滿秩矩陣A乘以滿秩矩陣B,得到的矩陣必然是滿秩的

證明: 因為A、B滿秩,|A| > 0, |B| > 0, 而|AB| = |A| * |B| > 0, 所以AB是滿秩的

其次,滿秩矩陣A必然可逆

證明:因為A滿秩,所以|A| > 0,即|A| != 0,所以必然可逆

所以,滿秩矩陣可以用進行連乘,而通過逆矩陣撤銷一部分的貢獻,就可以獲得特定部分(即[l, r])的貢獻

E智乃醬的雙塔問題

題目描述:

兩座N層高的塔,每座塔內部的每一層都有的僅能向上的樓梯,此外,除了頂樓以外,每層都有一座連線兩個高塔的樓梯,要麼是左 i 連到右 i + 1,要麼是右 i 連到左 i + 1,我們用' / '表示第一種情況,用 ' \ '表示第二種情況

現在你想知道從某座高塔的第hs層移動到第ht層的方案數

思路:

首先,映入腦海的是dp,假如每次都從第一層開始走,那肯定是用dp了,dp[i] [0] 表示到第 i 層的左側的方案數,dp[i] [1]表示到第 i 層右側的方案數

  • dp[i] [0] = dp[i - 1] [0] + s[i - 1] == ' \ ' : dp[i - 1] [1] ? 0;
  • dp[i] [1] = dp[i - 1] [1] + s[i - 1] == ' / ' :dp[i - 1] [0] ? 0;

但是,這個題是從任意一層,任意一邊開始,這樣就不能用dp了,因為不滿足無後效行的原則了,再看看“從某一層到另一層”顯然是要用字首和,但是how?簡單的維護字首和數字肯定不行,就像剛剛的dp一樣是不對的,這時候就到了矩陣的用處了,我們使用矩陣連乘當作“字首和”,消除影響時就可以用逆矩陣來消除

用兩個二維矩陣分別代表兩種有不同橋的層
在這裡插入圖片描述a表示從該層左邊到上面那層左邊的方案數,b表示當前這層左邊通過斜著的樓梯到右邊的上面那層的方案數,c表示當前這層從右邊通過斜著的左邊的樓梯到達左邊的那層的上面那層的方案數,d表示從當前這層的右邊到上面那層的右邊的方案數

所以如果是' / ',就是x那樣

如果是' \ ',就是 y 那樣

這樣其實一個矩陣的四個點都是由兩個轉移而來,比如矩陣x * x,得到矩陣xx中的a是由x1.a * x2.a + x1.c * x2.a, 也就是左邊的樓的第i層,可以由左邊樓的i-1層直接上來,也可以從i-1層的右邊斜著上來,和上面的dp方程不能說是一模一樣,只能說是大差不離

這樣按輸入給的,就可以將矩陣連乘得到一個“字首和”,每次查詢的時候就再通過逆矩陣消除開始的那層的下面的所有層的影響即可

#include<bits/stdc++.h>
using namespace std;
const int MAX_MAT = 2;
const long long mod = 1e9 + 7;
struct Mat
{
	long long a[MAX_MAT][MAX_MAT];
	Mat()
	{
		for (int i = 0; i < MAX_MAT; ++i)
		{
			for (int j = 0; j < MAX_MAT; ++j)
			{
				a[i][j] = 0;
			}
		}
		for (int i = 0; i < MAX_MAT; ++i)
		{
			a[i][i] = 1;
		}
	}
	Mat(long long a1, long long a2, long long a3, long long a4)
	{
		a[0][0] = a1;
		a[0][1] = a2;
		a[1][0] = a3;
		a[1][1] = a4;
	}
};
long long quickpow(long long x, long long y, long long MOD = 9223372036854775807LL)
{
	long long ans = 1;
	while (y)
	{
		if (y & 1)
		{
			ans = (x * ans) % MOD;
		}
		x = (x * x) % MOD;
		y >>= 1;
	}
	return ans;
}
long long A[MAX_MAT][MAX_MAT << 1];
long long get_inv(long long x)
{
	return quickpow(x, mod - 2, mod);
}
void row_minus(int a, int b, long long k)
{
	for (int i = 0; i < 2 * MAX_MAT; ++i)
	{
		A[a][i] = (A[a][i] - A[b][i] * k % mod) % mod;
		if (A[a][i] < 0)A[a][i] += mod;
	}
	return;
}
void row_multiplies(int a, long long k)
{
	for (int i = 0; i < 2 * MAX_MAT; ++i)
	{
		A[a][i] = (A[a][i] * k) % mod;
	}
	return;
}
void row_swap(int a, int b)
{
	for (int i = 0; i < 2 * MAX_MAT; ++i)
	{
		swap(A[a][i], A[b][i]);
	}
}
Mat getinv(Mat x)
{
	memset(A, 0, sizeof(A));
	for (int i = 0; i < MAX_MAT; ++i)
	{
		for (int j = 0; j < MAX_MAT; ++j)
		{
			A[i][j] = x.a[i][j];
			A[i][MAX_MAT + j] = i == j;
		}
	}
	for (int i = 0; i < MAX_MAT; ++i)
	{
		if (!A[i][i])
		{
			for (int j = i + 1; j < MAX_MAT; ++j)
			{
				if (A[j][i])
				{
					row_swap(i, j);
					break;
				}
			}
		}
		row_multiplies(i, get_inv(A[i][i]));
		for (int j = i + 1; j < MAX_MAT; ++j)
		{
			row_minus(j, i, A[j][i]);
		}
	}
	for (int i = MAX_MAT - 1; i >= 0; --i)
	{
		for (int j = i - 1; j >= 0; --j)
		{
			row_minus(j, i, A[j][i]);
		}
	}
	Mat ret;
	for (int i = 0; i < MAX_MAT; ++i)
	{
		for (int j = 0; j < MAX_MAT; ++j)
		{
			ret.a[i][j] = A[i][MAX_MAT + j];
		}
	}
	return ret;
}
const int MAXN = 100005;
const Mat tA(1, 1, 0, 1);
const Mat tB(1, 0, 1, 1);
Mat operator * (Mat x, Mat y)
{
	Mat c;
	for (int i = 0; i < MAX_MAT; ++i) {
		for (int j = 0; j < MAX_MAT; ++j) {
			c.a[i][j] = 0;
		}
	}
	for (int i = 0; i < MAX_MAT; ++i) {
		for (int j = 0; j < MAX_MAT; ++j) {
			for (int k = 0; k < MAX_MAT; ++k) {
				c.a[i][j] = (c.a[i][j] + x.a[i][k] * y.a[k][j] % mod) % mod;
			}
		}
	}
	return c;
}
Mat presum[MAXN];
char s[MAXN];
int n, m, hs, ht, ps, pt;
int main()
{
	scanf("%d %d", &n, &m);
	scanf("%s", s + 1);
	presum[0] = Mat(1, 0, 0, 1);
	for (int i = 1; i < n; ++i)
	{
		if (s[i] == '/')
		{
			presum[i] = presum[i - 1] * tA;
		}
		else
		{
			presum[i] = presum[i - 1] * tB;
		}
	}
	while (m--)
	{
		scanf("%d %d %d %d", &hs, &ht, &ps, &pt);
		Mat ans = getinv(presum[hs - 1]) * presum[ht - 1];
		printf("%lld\n", ans.a[ps][pt]);
	}
	return 0;
}

多階差分與多階字首和的引入

如果需要對區間進多次行加減操作,暴力跑肯定就不行,就需要引入差分來解決

//原陣列
1 1 1 1 1 1 
//差分後
1 0 0 0 0 0 -1

如果需要對區間[l, r]+x,那就對差分陣列的cha[l] += x, cha[r + 1] -= x即可,當所有的都修改完了,就可以通過一次字首和獲得修改後的陣列,屬實很方便

但是如果我要插入的是1 2 3 4 5 6...... 這樣的呢

//原陣列
1 2 3 4 5 6 ...
//一次差分後
1 1 1 1 1 1 ...
//兩次差分
1 0 0 0 0 0 ...

如果要插的是1 4 9 16 25 36……的呢

//原陣列
1 4 9 16 25 36 ...
//一次差分
1 3 5 7 9 11 ...
//二次差分
1 2 2 2 2 2 ...
//三次差分
1 1 0 0 0 0...

是不是感覺有點東西了

看個例題:

H小w的糖果

題目描述:

三種操作

  • 從pos位置開始往後,每個人發一個糖
  • 從pos往後,每個人發前一個人數量+1的糖,第一個人發1個
  • 從pos往後,按1 4 9 16……這樣來發

思路:

三次差分,手推出來三種操作對第三次差分陣列的影視是什麼,然後去修改即可,最後求三次字首和,輸出即可

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%d",&n)
#define sdd(n,m) scanf("%d %d",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int t;
int n, m, op, l;
ll sum[MAX];

inline void presum(){
    for(int i = 1; i <= n; ++i){
        sum[i] += sum[i - 1];
        if(sum[i] > mod)sum[i] %= mod;
    }
}

int main(){
    sd(t);
    while (t--) {
        mem(sum, 0);
        sdd(n, m);
        for(int i = 1; i <= m; ++i){
            sdd(op, l);
            if(op == 1){
                ++sum[l];
                sum[l + 1] -= 2;
                ++sum[l + 2];
            }
            else if(op == 2){
                ++sum[l];
                --sum[l + 1];
            }
            else {
                ++sum[l];
                ++sum[l + 1];
            }
        }
        presum();
        presum();
        presum();
        for(int i = 1; i <= n; ++i)printf("%lld ", sum[i]);
        cout<<endl;
    }
    return 0;
}

NOIP2013積木大賽 NOIP2018道路鋪設

題目描述:

這兩個題不能說是毫無差別,只能說是一模一樣

大概意思就是給你n個數,每次你可以選擇一個區間使得區間的數-1,但不能減為負數,問所有數都變成0最少需要幾次操作

思路:

這個題就直接貪心?

就直接求一次差分陣列,跑一遍看,將所有的大於0的都加起來就是答案

為什麼呢?因為如果差分陣列tr[i] 大於0,說明前面一個一定比他小,我們就能把他們倆繫結起來一起削掉小的那部分,剩下的大的那部分 tr[i] 就得自己削,就會產生額外的操作次數tr[i]

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%d",&n)
#define sdd(n,m) scanf("%d %d",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n, ans;
int tr[MAX];

int main(){
    sd(n);
    for(int i = 1; i <= n; ++i){
        sd(tr[i]);
        int d = tr[i] - tr[i - 1];
        if(d >= 1)ans += d;
    }
    cout<<ans<<endl;
    return 0;
}

題目描述:

給你一個01串,每一對(u, v)(滿足s[u] = s[v] = 1,且v > u)會產生v - u + 1的價值,問你總價值是多少

思路:

這個題挺有意思的,先找個例子寫出來就能發現一個好玩的東西,就是1對後面產生的價值是1 2 3 4……這樣的,而這個東西我們剛剛講了,可以用兩次差分來解決的。

所以我們需要先把所有1的位置往後移一位,因為從1的後一位開始產生貢獻

然後兩次字首和,再統計ans即可

1 0 0 0 1 0 0 1 0 1
  1 2 3 4 5 6 7 8 9  
          1 2 3 4 5
                1 2
#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%d",&n)
#define sdd(n,m) scanf("%d %d",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n;
string s;
ll tr[MAX];

int main(){
    sd(n);
    cin>>s;
    for(int i = 0; i < s.size(); ++i){
        if(s[i] == '1')tr[i + 1] = 1;
    }
    for(int i = 1; i <= n; ++i)tr[i] += tr[i - 1];
    for(int i = 1; i <= n; ++i)tr[i] += tr[i - 1];
    ll ans = 0;
    for(int i = 0; i < s.size(); ++i){
        if(s[i] == '1')ans += tr[i];
        ans %= mod;
    }
    
    cout<<ans<<endl;
    return 0;
}

上面一個是1 1 1 1…… ,一個是1 2 3 4 ……,一個是1 4 9 16……,這些其實沒一個值都是由同一個多項式函式f(x)由不同的x求出來,第一個1 1 1 1是f(x) = 1;第二個1 2 3 4 是f(x) = x;第三個1 4 9 16是f(x) = x2,這樣我們就發現了一個規律:假設f(x)中x的最高次數為p,那最少做p+1次差分就可以將任意長的區間修改變成一個p+1次的單點修改(是從 l 往後都修改,不存在 r 的情況),這就很有用了,我們來看一個例題:

D智乃醬的靜態陣列維護問題多項式

題目描述:

n個數,m次操作,每次會給一個k次多項式:f(x)=c0xk+c1xk−1+...+ck−1x1+ck,以及一個區間[l,r],然後你需要對al + f(1),a2 + f(2)……依次類推,m次修改完了以後,有q次詢問,每一次詢問都是問[l, r]的區間元素和,答案對1e9+7取模

思路:

這個題看起來是不是就很狗了,沒錯,就是和狗

根據題目的範圍k<=5,也就是說多項式最多時5次多項式,那我們最多就需要做6次差分,就能通過6次單點修改來代替

對於區間[l, r],我們對於l是6次單點修改,那對於r也是6次單點修改,且r對應的那6個應該是負的,因為是要消除 l 對 r 後面產生的影響

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  100000 + 50
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%lld",&n)
#define sdd(n,m) scanf("%lld %lld",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n, m, q;
ll k, l, r;
ll tr[MAX];
ll cr[MAX];
ll ar[MAX], br[MAX];

inline void presum(ll tr[], ll len, ll k){
    while (k--) {
        for(int i = 1; i <= len; ++i){
            tr[i] += tr[i - 1];
            if(tr[i] >= mod)tr[i] %= mod;
        }
    }
}

inline void diff(ll tr[], ll len, ll k){
    while (k--) {
        for(ll i = len; i >= 1; --i){
            tr[i] -= tr[i - 1];
            while(tr[i] < 0)tr[i] += mod;
        }
    }
}

ll calc(ll x, ll cr[], ll k){
    ll ans = 0;
    ll cnt = 1;
    for(ll i = k; i >= 0; --i){
        ans += cr[i] * cnt % mod;
        if(ans >= mod)ans %= mod;
        cnt = cnt * x % mod;
    }
    return ans;
}

ll incalc(ll x, ll cr[], ll l, ll r, ll k){
    return (mod - calc(x + r - l + 1, cr, k)) % mod;
}

int main(){
    sddd(n, m, q);
    for(int i = 1; i <= n; ++i)sd(tr[i]);
    diff(tr, n, 6);
    while(m--){
        cin>>l>>r>>k;
        for(int i = 0; i <= k; ++i)sd(cr[i]);
        for(int i = 1; i <= 6; ++i){
            ar[i] = calc(i, cr, k);
            br[i] = incalc(i, cr, l, r, k);
        }

        diff(ar, 6, 6);
        diff(br, 6, 6);
        for(int i = 1; i <= 6; ++i){
            tr[l + i - 1] += ar[i];
            while(tr[l + i - 1] >= mod)tr[l + i - 1] -= mod;
            tr[r + i] += br[i];
            while(tr[r + i] >= mod)tr[r + i] -= mod;
        }
    }
    presum(tr, n, 7);
    while (q--) {
        sdd(l, r);
        cout<<((tr[r] - tr[l - 1]) % mod + mod) % mod<<endl;
    }
    return 0;
}

這才求6次差分-字首和,還是不夠屌,所以來個1e18的看看?

智乃醬的字首和與差分

題目描述:

給你一個n個數的陣列a,一個k

  • k > 0 對a做k次字首和,並輸出
  • k = 0 直接輸出
  • k < 0 對a做k次差分,並輸出

輸出的數可能很大,要模998244353

−1018k≤1018

思路:

這個題確實夠噁心的,1e18次字首和或1e18次的差分,根本沒法求

不過我們觀察一下多次字首和是什麼東西

1 1 1 1 1 1 1 
1 2 3 4 5 6 7 
1 3 6 10 15 21 28
1 4 10 20 35 56 84 
1 5 15 35 70 126 210
1 6 21 56 126 252 462
1 7 28 84 210 462 924 

顯然啊,這是組合數對於第k行,其實就是\(C_{k}^{0}~~~~C_{k+1}^{1}~~~~C_{k+2}^{2}……\)

這就可以用卷積來計算,嗯我不會,自己看程式碼吧

還有一個比較神奇的規律是取模數大於陣列長度時,做k次字首和存在迴圈節,長度就是mod,就可以根據這個性質來解決問題,其他的自己看程式碼吧,等我學了卷積再回來更

#include <cmath>
#include <cstring>
#include <algorithm>
#include <map>
#include <list>
#include <queue>
#include <vector>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <deque>
using namespace std;
namespace NTT
{
const long long g = 3;
const long long p = 998244353;
long long wn[35];
long long pow2(long long a, long long b)
{
    long long res = 1;
    while (b)
    {
        if (b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}
void getwn()
{
    for (int i = 0; i < 25; i++) wn[i] = pow2(g, (p - 1) / (1LL << i));
}
void ntt(long long *a, int len, int f)
{
    long long i, j = 0, t, k, w, id;
    for (i = 1; i < len - 1; i++)
    {
        for (t = len; j ^= t >>= 1, ~j & t;);
        if (i < j) swap(a[i], a[j]);
    }
    for (i = 1, id = 1; i < len; i <<= 1, id++)
    {
        t = i << 1;
        for (j = 0; j < len; j += t)
        {
            for (k = 0, w = 1; k < i; k++, w = w * wn[id] % p)
            {
                long long x = a[j + k], y = w * a[j + k + i] % p;
                a[j + k] = (x + y) % p;
                a[j + k + i] = (x - y + p) % p;
            }
        }
    }
    if (f)
    {
        for (i = 1, j = len - 1; i < j; i++, j--) swap(a[i], a[j]);
        long long inv = pow2(len, p - 2);
        for (i = 0; i < len; i++) a[i] = a[i] * inv % p;
    }
}
void mul(long long *a, long long *b, int l1, int l2)
{
    int len, i;
    for (len = 1; len <= l1 + l2; len <<= 1);
    for (i = l1 + 1; i <= len; i++) a[i] = 0;
    for (i = l2 + 1; i <= len; i++) b[i] = 0;
    ntt(a, len, 0); ntt(b, len, 0);
    for (i = 0; i < len; i++) a[i] = a[i] * b[i] % p;
    ntt(a, len, 1);
}
};
typedef long long ll;
#define _for(i,a,b) for(int i=(a) ;i<=(b) ;i++)
#define _rep(i,a,b) for(int i=(a) ;i>=(b) ;i--)
#define scd(v) scanf("%d",&v)
#define scdd(a,b) scanf("%d %d",&a,&b)
#define endl "\n"
#define IOS ios::sync_with_stdio(false)
#define pb push_back
#define all(v) v.begin(),v.end()
#define int long long
#define odd(x) x&1
#define mst(v,a) memset(v,a,sizeof(v))
#define lson p<<1 ,l,mid
#define rson p<<1|1,mid+1,r
#define ls p<<1
#define rs p<<1|1
#define fi first
#define se second
#define pii pair<double,double>
#define inf 0x7f7f7f7f
const int N=3e5+10;
const int mod=998244353;
int n,m,k;
int a[N];
int ni[N],ki[N];
void get_ni(int n)//O(n)擴充歐幾求逆元
{
    ni[0] = ni[1]=1;//0和1逆元
    _for(i,2,n)
    {
        ni[i] = ((mod-mod/i)*ni[mod%i])%mod;
    }
    return;
}
void gei_ki(int k ,int len)//求組合數C(k,0) C(k+1,1) C(k+2,2)……
{
    k = (k%mod+mod)%mod;//某字首和定理,mod>len時,做k次字首和存在迴圈節
    ki[0]=1;
    _for(i,1,len-1)
    {
        ki[i] = ki[i-1] * ni[i]%mod * ((k-1+i)%mod )%mod;
    }
}
signed main()
{
    //!!//
//    freopen("data.txt","r",stdin);
    //!!
    IOS;
    NTT::getwn();
    get_ni(100000);
    
    cin>>n>>k;
    gei_ki(k,n);
    _for(i,0,n-1) cin>>a[i];//注意從第0項開始讀入
    NTT::mul(a,ki,n,n);//ntt加速求a與係數ki的卷積
    _for(i,0,n-1)
    {
        cout<<a[i]<<" ";
    }
}

多維字首和

相信大家對二維字首和肯定不陌生

//dp[i][j]表示從(1,1)到(i,j)形成的矩形的元素和
dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1] + tr[i][j]

這是利用簡單的容斥原理實現的,但要是推廣到三維、四維……n維,那就會麻煩的要死,容斥的項就有2n個,沒法搞

但是還有另一種求二維字首和的操作:

for(int i = 1; i <= n; ++i)
  for(int j = 1; j <= n; ++j)
    sum[i][j] += sum[i - 1][j];
for(int i = 1; i <= n; ++i)
  for(int j = 1; j <= n; ++j)
    sum[i][j] += sum[i][j - 1];

這是一種不利用容斥原理來實現的二維字首和,主要思想是先一列列求,求完以後再一行行求

這種方法特別適合推廣到多維字首和,一維維的求,最後就得到字首和

看個例子:

B智乃醬的子集與超集

題目描述:

n個物品,第i個物品的權值為ai,對於一個物品集合U = {ap1,ap2,ap3……apk},我們定義這個集合U的價值為ap1⊕ap2⊕ap3⊕……⊕apk。即定義一個集合的價值為:該集合所擁有的物品權值之異或和

現在有m次詢問,每次詢問的時候會給你一個物品集合U,想知道這個U的所有子集的價值之和,以及所有超集的價值之和

思路:

乍一看是不是感覺和字首和毛關係都沒有,確實,這就是個狀壓dp,不過這裡確實有字首和的思想在裡面,你可以把他看成n維的字首和,n<=20,顯然不能開一個20維的陣列,那我們就可以用一個n位的二進位制數表示出所有集合的情況,也就是狀態壓縮,其中每個數的每一位,0就代表這個位置的物品不選,1就代表選,就這樣可以預處理出來所有集合的價值,相當於字首和開始之前最開始的陣列值。

然後就進行n次字首和,這就是上面講的一維維的去求和的過程,如果當前集合 j 的第 i 維是1,那就說明它有一個第 i 位是0的,其他位和j一樣的集合,這就是是它的子集,會產生貢獻,就加起來就行。如果當前集合 j 的第 i 維是0,就說明它有一個第 i 位是1的,其他位都是和j一樣的集合,這就是它的超集,會對它產生貢獻,加起來就行。

詢問的時候就先造出來這個需要的數,然後就輸出即可

#include<map>
#include<set>
#include<stack>
#include<queue>
#include<cmath>
#include<bitset>
#include<cstdio>
#include<string>
#include<vector>
#include<sstream>
#include<cstring>
#include<stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;

#define eps 1e-8
#define endl '\n'
#define inf 0x3f3f3f3f
#define MAX  1048576
#define mod 1000000007
#define lowbit(x) (x & (-x))
#define sd(n) scanf("%lld",&n)
#define sdd(n,m) scanf("%d %d",&n,&m)
#define pd(n) printf("%d\n", (n))
#define pdd(n,m) printf("%d %d\n",n, m)
#define sddd(n,m,z) scanf("%d %d %d",&n,&m,&z)
#define io ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define mem(a,b) memset((a),(b),sizeof(a))
#define max(a,b) (((a)>(b)) ? (a):(b))
#define min(a,b) (((a)>(b)) ? (b):(a))

typedef  long long ll ;
typedef unsigned long long ull;
//不開longlong見祖宗!不看範圍見祖宗!
inline int IntRead(){char ch = getchar();int s = 0, w = 1;while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0';ch = getchar();}return s * w;}

int n, m;
int maxbit;
ll k, x;
ll tr[MAX];
ll pre[MAX];
ll suf[MAX];


int main(){
    sdd(n, m);
    maxbit = 1 << n;
    for(int i = 0; i < n; ++i)sd(tr[i]);
    for(int i = 0; i < maxbit; ++i){
        ll sum = 0;
        for(int j = 0; j < n; ++j){
            if(i & (1 << j))sum ^= tr[j];
        }
        pre[i] = suf[i] = sum;
    }
    for(int i = 0; i < n; ++i){
        for(int j = 0; j < maxbit; ++j){
            if(j & (1 << i))pre[j] += pre[j ^ (1 << i)];
            else suf[j] += suf[j ^ (1 << i)];
        }
    }
    while (m--) {
        sd(k);
        ll q = 0;
        for(int i = 0; i < k; ++i){
            sd(x);
            q |= (1 << (x - 1));
        }
        cout<<pre[q]<<' '<<suf[q]<<endl;
    }
    return 0;
}

思維導圖(bushi

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-YQbnEey0-1630764315730)(/Users/chelsea/Library/Application Support/typora-user-images/image-20210903184203454.png)]

相關文章