HNOI 2012-2013 题目选做

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\),那么它对答案的贡献是:

\[n-\sum_{i=1}^{k-1}a(i) \]

我们直接考虑计算总贡献:

\[\sum_{w=1}^{m^{k-1}}n-\sum_{i=1}^{k-1} a_w(i)=n\cdot m^{k-1}-\sum_{w=1}^{m^{k-1}}\sum_{i=1}^{k-1} a_w(i) \]

因为 \(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\),那么条件是:

\[\begin{cases} |b_{i+1}-b_{j+1}|\leq k\\ \frac{|b_{j+1}|}{t-1}\leq k\\ n-j\geq t-1 \end{cases} \]

那么我们考虑分别解决这些条件并维护最小的 \(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]);
}
posted @ 2022-01-04 20:39  C202044zxy  阅读(212)  评论(2编辑  收藏  举报