【题解】CF2600DP 选练(23.9.5-23.9.6)
低情商:感觉是比较套路的
高情商:十分 educational!!!
CF258D Little Elephant and Broken Sorting
题目描述:
有一个 \([1,n]\) 的排列 \(a\),会进行 \(m\) 次操作,操作为交换 \((a_i,a_j)\)。每次操作都有 \(50\%\) 的概率进行。
求进行 \(m\) 次操作以后的期望逆序对个数。
\(n,m \le 1000\)
题目分析:
期望逆序对数一个经典的做法就是对于每一个数对 \((x,y)\) 统计其为逆序对的概率,这里为了不重不漏不妨假设 \(x < y\)。
一个显然的想法就是求出 \(f[i][j]\) 表示数 \(i\) 最后在 \(j\) 位置的概率,然后对应相乘就是答案,但是它们的概率并不独立,所以只能放在一起处理。
所以就是考虑对于每一个 \((x,y)\) 求出 \(f[i][j]\) 表示 \(x\) 在 \(i\) 且 \(y\) 在 \(j\) 的概率,转移显然,所以一次判断的复杂度就是 \(O(nm)\)。
这样总共有 \(O(n^2)\) 个数对,所以直接做复杂度就是 \(O(n^3m)\),显然炸了。
但是我们观察到一点,对于不同的数对 \((x,y)\) 其只会影响 \(dp\) 的初值,而不会对转移产生影响,所以显然可以将所有的数对放到一起 \(dp\),也就是此时我们的状态就变成了 \(f[i][j]\) 表示 \(x\) 在 \(i\) 且 \(y\) 在 \(j\) 的期望数对数。
则答案显然就是 \(\sum_{i > j} f[i][j]\)。
复杂度 \(O(n^2 + nm)\)。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int n,x[N],y[N],a[N];
double sum[N];
double f[N][N],g[N][N];
set<int> st1[N],st2[N],tmp1[N],tmp2[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
}
for(int i=1; i<=m; i++) scanf("%d%d",&x[i],&y[i]);
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
if(a[i] < a[j]) f[i][j] ++;
}
}
for(int i=1; i<=m; i++){
for(int k=1; k<=n; k++){
if(k == x[i] || k == y[i]) continue;
double tmp = (f[x[i]][k] + f[y[i]][k]) / 2.0;
f[x[i]][k] = f[y[i]][k] = tmp;
tmp = (f[k][x[i]] + f[k][y[i]]) / 2.0;
f[k][x[i]] = f[k][y[i]] = tmp;
}
double tmp = (f[x[i]][y[i]] + f[y[i]][x[i]]) / 2.0;
f[x[i]][y[i]] = f[y[i]][x[i]] = tmp;
}
double ans = 0;
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
if(i > j) ans += f[i][j];
}
}
printf("%.7f\n",ans);
return 0;
}
CF1185G2 Playlist for Polycarp (hard version)
题目描述:
你现在正在选择歌曲。现在共有 \(n\) 首歌,每首歌有一个时长 \(t_i\) 和一个编号 \(g_i\) 。
您需要求出有多少种选出若干首歌排成一排的方案,使得任意相邻两首歌的编号不同,且所有歌的时长和恰好为 \(T\),答案对 \(10^9 + 7\) 取模。
\(1\le t_i\le 50,1\le g_i\le 3,1\le n\le50,1 \le T \le 2500\)
题目分析:
如果直接 \(dp\) 排列的话,会发现因为我们做不到插入一个数后快速判断产生的贡献,所以大概不可做。
所以可以将这个题拆成两个部分:选择某些歌使得它们的时长为 \(T\) 的方案数,将某种选择的方案排成两两编号不同的方案数。
第一个部分看上去就是一个简单背包,所以先不管,但是我们还要完成第二个部分,所以考虑我们应该记什么东西。
显然应该记录 \((x,y,z)\) 表示选择了 \(x\) 个编号为 \(1\) 的歌,\(y\) 个编号为 \(2\) 的歌,\(z\) 个编号为 \(3\) 的歌。
那么此时我们就知道了要求什么了:
第一部分就是:\(f[i][j][k][p]\) 表示时长之和为 \(p\),选择了 \(i\) 个编号为 \(1\) 的歌,\(j\) 个编号为 \(2\) 的歌,\(z\) 个编号为 \(3\) 的歌的方案数。
第二部分就是:\(dp[i][j][k]\) 表示 \(i\) 个编号为 \(1\) 的歌,\(j\) 个编号为 \(2\) 的歌,\(z\) 个编号为 \(3\) 的歌,将它们排成任意相邻两首歌编号不同的方案数。
对于第二部分可以考虑记 \(g[i][j][k][1/2/3]\) 表示 \(i\) 个编号为 \(1\) 的歌,\(j\) 个编号为 \(2\) 的歌,\(z\) 个编号为 \(3\) 的歌,最后一首歌的编号为 \(1/2/3\) 的方案数,这样的话就很好转移了,而显然 \(dp[i][j][k] = \sum_{p=1}^3 g[i][j][k][p]\),这一部分的复杂度就是 \(O(n^3T)\)。
对于第一部分,如果直接做这个 \(dp\) 的话复杂度就是 \(O(n^4T)\),但是有一个奇妙的思路就是,我们可以将编号不同的这三种歌分别做背包,然后最后再背包合并,这样的话复杂度就变成了 \(O(n^3T)\),我的代码的实现是直接将编号为 \(2,3\) 的合并到一起做背包,然后和编号为 \(1\) 的合并,复杂度也一样。
这样的总复杂度就是 \(O(n^3T)\)。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
int fac[55],inv[55],dp[55][55][55],g2[55][55][55][4];
int f1[2505][55],f2[2505][55][55],f[55][55][55];
int cnt[4],a[55],g[55];
int binom(int n,int m){
if(n < 0 || m < 0 || n < m) return 0;
return fac[n] * inv[m] % MOD * inv[n-m] % MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
signed main(){
int n,T;scanf("%lld%lld",&n,&T);
for(int i=1; i<=n; i++){
scanf("%lld%lld",&a[i],&g[i]);
cnt[g[i]]++;
}
fac[0] = 1;
for(int i=1; i<=n+2; i++) fac[i] = fac[i-1] * i % MOD;
inv[n+2] = power(fac[n+2],MOD-2);
for(int i=n+1; i>=0; i--) inv[i] = inv[i+1] * (i + 1) % MOD;
f1[0][0] = 1;
for(int p=1; p<=n; p++){
for(int i=cnt[1]; i>=0; i--){
for(int t=T-a[p]; t>=0; t--){
if(g[p] == 1 && i != cnt[1]) f1[t+a[p]][i+1] = (f1[t+a[p]][i+1] + f1[t][i])%MOD;
}
}
}
f2[0][0][0] = 1;
for(int p=1; p<=n; p++){
for(int j=cnt[2]; j>=0; j--){
for(int k=cnt[3]; k>=0; k--){
for(int t=T-a[p]; t>=0; t--){
if(g[p] == 2 && j != cnt[2]) f2[t+a[p]][j+1][k] = (f2[t+a[p]][j+1][k] + f2[t][j][k])%MOD;
if(g[p] == 3 && k != cnt[3]) f2[t+a[p]][j][k+1] = (f2[t+a[p]][j][k+1] + f2[t][j][k])%MOD;
}
}
}
}
for(int i=0; i<=cnt[1]; i++){
for(int j=0; j<=cnt[2]; j++){
for(int k=0; k<=cnt[3]; k++){
for(int t=0; t<=T; t++){
f[i][j][k] = (f[i][j][k] + f1[t][i] * f2[T-t][j][k])%MOD;
}
}
}
}
int ans = 0;
g2[1][0][0][1] = g2[0][1][0][2] = g2[0][0][1][3] = 1;
for(int i=0; i<=cnt[1]; i++){
for(int j=0; j<=cnt[2]; j++){
for(int k=0; k<=cnt[3]; k++){
if(i == 0 && j == 0 && k == 0) continue;
g2[i+1][j][k][1] = (g2[i][j][k][2] + g2[i][j][k][3])%MOD;
g2[i][j+1][k][2] = (g2[i][j][k][1] + g2[i][j][k][3])%MOD;
g2[i][j][k+1][3] = (g2[i][j][k][1] + g2[i][j][k][2])%MOD;
dp[i][j][k] = (g2[i][j][k][1] + g2[i][j][k][2] + g2[i][j][k][3]) % MOD;
dp[i][j][k] = dp[i][j][k] * fac[i] % MOD * fac[j] %MOD* fac[k] %MOD;
ans = (ans + dp[i][j][k] * f[i][j][k] %MOD) %MOD;
}
}
}
printf("%lld\n",ans);
return 0;
}
CF1223F Stack Exterminable Arrays
题目描述:
给一个序列进行栈操作,从左到右入栈,若当前入栈元素等于栈顶元素则栈顶元素出栈,否则当前元素入栈。若进行完操作后栈为空,这说这个序列是可以被消除的。
给你一个长度为 \(n\) 的序列 \(a\),问 \(a\) 有多少子串是可以被消除的。
\(1 \le n \le 3\times 10^5,1\le a_i \le n\)
题目分析:
统计子串一个显然的想法就是枚举右端点然后查找有多少个符合条件的左端点,设右端点为 \(r\)。
设 \(pre_r\) 表示距离 \(r\) 最近且满足 \([pre_r,r]\) 为可消除的序列的位置,\(dp_r\) 表示以 \(r\) 为右端点有多少个合法的左端点,则 \(dp_r = dp_{pre_r-1}+1\)。
其实也就是可以理解为 \(r\) 找到匹配 \(r\) 的位置 \(pre_r\),这样左端点 \(l \in (pre_r+1,r]\) 就一定不合法,只需要判断前面的即可。
下面的问题就转化为了怎么快速求解 \(pre_r\),\(pre_r\) 其实就是说在 \(r-1\) 的某个区间的基础上加入 \(a_r\) 之后合法,这个区间的左端点就是 \(pre_r\)。
因此我们可以从左到右枚举 \(i\),过程中维护 \(S_i\),其中 \(S_{i,j} = 0\) 代表以 \(i\) 为右端点的区间后面加入数 \(j\) 不存在合法区间,若 \(S_{i,j} = x\) 代表以 \(i\) 为右端点的区间后面加入数 \(j\) 的合法区间中最靠右的左端点为 \(x\)。
考虑加入 \(a_r\),若 \(S_{r-1,a_r} = pos\) 且 \(pos \not= 0\),那么因为 \([pos,r]\) 这一段已经被这个匹配完全覆盖了,所以就没有用了,即 \(S_r = S_{pos-1}\)。
记得每次枚举完了 \(r\) 都要让 \(S_{r,a_r} = r\),这个式子显然成立。
上述过程中我们只关心 \(S_{i,j} \not= 0\) 的位置,为了节省复杂度显然可以使用 map
,每次对于 \(S_r = S_{pos-1}\),那么 \(S_{pos-1}\) 在以后就永远也不会用到了,所以可以直接清空。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5+5;
int a[N],lst[N],dp[N],pre[N];
bool flag[N];
map<int,int> mp[N];
signed main(){
int T;scanf("%lld",&T);
while(T--){
int n;scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=1; i<=n; i++){
pre[i] = 0;
if(mp[i-1][a[i]]){
int pos = mp[i-1][a[i]];
pre[i] = pos;
swap(mp[i],mp[pos-1]); //注意直接 swap 是 O(1) 的,否则复杂度就炸了
// mp[i] = mp[pos-1];mp[pos-1].clear();
}
mp[i][a[i]] = i;
}
int ans = 0;
for(int r=1; r<=n; r++){
if(!pre[r]) continue;
dp[r] = dp[pre[r] - 1] + 1;
ans += dp[r];
}
printf("%lld\n",ans);
for(int i=1; i<=n; i++){
dp[i] = 0;pre[i] = 0;mp[i].clear();
}
}
return 0;
}
CF382E Ksenia and Combinatorics
题目描述:
有一棵 \(n\) 个节点的树,标号从 \(1\) 到 \(n\)。
除了 \(1\) 号节点至多与另外 \(2\) 个节点连边,其余至多与另外 \(3\) 个节点连边。
两个树是相同的,当且仅每个节点的相连节点都相同。
求有多少种不同的这样的树,满足最大匹配为 \(k\),答案对 \(1e9+7\) 取膜。
\(1 \le n,k \le 50\)
题目分析:
对于最大匹配数显然一个贪心就是:从深度大的点考虑到深度小的点,如果该点与其父亲都没有匹配,则将这两个点。
注意到题目相当于直接钦定了根节点为 \(1\),所以我们也不妨直接当作有根树来做,最后将得到的答案除以 \(n\) 就是题目要求的方案数。
所以一个显然的想法就是设 \(f[i][j][0/1]\) 表示含有 \(i\) 个节点的树,最大匹配数为 \(j\),根节点没有/有匹配的方案数。
每次转移就是枚举一个子树的信息,另一个子树可以直接算出来,设左子树大小为 \(l_n\),右子树大小为 \(r_n\),转移时要乘上系数:\(\binom{l_n+r_n}{l_n} \times (l_n+r_n+1)\)。
这个系数的意思就是选 \(l_n\) 个点给左子树,\(l_n+r_n+1\) 这些点都可以作为根,因为我们只需要求 \(f[n]\) 的答案所以不需要乘 \(\binom{n}{i}\) 这个系数。
以及为了不重不漏我们可以钦定:\(l_n \le r_n\),且当 \(l_n = r_n\) 时要将答案除以 \(2\)。
代码的话就是因为我们的转移顺序不好确定,所以可以考虑记忆化搜索。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 55;
const int MOD = 1e9+7;
int f[N][N][2],vis[N][N][2],fac[N],inv[N];
//f[i][j][0/1] 表示大小为 i,匹配数量为 j,钦定的根节点选/不选
int binom(int n,int m){
if(n < m || n < 0 || m < 0) return 0;
return fac[n] * inv[m] %MOD* inv[n-m]%MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int solve(int n,int k,int p){
if(vis[n][k][p]) return f[n][k][p];
if(n == 0 && k == 0 && p == 1) return 1; //如果不选的方案为 1 则会导致匹配了一个不存在的点
else if(n == 0) return 0;
if(n == 1 && k == 0 && p == 0) return 1;
else if(n == 1) return 0;
for(int l_n=0; l_n<n; l_n++){
int r_n = n - l_n - 1;
if(r_n < l_n) continue;
for(int l_k=0; l_k<=k; l_k++){
for(int l_p=0; l_p<=1; l_p++){
for(int r_p=0; r_p<=1; r_p++){
if((!l_p || !r_p) && p){
int r_k = k - l_k - 1;
int tmp = solve(l_n,l_k,l_p) * solve(r_n,r_k,r_p) %MOD* binom(l_n+r_n,l_n) %MOD* (l_n + r_n + 1)%MOD;
if(l_n == r_n) tmp = tmp * power(2,MOD-2) % MOD;
f[n][k][p] = (f[n][k][p] + tmp)%MOD;
}
else if(l_p && r_p && !p){
int r_k = k - l_k;
int tmp = solve(l_n,l_k,l_p) * solve(r_n,r_k,r_p) %MOD* binom(l_n+r_n,l_n) %MOD* (l_n + r_n + 1)%MOD;
if(l_n == r_n) tmp = tmp * power(2,MOD-2)%MOD;
f[n][k][p] = (f[n][k][p] + tmp)%MOD;
}
}
}
}
}
vis[n][k][p] = true;
return f[n][k][p];
}
signed main(){
int n,k;scanf("%lld%lld",&n,&k);
fac[0] = 1;
for(int i=1; i<=n; i++) fac[i] = fac[i-1] * i % MOD;
inv[n] = power(fac[n],MOD-2);
for(int i=n-1; i>=0; i--) inv[i] = inv[i+1] * (i+1) % MOD;
printf("%lld\n",(solve(n,k,0) + solve(n,k,1) % MOD) * power(n,MOD-2) % MOD);
return 0;
}
CF1699E Three Days Grace
题目描述:
给定一个初始有 \(n\) 个元素的可重复集合 \(A\),其中每个元素都在 \(1\) 到 \(m\) 之间。
每次操作可以将 \(A\) 中的一个元素(称之为 \(x\))从 \(A\) 中删除,然后在 \(A\) 中加入两个元素 \(p,q\),满足 \(p\cdot q=x\) 且 \(p,q>1\)。
显然每次操作后 \(A\) 的大小会增加 \(1\)。
定义 \(A\) 的平衡值为 \(A\) 中的最大值减去最小值,求任意次操作(可以是 \(0\) 次)后最小可能的平衡值。
\(1 \le n \le 10^6,1 \le m \le 5 \times 10^6\)
题目分析:
既然是极差最小,一个显然的想法就是枚举最小值,然后就是要让每个数全部分解为大于等于最小值的数,且最大值最小。
这东西虽然很想直接贪心,但是好像没啥很好的策略,所以就考虑 \(dp\)。
也就是设 \(dp[i][j]\) 表示将数 \(i\) 分解为大于等于 \(j\) 的数,最大值最小是多少。
转移的话显然就是分类讨论一下:
- 若 \(j \nmid i\),则 \(dp[i][j+1] \to dp[i][j]\)
- 若 \(j \mid i\),则 \(dp[\frac{i}{j}][j] \to dp[i][j]\)
所以我们就可以考虑从大到小枚举最小值,这样第一个转移就可以直接被忽略了,而对于第二个转移则显然是一个调和级数级别的,就完全可过。
要注意的细节就是对于第二个转移因为我们要求分解出的数全部大于等于 \(j\) 所以 \(\frac{i}{j} \ge j \to i \ge j^2\)。
还可以发现一个性质就是对于最小值减小,最大值单调递减,因此可以使用双指针维护。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
const int INF = 1e9+5;
int a[N],tot[N],dp[N];
bool flag[N];
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
int n,m;scanf("%lld%lld",&n,&m);
int ans = INF,mn = INF;
for(int i=1; i<=n; i++){
scanf("%lld",&a[i]);mn = min(mn,a[i]);
flag[a[i]] = true;
}
for(int i=1; i<=m; i++){
dp[i] = i;
if(flag[i]) tot[dp[i]]++;
}
int mx = m;
for(int i=m; i>=1; i--){
for(int j=i*i; j<=m; j+=i){
if(flag[j]) tot[dp[j]]--;
dp[j] = min(dp[j],dp[j/i]);
if(flag[j]) tot[dp[j]]++;
}
while(!tot[mx]) --mx;
if(i <= mn) ans = min(ans,mx - i);
}
printf("%lld\n",ans);
for(int i=1; i<=n; i++) flag[a[i]] = false;
for(int i=1; i<=m; i++) tot[i] = 0;
}
return 0;
}
CF1700F Puzzle
题目描述:
给定两个 \(2 \times n\) 的 \(01\) 矩阵 \(A\) 和 \(B\),定义一次操作为交换 \(A\) 中任意两个相邻的位置中的值,输出使得 \(A=B\) 的最小操作次数,如果无法使 \(A=B\) 则输出 \(-1\)。
\(1 \le n \le 2\times 10^5\)
题目分析:
如果只有一行的话这个题就是一个很典的题,因为序列元素均为 \(01\) 所以可以对于每一个位置 \(x\),找到 \((x,x+1)\) 交换的次数,注意到我们交换 \(A\) 与交换 \(B\) 其实本质相同,所以为了方便理解我们认为都可以交换,设 \(A\) 的前缀和为 \(a\),\(B\) 的前缀和为 \(b\),则 \((x,x+1)\) 的交换次数就是 \(|a_x - b_x|\),这个式子就是为了使得前缀和相同而移动的 \(1\) 的次数。
而现在的两行其实只是多了交换行这个操作,而交换 \(00\) 或者 \(11\) 并没有任何意义,所以考虑交换 \(10\) 或者 \(01\),这样交换其实就是当 \(a_{1,x} < b_{1,x} \wedge a_{2,x} > b_{2,x}\) 或者 \(a_{1,x} > b_{1,x} \wedge a_{2,x} < b_{2,x}\),这样就可以让两次的列交换变成一次的行交换。
而且也可以证明,如果出现了上述情况则必然存在 \(10\) 或者 \(01\) 的情况,也就是一定可以交换。
所以直接暴力做就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
int a[3][N],b[3][N];
signed main(){
int n;scanf("%lld",&n);
int tmp1 = 0,tmp2 = 0;
for(int i=1; i<=2; i++){
for(int j=1; j<=n; j++){
scanf("%lld",&a[i][j]);
tmp1 += a[i][j];
}
}
for(int i=1; i<=2; i++){
for(int j=1; j<=n; j++){
scanf("%lld",&b[i][j]);
tmp2 += b[i][j];
b[i][j] += b[i][j-1];
}
}
if(tmp1 != tmp2){
printf("-1\n");
return 0;
}
int ans = 0;
int s[3] = {0,0,0};
for(int i=1; i<=n; i++){
s[1] += a[1][i],s[2] += a[2][i];
while(s[1] < b[1][i] && s[2] > b[2][i]) s[1]++,s[2]--,ans++;
while(s[1] > b[1][i] && s[2] < b[2][i]) s[1]--,s[2]++,ans++;
ans += abs(s[1] - b[1][i]) + abs(s[2] - b[2][i]);
}
printf("%lld\n",ans);
return 0;
}
CF1111D Destroy the Colony
题目描述:
有一个恶棍的聚居地由几个排成一排的洞穴组成,每一个洞穴恰好住着一个恶棍。
每种聚居地的分配方案可以记作一个长为偶数的字符串,第 \(i\) 个字符代表第 \(i\) 个洞里的恶棍的类型。
如果一个聚居地的分配方案满足对于所有类型,该类型的所有恶棍都住在它的前一半或后一半,那么钢铁侠可以摧毁这个聚居地。
钢铁侠的助手贾维斯有不同寻常的能力。他可以交换任意两个洞里的野蛮人(即交换字符串中的任意两个字符)。并且,他可以交换任意次。
现在钢铁侠会问贾维斯 \(q\) 个问题。每个问题,他会给贾维斯两个数 \(x\) 和 \(y\)。贾维斯要告诉钢铁侠,从当前的聚居地分配方案开始,他可以用他的能力创造多少种不同的方案,使得与原来住在第 \(x\) 个洞或第 \(y\) 个洞中的恶棍类型相同的所有恶棍都被分配到聚居地的同一半,同时满足钢铁侠可以摧毁这个聚居地。
如果某一个洞里的恶棍在两种方案中类型不同,则这两种方案是不同的。
答案对 \(10^9+7\) 取模。
\(2 \le |s| \le 10^5,1 \le q \le 10^5\),字符串 \(s\) 仅包含小写和大写英文字母。
题目分析:
考虑如果没有 \(x,y\) 的限制该怎么做。
因为我们无论怎么分配类型相同的都必然在同一部分,所以就可以让类型相同的放在一起处理,所以下文直接记 \(c_i\) 表示类型为 \(i\) 的数的个数。
一个显然的观察就是,对于任意一种分配每种类型的数在前半部分还是后半部分的方案,其组合之后对答案的影响都是:
所以我们本质上只需要对于钦定该类型在前面还是在后面的方案数计数,然后将得到的方案数乘以上面的系数就是答案。
而因为我们有前面和后面的大小都为 \(\frac{n}{2}\) 的限制,所以此时本质上就是一个给定最多 \(52\) 个物品,每个物品有一个重量 \(c_i\),要求重量恰好为 \(\frac{n}{2}\) 的方案数。
对于一个询问相当于要求强制不选 \(s_x\) 和 \(s_y\) 这两个物品。
一个思路就是可删除背包,但是这样就没意思了。
我们考虑如果只有强制不选一个的限制,那么我们就可以直接维护前后缀然后做一个前后缀背包合并得到答案,此时有两个的限制而我们物品的个数很少,可以考虑直接钦定一个不选,然后对于另一个限制使用前后缀背包合并解决。
这些东西都可以放在预处理部分,这样就可以 \(O(1)\) 查询。
代码:
(这份代码只是为了方便理解思路,具体通过本题还需要一些卡常)
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5;
const int MOD = 1e9+7;
int g[55][N],f[55][N],ans[55][55],fac[N],inv[N],cnt[55];
char s[N];
void add(int &a,int b){
a = (a + b) % MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int binom(int n,int m){
if(n < m || n < 0 || m < 0) return 0;
return fac[n] * inv[m] % MOD * inv[n-m]%MOD;
}
int get(char c){
if('a' <= c && c <= 'z') return c - 'a' + 1;
return c - 'A' + 1 + 26;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%s",s+1);
int n = strlen(s+1);
fac[0] = 1;
for(int i=1; i<=n; i++) fac[i] = fac[i-1] * i % MOD;
inv[n] = power(fac[n],MOD-2);
for(int i=n-1; i>=0; i--) inv[i] = inv[i+1] * (i+1) % MOD;
for(int i=1; i<=n; i++) cnt[get(s[i])]++;
int tmp = 0;
tmp = fac[n/2] * fac[n/2] %MOD * 2 % MOD;
for(int i=1; i<=52; i++) tmp = tmp * inv[cnt[i]] % MOD;
for(int i=1; i<=52; i++){ //考虑钦定不选 i
memset(f,0,sizeof(f));memset(g,0,sizeof(g));
int sum = 0;
f[0][0] = 1;
for(int j=0; j<52; j++){ //前 j 个
sum += cnt[j];
for(int k=0; k<=n/2; k++){
add(f[j+1][k],f[j][k]);
if(j+1 != i && cnt[j+1]) add(f[j+1][k+cnt[j+1]],f[j][k]);
}
}
sum = 0;
g[53][0] = 1;
for(int j=53; j>1; j--){
sum += cnt[j];
for(int k=0; k<=n/2; k++){
add(g[j-1][k],g[j][k]);
if(j-1 != i && cnt[j-1]) add(g[j-1][k+cnt[j-1]],g[j][k]);
}
}
for(int j=1; j<=52; j++){
for(int k=0; k<=n/2; k++){
add(ans[i][j],f[j-1][k] * g[j+1][n/2-k] %MOD);
}
}
}
int q;scanf("%lld",&q);
while(q--){
int x,y;scanf("%lld%lld",&x,&y);
printf("%lld\n",ans[get(s[x])][get(s[y])] * tmp % MOD);
}
return 0;
}
CF1762F Good Pairs
题目描述:
给定一个 \(n\) 个数的序列 \(a\) 和一个数 \(k\),定义一个 子区间 \([l,r]\) 是好的,当且仅当这个区间内存在一个 子序列 满足:
-
包含 \(a_l\) 和 \(a_r\)
-
任意相邻两个数差的绝对值不超过 \(k\)
请求出所有合法的子区间数量。多组数据,\(1\le \sum n\le 5\times 10^5,\ 0\le k\le 10^5,\ 1\le a_i\le 10^5\)。
题目分析:
既然是统计合法子区间数量,一个想法就是枚举右端点 \(r\)。
设 \(dp[r]\) 表示以 \(r\) 为右端点的合法子区间数量,那么设 \(pos\) 表示 \(r\) 左端第一个满足 \(|a_{pos} - a_r| \le k\) 的数,那么 \(dp[r]\) 可以从 \(dp[pos]\) 转移来吗。
这显然不行,因为会有缺的,而且不好补,而如果从前面所有的满足 \(|s_{pos} - a_r| \le k\) 的转移过来就有重的,所以看上去这个就不大有前途。
那么就意味着我们需要发现一些性质,既然任意两个数的差不超过 \(k\),所以我们不妨假设 \(a_l > a_r\),则对于一个合法的子序列,从 \(r\) 向前第一个大于 \(a_r\) 的数 \(a_{pos}\) 一定不超过 \(a_r + k\),也就是说我们可以把这中间的所有值忽略,将这个子序列变成 \([l,pos] \cup \{r\}\),递归地看待这个问题,也就是合法的子序列一定可以理解为一个 \(r\) 到 \(l\) 严格上升的子序列。
这样这个问题就很可以做了,也就是 \(dp[r]\) 可以直接从 \(dp[pos]\) 转移,而缺失的部分就是 \(a_l \in [a_{r}+1,a_{pos}]\) 的点,十分好维护。
但是我们上面只讨论了 \(a_l > a_r\),对于 \(a_l < a_r\) 可以通过翻转数组使用同样的方式处理,对于 \(a_l = a_r\) 可以只选择这两个位置就一定合法,也很好维护。
上面这些东西直接放到线段树上,复杂度就是 \(O(n \log n)\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
const int INF = 1e9+5;
int n,k,rt,tot,dp[N],a[N],cnt[N],mx[4 * N],sz[4 * N],ls[4 * N],rs[4 * N];
bool vis[N];
void pushup(int now){
mx[now] = 0,sz[now] = 0;
if(ls[now]) mx[now] = max(mx[now],mx[ls[now]]),sz[now] += sz[ls[now]];
if(rs[now]) mx[now] = max(mx[now],mx[rs[now]]),sz[now] += sz[rs[now]];
}
void modify(int &now,int now_l,int now_r,int pos,int val){
if(!now) now = ++tot;
if(now_l == now_r){
sz[now]++;mx[now] = max(mx[now],val);
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(ls[now],now_l,mid,pos,val);
if(pos > mid) modify(rs[now],mid+1,now_r,pos,val);
pushup(now);
}
int query1(int now,int now_l,int now_r,int l,int r){
if(!now) return 0;
if(l <= now_l && now_r <= r) return mx[now];
int mid = (now_l + now_r)>>1,ans = 0;
if(l <= mid) ans = max(ans,query1(ls[now],now_l,mid,l,r));
if(r > mid) ans = max(ans,query1(rs[now],mid+1,now_r,l,r));
return ans;
}
int query2(int now,int now_l,int now_r,int l,int r){
if(!now) return 0;
if(l <= now_l && now_r <= r) return sz[now];
int mid = (now_l + now_r)>>1,ans = 0;
if(l <= mid) ans = ans + query2(ls[now],now_l,mid,l,r);
if(r > mid) ans = ans + query2(rs[now],mid+1,now_r,l,r);
return ans;
}
int solve(){
for(int i=1; i<=tot; i++) mx[i] = 0,sz[i] = 0,ls[i] = 0,rs[i] = 0;
tot = 0;rt = 0;
int res = 0;
for(int r=1; r<=n; r++){
int pos = query1(rt,1,1e5,a[r]+1,a[r]+k);
if(pos == 0) dp[r] = 0;
else dp[r] = dp[pos] + query2(rt,1,1e5,a[r]+1,a[pos]);
res += dp[r];
modify(rt,1,1e5,a[r],r);
}
return res;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]),cnt[a[i]]++;
int ans = 0;
for(int i=1; i<=n; i++){
if(vis[a[i]]) continue;
vis[a[i]] = true;
ans += cnt[a[i]] * (cnt[a[i]] - 1) / 2 + cnt[a[i]];
}
ans += solve();
reverse(a+1,a+n+1);
ans += solve();
printf("%lld\n",ans);
for(int i=1; i<=tot; i++) mx[i] = 0,sz[i] = 0,ls[i] = 0,rs[i] = 0;
tot = 0,rt = 0;
for(int i=1; i<=n; i++) vis[a[i]] = false,cnt[a[i]] = 0;
}
return 0;
}