0x5C~0x5D
0x5C 计数类DP
a.[√] Gerald and Giant Chess
sol:
发现格子数很大,但是黑色格子数很小,所以考虑往黑色格子上靠。
所以考虑到容斥一下,即 不经过黑格子的路径条数=路径总条数-至少经过一个黑格子的路径条数。
从点\((1,1)\)到点\((x,y)\)的路径总条数应该为\(C_{x+y-2}^{x-1}\) ,只需求经过了黑格子的路径条数。
容易想到对把这些黑格子拿出来,同时按照距离点\((1,1)\)的远近(或按行列)排序,转为序列问题。
设\(f[i]\)表示不经过其他黑格子,从\((1,1)\)(序列上)第i个黑格子\((x_i,y_i)\)的路径条数。
转移应该为:
\[ f[i]=C_{x_i+y_i-2}^{x_i-1}-\sum_{j=0}^{i-1}f[j]*C_{x_i+y_i-x_j+y_j}^{x_i-y_i}\ \ (x_i≥x_j\ , \ y_i≥y_j) \]
注意到答案实际上即为:\(f[n+1]\)!
code:
#include<cmath>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define IL inline
#define LL long long
#define DB double
using namespace std;
IL int gi() {
RG int x=0,w=0; char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') w=1;ch=getchar();}
while (ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
return w?-x:x;
}
const int N=2003;
const int M=2e5+3;
const int mod=1e9+7;
int h,w,n,fc[M],fcn[M];
LL f[N];
struct Black{int x,y;}b[N];
IL bool cmp(Black A,Black B) {return A.x+A.y<B.x+B.y;}
IL int qpow(int x,int p) {
RG int ans=1;
for(;p;p>>=1,x=1ll*x*x%mod)
if(p&1) ans=1ll*ans*x%mod;
return ans;
}
IL int C(int a,int b) {return 1ll*fc[a]*fcn[b]%mod*fcn[a-b]%mod;}
int main()
{
RG int i,j;
h=gi(),w=gi(),n=gi();
for(i=1;i<=n;++i) b[i].x=gi(),b[i].y=gi();
b[0]=(Black){1,1},b[n+1]=(Black){h,w};
sort(b+1,b+n+2,cmp);
fc[0]=fcn[0]=1;
for(i=1;i<=h+w;++i)
fc[i]=1ll*fc[i-1]*i%mod,fcn[i]=qpow(fc[i],mod-2);
for(i=1;i<=n+1;++i) {
RG LL tmp=0;
RG int now=b[i].x+b[i].y;
for(j=1;j<i;++j)
if(b[i].x>=b[j].x&&b[i].y>=b[j].y)
tmp=(tmp+1ll*f[j]*C(now-b[j].x-b[j].y,b[i].x-b[j].x)%mod)%mod;
f[i]=((C(b[i].x+b[i].y-2,b[i].x-1)-tmp)%mod+mod)%mod;
}
printf("%lld\n",f[n+1]);
return 0;
}
b.[√] Connected Graph
sol:
吐槽:E心的高精!!!code交poj上SE(System Error)什么鬼,,,
进入正题,
同上面的那一个题,连通无向图数=图总数-不连通无向图数。
x个点的图的总数显然为\(2^{(\frac {x*(x-1)}{2})}\)
假设现在已经求出了i-1个点构成的连通无向图数,记为\(f[i-1]\)。
现在考虑i个点构成的图的生成,只需考虑不连通的无向图数。
可以这样考虑,整个图可以分为一个连通块(A)+若干连通块(B)。
只要保证A部分和B部分之间没有连边就好了。
所以考虑维护A,让A部分逐渐变化,可以保证不重不漏。
用f表示出来,则\(f[i]\)的求法应该为:
\[ f[i]=2^{(\frac {i*(i-1)}{2})}-\sum_{j=1}^{i-1}f[j]*C_{i-1}^{j-1}*2^{(\frac {(i-j)*(i-j-1)}{2})} \]
code(不是我自己的(╯▽╰)):
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct Int {
int a[800],len;
Int(int x=0) {
len=0;
memset(a,0,sizeof(a));
do {
a[++len]=x%10;
x/=10;
}while(x);
}
inline void update(int n) {
int k;
for(k=1;k<=n||a[k];++k)
if(a[k]>=10) a[k+1]+=a[k]/10,a[k]%=10;
len=max(k-1,1);
while(!a[len]&&len>1)--len;
}
int& operator [](int x){return a[x];}
const int& operator [](int x)const{return a[x];}
inline void print() {
for(register int i=len;i;--i)printf("%d",a[i]);
printf("\n");
}
};
inline Int operator +(const Int& a,const Int& b) {
Int ans;int n=max(a.len,b.len);
for(register int i=1;i<=n;++i) ans[i]+=a[i]+b[i];
ans.update(n);
return ans;
}
inline Int operator -(const Int& a,const Int& b) {
Int ans;int n=max(a.len,b.len);
for(register int i=1;i<=n;++i) {
ans[i]+=a[i]-b[i];
if(ans[i]<0) ans[i]+=10,--ans[i+1];
}
ans.update(n);
return ans;
}
inline Int operator *(const Int& a,const Int& b) {
Int ans;
for(register int i=1;i<=a.len;++i)
for(register int j=1;j<=b.len;++j)
ans[i+j-1]+=a[i]*b[j];
ans.update(a.len+b.len-1);
return ans;
}
Int f[51],c[51][51],qua[51*51];
inline void init() {
c[0][0]=Int(1);
for(register int i=1;i<=50;++i) {
c[i][0]=Int(1);
for(register int j=1;j<=i;++j)
c[i][j]=c[i-1][j-1]+c[i-1][j];
}
qua[0]=Int(1);Int base(2);
for(register int i=1;i<=50*50;++i)qua[i]=qua[i-1]*base;
f[1]=Int(1);
for(register int i=2;i<=50;++i) {
f[i]=qua[i*(i-1)/2];
for(register int j=1;j<i;++j)f[i]=f[i]-f[j]*c[i-1][j-1]*qua[(i-j)*(i-j-1)/2];
}
}
int main()
{
init();int x;
while(~scanf("%d",&x)&&x)f[x].print();
return 0;
}
c.[√] A decorative fence
sol:
考虑一块一块板子的补,都从左边补上。
假设当前是第i块板子,在已放的板子中从小到大排第j大,记为\(f[i,j,0/1]\)。
注意上述放板子不一定顺序取,i不是代表的前i块板子(当然事实上二者等价),0/1代表低(高)位。
那么转移应该为:
\[ f[i,j,0]=\sum_{k=j}^{i-1}f[i-1,k,1]\\ f[i,j,1]=\sum_{k=1}^{j-1}f[i-1,k,0] \]
DP完成后,考虑把答案拼凑出来。
可以发现第一块板子需单独考虑,剩下的板子的选择都会受之前的板子的影响。
记录一下用过的板子,上一块板子,和当前的状态(高低位)模拟即可。
code:
#include<cmath>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define IL inline
#define LL long long
#define DB double
using namespace std;
const int N=23;
LL m,f[N][N][2];
int n,last,state,use[N];
IL void DP() {
RG int i,j,k;
f[1][1][0]=f[1][1][1]=1;
for(i=2;i<=20;++i)
for(j=1;j<=i;++j) {
for(k=j;k<i;++k) f[i][j][0]+=f[i-1][k][1];
for(k=1;k<j;++k) f[i][j][1]+=f[i-1][k][0];
}
}
int main()
{
RG int i,j,k,T;
for(DP(),scanf("%d",&T);T;--T) {
scanf("%d%lld",&n,&m);
memset(use,0,sizeof(use));
for(i=1;i<=n;++i) {
if(f[n][i][1]>=m) {
last=i,state=1;
break;
}
m-=f[n][i][1];
if(f[n][i][0]>=m) {
last=i,state=0;
break;
}
m-=f[n][i][0];
}
use[last]=1;
printf("%d ",last);
for(i=2;i<=n;++i) {
state^=1;
for(j=1,k=0;j<=n;++j) {
if(use[j]) continue;
++k;
if(!state&&last>j||state&&j>last)
if(f[n-i+1][k][state]>=m) {last=j;break;}
else m-=f[n-i+1][k][state];
}
use[last]=1;
printf("%d ",last);
}
putchar('\n');
}
return 0;
}
0x5D 数位统计DP
d.[√] Apocalypse Someday
sol:
容易考虑到设\(g[i]\)表示由i位构成的魔鬼数个数,\(f[i,0/1/2]\)表示i位构成的末尾又连续的0/1/2个6的非魔鬼数个数。
那么考虑每次从末尾填一个新的数,则转移应该为:
\[ f[i,2]=f[i-1,0],f[i,1]=f[i-1,0]\\ f[i,0]=9*(f[i-1,0]+f[i-1,1]+f[i-1,2])\\ g[i]=f[i-1,2]+g[i-1]*10 \]
和上面的题目类似,考虑把答案拼凑出来。
注意,由g的转移方式可知,g中的方案数包括含有前导0的。
首先利用g确定位数。
然后从低位到高位依次填数,枚举当前位填的数,
然后计算出填了它之后能有多少个魔鬼数(还需特别考虑已经是魔鬼数和当前填了6的情况,详见代码)。
如果小于总数,则当前为应该填一个更大的,否则就填它。
code:
#include<cmath>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define IL inline
#define LL long long
#define DB double
using namespace std;
IL int gi() {
RG int x=0,w=0; char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') w=1;ch=getchar();}
while (ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
return w?-x:x;
}
const int N=17;
int n,m,ans[N];
LL g[N],f[N][3];
IL void DP() {
RG int i;
f[0][0]=1,f[1][0]=9,f[1][1]=1;
for(i=2;i<=15;++i) {
g[i]=f[i-1][2]+10*g[i-1];
f[i][2]=f[i-1][1],f[i][1]=f[i-1][0];
f[i][0]=9*(f[i-1][2]+f[i-1][1]+f[i-1][0]);
}
}
int main()
{
RG int i,j,k,p,T;
for(DP(),T=gi();T;--T) {
n=gi();
for(m=3;g[m]<n;++m);
for(i=m,k=0;i;--i) {
//k用于记录末尾已经有几个连续的6,k=3代表已经是魔鬼数。
for(j=0;j<=9;++j) {
RG LL cnt=g[i-1];
if(j==6||k==3)
for(p=max(3-k-(j==6),0);p<=2;++p) cnt+=f[i-1][p];
if(cnt<n) n-=cnt;
else {
if(k<3) k=(j==6)?k+1:0;
printf("%d",j);
break;
}
}
}
putchar('\n');
}
return 0;
}
f.[√] 同类分布
sol:
首先考虑需要记录的东西,记\(pos\)表示填到的位数,\(sum\)表示各位之和,\(res\)表示填出来数的数值。
如果存在\(sum|res\)那么就可以贡献1的答案。
但是发现\(res\)过于大,无法实现直接记录,那么考虑取模。
可以发现最好的模数就是\(sum\)本身了。
故此时若存在\(res==0\&\&sum==mod\)即可贡献1的答案。
然后发现\(mod\)随\(sum\)是不断变化的,所以考虑到枚举所有的\(mod\in[1,len*9]\),\(len\)为上界数的长度。
最后用记忆化搜索的方式实现即可。
注意当前位数的取值范围有可能被上一位限制了,不一定为\([0,9]\)。
code:
#include<cmath>
#include<string>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define RG register
#define IL inline
#define LL long long
#define DB double
using namespace std;
int mod,len,a[23];
LL l,r,f[23][203][203];
LL dfs(int pos,int sum,int res,int lim) {
if(pos>len) return sum==mod&&!res?1:0;
if(!lim&&f[pos][sum][res]!=-1) return f[pos][sum][res];
RG LL ans=0;
RG int i,upl=lim?a[len-pos+1]:9;
for(i=0;i<=upl;++i)
ans+=dfs(pos+1,sum+i,(10*res+i)%mod,lim&&i==upl);
return lim?ans:f[pos][sum][res]=ans;
}
IL LL getans(LL x) {
RG LL ans=0;
for(len=0;x;x/=10) a[++len]=x%10;
for(mod=1;mod<=len*9;++mod) {
memset(f,-1,sizeof(f));
ans+=dfs(1,0,0,1);
}
return ans;
}
int main()
{
scanf("%lld%lld",&l,&r);
printf("%lld\n",getans(r)-getans(l-1));
return 0;
}