2012 集合选数
题目描述
解法
要不是吃饭去了我肯定能完全想明白,话说网上的题解点都不负责任,构造怎么得来的不写一下?
先考虑只有 \(2x\) 被禁用的情况,一开始我想了很多方法都避免不了状压,究其原因是限制过于分散造成我们需要记录的信息太多。回想限制最紧凑的模型是线性 \(dp\),因为它的限制都是挨在一起的所以无需记录。
但是又注意到问题的限制是不交的链,那么对于每条链我们可以取出来分别计算,然后乘法原理合并。计算每条链的时候用线性 \(dp\) 就行了,要求只有相邻两个数不能都选。
回到本题 \(2x,3x\) 的情况,那么问题的限制是若干个不交的矩形,要求是相邻的数不能同时选:
1 2 4 8 .....
3 6 12 24 ....
9 18 36 72 ....
....
因为矩形的大小很小,所以我们对行状压然后暴力转移即可,注意去掉不合法状态就可以轻松跑过。
#include <cstdio>
const int M = 100005;
const int MOD = 1e9+1;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,vis[M],lim[M];
int a[12][20],dp[12][1<<18],g[1<<18];
void build(int x)
{
for(int i=1;i<=11;i++)
{
if(i==1) a[1][1]=x;
else a[i][1]=a[i-1][1]*3;
if(a[i][1]>n) break;
m=i;vis[a[i][1]]=1;int cnt=1;
for(int j=2;j<=18;j++)
{
a[i][j]=a[i][j-1]*2;
if(a[i][j]>n) break;
vis[a[i][j]]=1;cnt=j;
}
lim[i]=1<<cnt;
}
}
int ask(int x)
{
int res=0;
for(int i=0;i<lim[1];i++)
dp[1][i]=g[i];
for(int i=2;i<=m;i++) for(int j=0;j<lim[i];j++)
{
if(!g[j]) continue;dp[i][j]=0;
for(int k=0;k<lim[i-1];k++)
if(g[k] && (k&j)==0)
dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
}
for(int i=0;i<lim[m];i++)
res=(res+dp[m][i])%MOD;
return res;
}
signed main()
{
n=read();ans=1;
for(int i=0;i<(1<<18);i++)
g[i]=!((i<<1)&(i));
for(int i=1;i<=n;i++) if(!vis[i])
build(i),ans=1ll*ans*ask(i)%MOD;
printf("%d\n",ans);
}
2012 与非
题目描述
解法
我真他吗要困死了,真的要尊重生理规律啊,晚上睡晚了我现在想死 \(.....\)
既然题目是问 \([L,R]\) 中能被凑出的数的个数,而且还是位运算,那么我们考虑魔改线性基。那么考虑线性基里面的第 \(i\) 个元素应该是,第 \(i\) 个数位是 \(1\),前面的数位都是 \(0\),后面的数位尽量为 \(0\)
这样设计线性基的元素目的是可以很容易地消去\(/\)增添第 \(i\) 个数位的值,并且对较大的数位无影响,并且对较小的数位影响尽量小。并且我们可以得到一个性质:是如果线性基某数位 \(i\) 的元素在数位 \(j\) 上有值(\(i\not=j\)),那么线性基的数位 \(j\) 一定没有元素,这是因为如果有的话为了化到最简可以消去。
那么如何构造出线性基呢?这里我们不采取依次插入的模式,而是从大到小依次构造。利用性质可以判断第 \(i\) 位是否应该有元素,如果有的话我们把每个都用上。设现在的数是 \(x\),那么我们把 \(x\) 和自己操作一次之后再和新加入的数 \(a\) 操作,操作的效果类似于取并集,这样我们就达到了简化当前元素的目的。
最后考虑怎么计算答案,首先我们可以做一个差分,然后就需要用到紧贴的思想,如果上界在这一位上位 \(1\) 那么考虑如果这一位填 \(0\) 后面可以任意填,如果填 \(1\) 那么继续循环,时间复杂度 \(O(n\log n)\)
更多的细节还是需要看看代码,难以讲得特别清楚。
#include <cstdio>
#define int long long
const int M = 1005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,l,r,a[M],b[M],sum[M];
int ask(int x)
{
if(x<0) return -1;
if(x>(1ll<<m)-1) return (1ll<<sum[m-1])-1;
int res=0,s=0;
for(int i=m-1;i>=0;i--) if(x>>i&1)
{
//0
if(i && !(s>>i&1))
res+=(1ll<<sum[i-1])-1;
//1
if(b[i])//Meanwhile s>>i&1==0
{
s|=b[i];
if(s>x) break;
res++;
}
else if(!(s>>i&1)) break;
}
return res;
}
signed main()
{
n=read();m=read();l=read();r=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=m-1,s=0;i>=0;i--)
if(!(s>>i&1))
{
int &now=b[i];now=(1ll<<m)-1;
for(int j=1;j<=n;j++)
if(a[j]>>i&1) now&=a[j];
else now&=~a[j];
sum[i]=1;s|=now;
}
for(int i=1;i<m;i++) sum[i]+=sum[i-1];
printf("%lld\n",ask(r)-ask(l-1));
}
2013 比赛
题目描述
解法
遇到这种乱搞题我是真做不来,虽然所有需要的性质我都观察出来了
首先我们需要有一个整体观念,因为要取模我们认为答案会超过 \(\tt int\),所以说就算你不进入不合法情况然后一个一个搜也会 \(\tt TLE\),那么我们要使用记忆化的技巧,明确这一点之后有如下剪枝:
- 当一只球队的分数已经超过给定的分数时跳出。
- 当一只球队如果赢下剩下的所有比赛分还是不够时也跳出。
- 可以预先解方程计算出胜场数 \(nx\) 和平局数 \(ny\),搜索时必须符合此限制。
- 由于每个人是等价的,我们在搜完一层(指决策完某个人的所有比赛)之后可以得到剩下人还需要的分数,那么我们把分数数组排序之后哈希,如果哈希值相同的情况方案数相同,所以这里可以记忆化。
我觉得这种题唯一需要的就是勇气,很多看上去不强的剪枝可能有奇效。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,sum,nx,ny,a[15],c[15];map<int,int> mp;
int dfs(int x,int y)
{
if(x==n) return 1;
if(a[x]+3*(n-y+1)<c[x]) return 0;
if(y==n+1)
{
int b[15]={},s=0;
for(int i=x+1;i<=n;i++) b[i]=c[i]-a[i];
sort(b+1,b+1+n);
for(int i=x+1;i<=n;i++) s=30*s+b[i]+1;
if(mp.find(s)!=mp.end()) return mp[s];
return mp[s]=dfs(x+1,x+2);
}
int ans=0;
if(a[x]+3<=c[x] && nx)//win
{
a[x]+=3;nx--;
ans+=dfs(x,y+1);
a[x]-=3;nx++;
}
if(a[y]+3<=c[y] && nx)//lose
{
a[y]+=3;nx--;
ans+=dfs(x,y+1);
a[y]-=3;nx++;
}
if(a[x]+1<=c[x] && a[y]+1<=c[y] && ny)
{
a[x]++;a[y]++;ny--;
ans+=dfs(x,y+1);
a[x]--;a[y]--;ny++;
}
return ans%MOD;
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
c[i]=read(),sum+=c[i];
sort(c+1,c+1+n);
nx=sum-n*(n-1);ny=n*(n-1)/2-nx;
printf("%lld\n",dfs(1,2)%MOD);
}
2013 数列
题目描述
解法
这道题真的是要好好写写,自己的想法和正解根本没沾边。
一开始我写出了一个根本就优化不来的容斥,但是我忽略了一个关键的条件:\(m(k-1)<n\),这说明不考虑首项的情况下,差分数组的总数是知道的,那么我们以他为切入点来分析问题。
考虑一个差分数组 \(a\),那么它对答案的贡献是:
我们直接考虑计算总贡献:
因为 \(a(i)\) 是互相独立并且在 \([1,m]\) 中自由选取的,所以后面一项的和是 \(m^{k-2}\cdot (k-1)\cdot\frac{m(m+1)}{2}\)
#include <cstdio>
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,p;
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%p;
a=a*a%p;
b>>=1;
}
return r;
}
signed main()
{
n=read();k=read();m=read();p=read();
int ans=n%p*qkpow(m,k-1)%p-
qkpow(m,k-2)*(k-1)%p*((m+1)*m/2%p)%p;
printf("%lld\n",(ans+p)%p);
}
2013 旅行
题目描述
解法
这道题太神了,我花了一晚上的时间才完全搞懂
首先我们只考虑最小值是多少,先给结论吧,设 \(b_i\) 表示权值数组的后缀和,\(sum=b_1\):
- 若 \(sum=0\),设 \(tot\) 表示 \(b\) 数组中 \(0\) 点个数。如果 \(tot\geq m\),那么答案为 \(0\);如果 \(tot<m\),那么答案为 \(1\)
- 若 \(sum\not=0\),则答案 \(k=\lceil\frac{|sum|}{m}\rceil\)
显然上述都是答案下界,下面给出这两个结论的构造性证明:
对于第一个结论的第一部分是很显然的,我们考虑证明一下第二部分:
考虑相邻的位置 \(1,-1\),如果我们把他们合并成 \(0\),得到新数列长度 \(-1\) 并且总和还是 \(0\),然后可以继续操作 \(0\) 或者操作 \(1,-1\) 直接数列长度为 \(m\),然后直接选取每个单个的数就构造出了答案。
对于第二个结论,我们考虑分为 \(m\leq |sum|\) 和 \(m>|sum|\) 两个部分来讨论。
如果 \(m\leq |sum|\),那么原序列显然取遍 \([0,sum]\),我们可以从小到大取前缀和等于 \(k,2k,3k...sum\) 的这些断点分开,由于 \(k=\lceil\frac{|sum|}{m}\rceil\) 显然每一段权值 \(\leq k\) 并且段数正好为 \(m\)
如果 \(m>|sum|\),我们可以类似地合并 \(-1,1\) 让整个序列只有 \(m\) 个数,并且按照我们的方法可以让这 \(m\) 的个数的绝对值都小于等于 \(1\),所以直接选取单个数即可。
有了上面的结论我们可以按贪心的思路构造最小字典序的解,具体来说我们可以考虑找出当前所有合法的位置,然后选取其中 \(a\) 最小的,先考虑 \(sum\not=0\) 的做法,合法位置的充要条件可以考虑归纳法,就是只要后面还能满足数列的若干性质即可,设还剩 \(t\) 个端点,上一个端点是 \(i\),现在考虑的端点是 \(j\),那么条件是:
那么我们考虑分别解决这些条件并维护最小的 \(a\) 即可,观察到 \(b_{j+1}\) 出现了很多次,我们可以按权值分类,每一类权值都维护一个关于 \(a\) 递增的单调队列。每个增量一个端点的时候搜索 \([b_{i+1}-k,b_{i+1}+k]\) 这个区域之内的单调队列,并且同时判断第二个条件,选取最小的 \(a\) 即可,第三个条件在 \(t\) 减小的时候动态加入候选点即可。
对于 \(sum=0\) 的情况也需要用单调队列只不过更为简单,在此就不详细展开。看上去我们暴力搜索复杂度很高,因为只会搜索 \(m\) 次所以复杂度其实是 \(O(mk)=O(n)\) 的。
总结
字典序问题不必枚举一个之后再暴力检验,在可以在保证后面合法的条件下选取最优点。
在维护若干柿子的时候注意关注承接变量,可以让这个变量为纽带联系这些柿子。
本题的结论和证明也很有意思,总之在权值连续变化的数列问题中,可以把它看成函数然后考虑函数的取值特点,这时候连续的条件往往会产生许多强烈的性质,我道听途说这个是介值定理。
#include <cstdio>
const int M = 2000005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,d,tot,now,a[M],b[M],p[M],cnt[M];
struct node
{
int x,id;
node(int X=0,int I=0) : x(X) , id(I) {}
bool operator < (const node &b) const
{
return x<b.x;
}
}t[M],ans;
struct Q
{
int l,r;
Q(int x=1){l=x;r=x-1;}
void push(node x)
{
while(l<=r && x<t[r]) r--;
t[++r]=x;
}
void get()
{
while(l<=r && t[l].id<now) l++;
if(l<=r && t[l]<ans) ans=t[l];
}
}q[M];
int Abs(int x)
{
return x>0?x:-x;
}
void add(int x)
{
q[b[x+1]+n].push(node(a[x],x));
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
a[i]=read(),b[i]=(read()?1:-1);
for(int i=n;i>=1;i--)
b[i]+=b[i+1],cnt[b[i+1]+n]++,tot+=!b[i];
for(int i=0,s=1;i<=2*n;i++)
q[i]=Q(s),s+=cnt[i];
if(!b[1]) d=(tot<m);
else d=(Abs(b[1])-1)/m+1;
if(!d)
{
tot=0;now=1;
for(int i=1;i<=n;i++)
if(!b[i+1]) p[++tot]=i;
for(int i=1,j=1;i<m;i++)
{
while(j<=tot && tot-j>=m-i)//make sure enough 0-position
q[0].push(node(a[p[j]],p[j])),j++;
ans.x=M;q[0].get();now=ans.id+1;
printf("%d ",ans.x);
}
}
else
{
int r=now=1;
while(n-r>=m-1) add(r++);//vaild positions
while(m>1)
{
int sd=b[now]+n;ans.x=M;
for(int i=sd-d;i<=sd+d;i++)
if(Abs(i-n)<=d*(m-1)) q[i].get();
printf("%d ",ans.x);
now=ans.id+1;add(r++);m--;
}
}
printf("%d\n",a[n]);
}