数学
Adelaide_Black · · 个人记录
组合计数类
P8557 炼金术(Alchemy)
题意简述
有
思路分析
从每块金属的角度出发,对于
code
#include<iostream>
#include<cstdio>
#include<cmath>
#define ll long long
#define Mod 998244353
using namespace std;
ll n,k;
ll pow(ll a,ll b){
ll ans=1;
for(;b;b>>=1){
if(b&1) ans*=a,ans=ans%Mod;
a=(a*a)%Mod;
}
return ans;
}
int main(){
scanf("%lld%lld",&n,&k);
ll num=pow((long long)2,k);
num-=1;
cout<<pow(num,n)<<endl;
return 0;
}
不要老想着从整体出发,抓住特定的角度来计数。
这里就是从金属的角度考虑,比从炉子的角度考虑更优。
P5303 [GXOI/GZOI2019] 逼死强迫症
题意简述
有一个
思路总结
首先考虑不存在
再考虑有
因为中间的矩阵可以上下颠倒,所以有两种方案,有小方块的情况的方案前面的系数要乘个
由于数据范围的原因,所以要用矩阵乘法优化递推式的计算。定义状态为
状态转移矩阵为:
code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define ll long long
#define Mod 1000000007
using namespace std;
ll T,n;
ll zt[5][5]=
{
{1,1,0,0,0},
{1,0,0,0,0},
{2,0,1,1,1},
{0,0,1,0,0},
{2,0,0,0,1}
};
ll cs[5]={2,0,1,1,1};
ll a[5],b[5][5];
void mul(){
//a*b
ll c[5];
memset(c,0,sizeof(c));
for(int i=0;i<5;i++){
for(int k=0;k<5;k++){
c[i]=(c[i]+a[k]*b[k][i])%Mod;
}
}
memcpy(a,c,sizeof(c));
return;
}
void selfmul(){
ll c[5][5];
memset(c,0,sizeof(c));
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
for(int k=0;k<5;k++){
c[i][j]=(c[i][j]+b[i][k]*b[k][j])%Mod;
}
}
}
memcpy(b,c,sizeof(c));
return;
}
void ksm(ll x){
for(;x;x>>=1){
if(x&1) mul();
selfmul();
}
}
int main(){
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);
if(n<3) cout<<"0"<<endl;
else{
memcpy(a,cs,sizeof(cs));
memcpy(b,zt,sizeof(zt));
ksm(n-3);
cout<<a[0]<<endl;
}
}
return 0;
}
P7738 [NOI2021] 量子通信
从
所以我们可以把词典中的单词都处理为
统计
最后拿了
#include <bits/stdc++.h>
using namespace std;
#define gc getchar()
#define mem(x,v) memset(x,v,sizeof(x))
typedef unsigned long long ull;
typedef unsigned short us;
namespace IO{
char buf[1<<23],*p1=buf,*p2=buf;
#ifdef __WIN32
#define gc getchar()
#else
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin),p1==p2)?EOF:*p1++)
#endif
inline int read(){
int x=0;char s=gc;
while(!isdigit(s))s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
return x;
}
} using namespace IO;
const int N=400000+5;
const int W=65536;
bool s[N][256],q[256];
ull myRand(ull &k1,ull &k2){
ull k3=k1,k4=k2;
k1=k4,k3^=(k3<<23),k2=k3^k4^(k3>>17)^(k4>>26);
return k2+k4;
}
void gen(int n,ull a1,ull a2){
for(int i=0;i<n;i++)
for(int j=0;j<256;j++)
s[i][j]=(myRand(a1,a2)&(1ull<<32))?1:0;
}
struct EDGE{
int cnt,hd[W],nxt[N],to[N];
void add(int x,int y){nxt[++cnt]=hd[x],hd[x]=cnt,to[cnt]=y;}
}buc[16];
ull n,m,a1,a2;
us val[N][16],mp[W],qq[16];
int main(){
cin>>n>>m>>a1>>a2,gen(n,a1,a2);
for(int i=0;i<n;i++)
for(int j=0;j<16;j++){
//j 表示一个单词的第几位
//将 256 位二进制数 16 位 16 位的划分;
//因为每 16 位二进制转化为 10 进制表示后为一位 65536 进制下的数
for(int k=0;k<16;k++)
val[i][j]+=s[i][j*16+k]<<k;
//k 表示将这 16 位二进制数转化为 10 进制数
buc[j].add(val[i][j],i);
//用 链式向前星 将每一位数与其单词建立联系,方便之后查找
}
for(int i=1;i<W;i++)mp[i]=mp[i>>1]+(i&1);
//处理从 1 ~ 65536 每一个数的二进制表示下有几个 1
for(int i=0,las=0,k;i<m;i++,mem(qq,0)){
for(int j=0,vv=0;j<64;j++,vv=0){
char s=gc;
if(isdigit(s))vv=s-'0';
else if(s>='A'&&s<='F')vv=10+s-'A';
else {j--; continue;}
for(int l=0;l<4;l++)q[j*4+l]=(vv>>3-l)&1;
//从高位到低位拆解二进制数
//- 号的优先级在 >> 前面
}
bool ok=0; k=read();
for(int j=0;j<16;j++)
for(int l=0;l<16;l++)
qq[j]+=(q[j*16+l]^las)<<l;
//将新输入的数转成 65536 进制的数并将它异或上上一个答案使得得到正确输入
for(int j=0;j<16;j++){
for(int p=buc[j].hd[qq[j]];p;p=buc[j].nxt[p]){
int it=buc[j].to[p],cnt=0;
us *pval=val[it],*pq=qq;
//找包含这个数位的所有数,与 y 计算,验证是否小于等于 k
for(int l=0;l<16;l++,pval++,pq++){
cnt+=mp[(*pval)^(*pq)];
if(cnt>k)break;
} if(cnt<=k){ok=1; break;}
} if(ok)break;
} cout<<(las=ok)<<'\n';
}
return 0;
}
P2822 [NOIP2016 提高组] 组合数问题
这道题主要利用了组合数的这个性质造出了一个递推式:
可以运用选物品的思想来理解这个例子,对于第
因为
之后再利用二维前缀和统计是
code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
int n,m,k,t;
int sum[2005][2005],ans[2005][2005];
int main(){
scanf("%d%d",&t,&k);
sum[0][0]=sum[1][0]=sum[1][1]=1;
for(int i=2;i<=2000;i++){
sum[i][0]=1;
for(int j=1;j<=i;j++){
sum[i][j]=(sum[i-1][j-1]+sum[i-1][j])%k;
}
}
for(int i=1;i<=2000;i++){
for(int j=1;j<=2000;j++){
if(j>i) ans[i][j]=ans[i-1][j]+ans[i][j-1]-ans[i-1][j-1];
else ans[i][j]=ans[i-1][j]+ans[i][j-1]-ans[i-1][j-1]+(sum[i][j]==0?1:0);
}
}
while(t--){
scanf("%d%d",&n,&m);
cout<<ans[n][m]<<endl;
}
return 0;
}
- P5239 回忆京都
和这一题一样的,需要注意的一点是由于前缀和运算中有减的操作,所以要
P8576 「DTOI-2」星之界
关于数学公式推导
关于阶乘和逆元的处理
对于
对于
这里是把逆元当成分数的形式来看,已知
之后整块答案更新的维护也是运用这个原理,这里就不赘述了。
通过并查集来维护块内相同的数
因为我们通过分块将序列分成了若干个长度相同的块状,对于同一块内整体修改同一元素的操作,可以用并查集优化修改操作来维护。
将块中第一个出现的元素当做父亲,其它元素指向它,每次更改的时候,如果是更改整块的元素,就将
code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define Mod 998244353
#define ll long long
using namespace std;
int n,q,B=317;
int a[100005];
int inv[100005],fac[10000005],powinv[100005][320],powfac[100005][320];
int pos[100005],bl[320],br[320];
struct node{
int head,num;
}e[320][100005];
int fa[100005],v[100005];
int sum[320],mul[320];
int ksm(int x,int y){
ll ans=1;
for(;y;y>>=1){
if(y&1) ans=(ll)(ans)*x%Mod;
x=(ll)x*x%Mod;
}
return ans;
}
int find(int x){
if(fa[x]==x) return fa[x];
return fa[x]=find(fa[x]);
}
void init(int x){
for(int i=bl[x];i<=br[x];i++){
if(!e[x][a[i]].head){
e[x][a[i]].head=i;
e[x][a[i]].num=1;
fa[i]=i;
v[i]=a[i];
}
else{
fa[i]=e[x][a[i]].head;
e[x][a[i]].num++;
}
}
sum[x]=0,mul[x]=1;
for(int i=bl[x];i<=br[x];i++){
sum[x]+=a[i];
mul[x]=(ll)mul[x]*inv[a[i]]%Mod;
}
return;
}
void clac1(int now,int l,int r,int x,int y){
for(int i=bl[now];i<=br[now];i++){
a[i]=v[find(i)];
e[now][a[i]].head=e[now][a[i]].num=0;
//不要直接memset会超时
}
for(int i=bl[now];i<=br[now];i++) fa[i]=i;
//父节点要单独更新,因为并查集还在查找相同的数
for(int i=l;i<=r;i++) if(a[i]==x) a[i]=y;
init(now);
}
void clac2(int now,int x,int y){
sum[now]+=(y-x)*e[now][x].num;
mul[now]=(ll)mul[now]*(ll)powfac[x][e[now][x].num]%Mod*(ll)powinv[y][e[now][x].num]%Mod;
e[now][y].num+=e[now][x].num;
if(!e[now][y].head){
e[now][y].head=e[now][x].head;
v[e[now][y].head]=y;
}
else fa[e[now][x].head]=e[now][y].head;
e[now][x].head=e[now][x].num=0;
return;
}
void change(int l,int r,int x,int y){
if(pos[l]==pos[r]){
clac1(pos[l],l,r,x,y);
}
else{
int L,R;
if(l!=bl[pos[l]]){
clac1(pos[l],l,br[pos[l]],x,y);
L=pos[l]+1;
}
else L=pos[l];
if(r!=br[pos[r]]){
clac1(pos[r],bl[pos[r]],r,x,y);
R=pos[r]-1;
}
else R=pos[r];
for(int i=L;i<=R;i++){
clac2(i,x,y);
}
}
return;
}
int ask(int l,int r){
ll y=1,sum_ans=0;
if(pos[l]==pos[r]){
for(int i=l;i<=r;i++){
int z=v[find(i)];
//暴力枚举的要用最新的数
sum_ans+=z;
y=(ll)y*inv[z]%Mod;
}
}
else{
int L,R;
if(bl[pos[l]]!=l){
for(int i=l;i<=br[pos[l]];i++){
int z=v[find(i)];
sum_ans+=z;
y=(ll)y*inv[z]%Mod;
}
L=pos[l]+1;
}
else L=pos[l];
if(br[pos[r]]!=r){
for(int i=bl[pos[r]];i<=r;i++){
int z=v[find(i)];
sum_ans+=z;
y=(ll)y*inv[z]%Mod;
}
R=pos[r]-1;
}
else R=pos[r];
for(int i=L;i<=R;i++){
sum_ans+=sum[i];
y=(ll)y*mul[i]%Mod;
}
}
return (ll)fac[sum_ans]*y%Mod;
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),fa[i]=i;
fac[0]=inv[0]=1;
for(int i=1;i<=10000001;i++) fac[i]=(ll)fac[i-1]*i%Mod;
inv[100000]=ksm(fac[100000],Mod-2);
for(int i=100000-1;i>=1;i--) inv[i]=(ll)inv[i+1]*(i+1)%Mod;
for(int i=1;i<=100000;i++){
powfac[i][0]=powinv[i][0]=1;
for(int j=1;j<=B;j++){
powfac[i][j]=(ll)powfac[i][j-1]*fac[i]%Mod;
powinv[i][j]=(ll)powinv[i][j-1]*inv[i]%Mod;
}
}
for(int i=1;i<=n;i++){
pos[i]=(i/B)+1;
if(pos[i]!=pos[i-1]){
bl[pos[i]]=i,br[pos[i-1]]=i-1;
}
}
br[pos[n]]=n;
for(int i=1;i<=(n/B)+1;i++){
init(i);
}
while(q--){
int type,l,r,x,y;
scanf("%d",&type);
if(type==1){
scanf("%d%d%d%d",&l,&r,&x,&y);
if(x==y) continue;
change(l,r,x,y);
}
else {
scanf("%d%d",&l,&r);
printf("%d\n",ask(l,r));
}
}
return 0;
}
遇到的几个坑点
-
重构块初始化时,要先将元素更新完在更新并查集,不能同时更新
-
对于块的初始化,不能用
memset ,直接把块内有值的元素赋值为0 即可,不然会超时。 -
好像没了
-
第一道分块。分块的结构确实很清晰。