棋盘
需要注意的几点:
- 题面编号都是从0开始的,所以第1行实际指的是中间那行
- 对(2^{32})取模,其实就是(unsigned int),直接自然溢出啥事没有
- 棋子攻击范围不会旋转
首先,我们得找出所有满足条件的同一行的状态,在此之前,我们要先处理出状态(S)下,第(k)行能被棋子攻击到的格子(at[k][S])
先钦定自己不能攻击自己
再枚举每一个状态中每一个1,然后对攻击范围模板进行左移/右移操作,把这一行这种状态能攻击到的格子或上他
接下来,枚举每一个状态,如果(at[1][i] and i==0),则记录下这种合法的情况。
这个时候我们就可以枚举每一行的状态来求解了,但是这样时间复杂度是(O(ncdot 2^{2m}))的,而在(n)给到(1e6)的情况下无疑会炸掉,需要考虑进一步的优化
(颓了一下题解)
其实,每一种状态都是由之前的某种状态转移来的,且可以重复,那就说明如果两个状态可以排在一起,他们就可以多次排在一起
考虑矩阵快速幂优化
设初始矩阵中(A[i][j]=1)表示(j)可以排在(i)的后面
则有这样的柿子:(f[i][j]=f[i][k]cdot f[k][j])
这样的情况下,我们求(A^n),因为第一种情况是为空,所以(sumlimits_{i=1}^{cnt}A^n[1][i])即为答案,意为开始为空,经过(n)行后最后一行为状态(i)的总数
code:
code:
#include<bits/stdc++.h>
using namespace std;
#define ll unsigned int
#define ull unsigned long long
#define ZZ_zuozhe
#define M 6
#define N 80
ll n,m,p,k;
ll lim[4];
ll at[4][1<<M];
ll sit[N],cnt=0;
struct mat
{
ll a[N][N];
mat(){memset(a,0,sizeof a);}
};
mat operator*(mat a,mat b)
{
mat c;
for(int i=1;i<=cnt;i++)for(int j=1;j<=cnt;j++)for(int k=1;k<=cnt;k++)
{
c.a[i][j]=c.a[i][j]+a.a[i][k]*b.a[k][j];
}
return c;
}
mat ksm(mat a,ll b)
{
mat res=a;
b--;
while(b)
{
if(b&1)res=res*a;
a=a*a;
b>>=1;
}
return res;
}
mat a,ans;
ll t;
int main()
{
scanf("%u%u%u%u",&n,&m,&p,&k);
for(int i=0;i<3;i++)
{
for(int j=0;j<p;j++)
{
scanf("%u",&t);
if(t)lim[i]|=(1<<j);
}
}
lim[1]-=(1<<k);
ll all=(1<<m)-1;
for(int i=0;i<=all;i++)
{
at[0][i]=at[1][i]=at[2][i]=0;
for(int j=0,p=i;p;j++,p>>=1)
{
if((p&1)==0)continue;
at[0][i]|=(j<k)?lim[0]>>(k-j):lim[0]<<(j-k);
at[1][i]|=(j<k)?lim[1]>>(k-j):lim[1]<<(j-k);
at[2][i]|=(j<k)?lim[2]>>(k-j):lim[2]<<(j-k);//处理每种状态每行能够攻击到的位置集合
}
}
for(int i=0;i<=all;i++)
{
if((i&at[1][i])==0)sit[++cnt]=i;
}
for(int i=1;i<=cnt;i++)
{
for(int j=1;j<=cnt;j++)
{
if(((sit[i]&at[0][sit[j]])==0)&&((sit[j]&at[2][sit[i]])==0))a.a[i][j]++;
//处理初始矩阵,如果j放在i后面不冲突就++
}
}
a.a[1][1]=1;
ans=ksm(a,n);
ll aa=0;
for(int i=1;i<=cnt;i++)
{
aa+=ans.a[1][i];
}
printf("%u
",aa);
return 0;
}
最大前缀和
开始的思路
其实求最大前缀和似乎就是每个负数前判一次……
这几个数总排列方式是(n!),答案要乘他就说明直接拿可能值(cdot)这种情况的次数求和就行……
(n)的范围看起来挺合适拿来状压的
口胡一下:(f[i][S])为选(i)个当前状态为(S)的期望和
如果新取一个负数答案应该是直接加,取正数就得更新,但是好像也不一定会更新……
是不是得记录下当前值和更新答案的临界值什么的……
或者用状压记录负数位置……?
(实际证明没一点挨上边)
正解
状压还能这么出的吗……
深挖一下最大前缀和的性质:
设数列(A)的最大前缀和为(pre_j),即(sumlimits_{i=1}^{j} a_i)
那么由此可以推出:
- 对于任意(1leq x leq j),有(sumlimits_{i=x}^{j} A_i)定不小于0(否则可以把这部分减去)
- 对于任意(j+1leq x leq n),有(sumlimits_{i=j+1}^{x} A_i)定不大于0(否则可以把这部分加上)
然后,我们发现,原数列可以分成两部分,分别满足不同的条件,既然如此,我们把满足左边条件的排列个数与满足右边条件的排列个数相乘,就可以得到总方案个数。
但是,题目中要求我们求最大前缀和的期望(cdot n!),也就是用每种可能值乘上能得到它的方案个数,所以,结合状压,我们可以将每种状态满足左右的排列个数相乘,再把每种状态的满足条件方案个数乘上每种状态选的数的总和,即为所求
对于右边,我们在一个所有前缀和为负的数列后面加一个数,若新数列总和仍然为负,则新数列所有前缀和为负
对于左边,我们在一个所有后缀和为正的数列前面加一个数,若新数列总和仍然为非负,则新数列所有后缀和为非负
基于这样的过程,我们可以枚举子集,则有
答案为
另外注意:答案要求非负整数,所以最后要模成正的
code:
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 21
#define mod 998244353
#define S (1<<20)
ll n,all;
ll a[N];
ll sum[S];
ll f[S],g[S];
ll ans=0;
int main()
{
scanf("%lld",&n);
all=(1<<n)-1;
for(int i=0;i<n;i++)scanf("%lld",&sum[1<<i]);
for(int i=0;i<=all;i++)sum[i]=(sum[i&(-i)]+sum[i^(i&-i)])%mod;
g[0]=1;
for(int i=0;i<=all;i++)
{
if(sum[i]<0)
{
for(int j=0;j<n;j++)
{
if(i&(1<<j))
{
g[i]=(g[i]+g[i^(1<<j)])%mod;
}
}
}
}
f[0]=1;
for(int i=0;i<=all;i++)
{
if(sum[i]>=0)
{
for(int j=0;j<n;j++)
{
if((i&(1<<j))==0)
{
f[i|(1<<j)]=(f[i|(1<<j)]+f[i])%mod;
}
}
}
}
for(int i=0;i<=all;i++)
{
//cout<<sum[i]<<' '<<f[i]<<' '<<g[all^i]<<endl;
ans=(ans+sum[i]*f[i]%mod*g[all^i]%mod+mod)%mod;
}
printf("%lld",ans);
return 0;
}
小星星
事实证明确实不是很擅长 (dp) 这块,必须得多看了……
这种神仙思路怎么想到啊……
先是一个 (O(ncdot 3^n)) 的暴力:
设 (f[u][u'][S]) 为 (u) 在原图上对应 (u') ,其在新图子树中的点在原图中对应情况为 (S) 的方案数
那么 (u) 在树上,可以树形 (dp) 求出来,具体的方法是一边合并子树一边计算答案,详细见代码注释
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 18
#define E 36
#define S (1<<18)
#define pb push_back
ll n,m,t1,t2;
vector<ll> e[N];
ll to[E],nx[E],h[N],tot=0;
inline void add(ll a,ll b){
to[++tot]=b;
nx[tot]=h[a];
h[a]=tot;
}
vector<ll> s[N];
ll siz[N],f[N][N][S];
inline void dfs(ll u,ll fa){
siz[u]=1;
for(int i=1;i<=n;i++)f[u][i][1<<i-1]=1;//初始化:u在原图对应i
for(int i=h[u];i;i=nx[i])if(to[i]!=fa){ ll v=to[i];
dfs(v,u);
//将子树合并过来
for(int uu=1;uu<=n;uu++){//新图上映射点
for(int j=0;j<s[siz[u]].size();j++){//找到符合条件的当前映射子树集合
ll su=s[siz[u]][j];
if(!(su&(1<<uu-1)))continue;//uu的子树必须有uu
for(int tv=0;tv<e[uu].size();tv++){ ll vv=e[uu][tv];//找一条从uu连出去的vv,用来对应v
if(su&(1<<vv-1))continue; //vv是要合并进来的,之前不能有
for(int k=0;k<s[siz[v]].size();k++){//找到vv的子树集合
ll sv=s[siz[v]][k];
if((su&sv)||!(sv&(1<<vv-1)))continue;//vv子树必须有vv,而且不能和uu子树相交
f[u][uu][su|sv]+=f[u][uu][su]*f[v][vv][sv];//合并起来
}
}
}
}
siz[u]+=siz[v];//更新子树大小
}
}
ll ans=0;
inline ll read(){
ll f=0,s=0; char c=getchar();
while(c>'9'||c<'0')f=(c=='-'),c=getchar();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
return f?-s:s;
}
int main(){
n=read(); m=read();
for(int i=1;i<=m;i++){
t1=read(),t2=read();
e[t1].pb(t2); e[t2].pb(t1);
}
for(int i=1;i<n;i++){
t1=read(); t2=read();
add(t1,t2); add(t2,t1);
}
for(int i=1,x=0;i<(1<<n);i++,x=0){
for(int j=1;j<=n;j++)if(i&(1<<j-1))x++;
s[x].pb(i);//处理出每种大小的集合,用来枚举子树映射集合
}
dfs(1,0);
for(int i=1;i<=n;i++)ans+=f[1][i][(1<<n)-1];
printf("%lld",ans);
return 0;
}
正解:
似乎优化子集dp的常见方法是容斥
之前的算法中,我们规定每个点被映射有且仅有一次,如果不规定,单次dfs复杂度 (O(ncdot 3^n) o O(n^3)),但是这样会造成答案偏大,因为一些点被重复映射,一些点没有被映射,要去掉这些与题意不符的答案,就用到了容斥。
我们的目标是求所有点都被映射有且仅有一次的方案数,那么首先减去有一个点未被映射的方案,这时就多减了有两个点未被映射的方案,同理需要再加上三个点的方案,减去四个点的方案……最后结果是所有不符合题意的答案都被除去
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
#define N 18
#define E 36
#define S (1<<18)
#define pb push_back
ll n,m,t1,t2;
vector<ll> e[N];
ll to[E],nx[E],h[N],tot=0;
inline void add(ll a,ll b){
to[++tot]=b;
nx[tot]=h[a];
h[a]=tot;
}
vector<ll> s[N];
ll del[N];
ll siz[N],f[N][N];
inline void dfs(ll u,ll fa){ ll sum=0;
for(int i=1;i<=n;i++)f[u][i]=1;//初始化:u在原图对应i
for(int i=h[u];i;i=nx[i])if(to[i]!=fa){ ll v=to[i];
dfs(v,u);
//将子树合并过来
for(int uu=1;uu<=n;uu++){//新图上映射点
sum=0;
for(int tv=0;tv<e[uu].size();tv++){ ll vv=e[uu][tv];
sum+=f[v][vv]*(del[uu]&del[vv]);
}
f[u][uu]*=sum;//乘法原理,已经得到的方案和新方案里选一对
}
}
}
ll ans=0;
inline ll read(){
ll f=0,s=0; char c=getchar();
while(c>'9'||c<'0')f=(c=='-'),c=getchar();
while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
return f?-s:s;
}
signed main(){
n=read(); m=read();
for(int i=1;i<=m;i++){
t1=read(),t2=read();
e[t1].pb(t2); e[t2].pb(t1);
}
for(int i=1;i<n;i++){
t1=read(); t2=read();
add(t1,t2); add(t2,t1);
}
for(int i=0;i<(1<<n);i++){ ll siz=n,res=0;
for(int j=1;j<=n;j++)del[j]=0;
for(int j=1;j<=n;j++)if((1<<j-1)&i)del[j]=1,siz--; //del[i]表示留下i
dfs(1,0);
for(int j=1;j<=n;j++)res+=f[1][j]; //sum(f[1][j])即为这种情况下的答案
if(siz%2)ans-=res;
else ans+=res;
}
printf("%lld",ans);
return 0;
}