2021牛客多校第2场 G J K
写在前面:
F 题,简单计算几何题。需要的知识:球的方程,球冠的体积,球之间的位置关系。几何相关知识我一窍不通,有必要学习一下。
I 题,常规搜索题,出题人说是bfs,但我用IDA*做的。
G League of Legends(https://ac.nowcoder.com/acm/contest/11253/G)
标签:DP优化,单调队列
题意:校队的\(n\)位成员的空闲时间可以表示成\([l_i,r_i)\)的一段区间,现在要将他们分成\(k\)组,每个人属于且仅属于其中一组,每组的时间是此组中所有成员的公共时间,要求每组的时间不为\(0\),求各组的时间之和的最大值。若无解,输出\(0\)。
\(1 \le k \le n \le 5000, \ 0 \le a < b \le 10^5\)
解法:
如果存在两个人\(i,j\),满足\(i\)包含\(j\),即\(l_i \le l_j \le r_j \le r_i\),那么如果将\(i\)和\(j\)放到一组,这组的时间将由\(j\)决定,而与\(i\)无关;若将\(i\)放到另一组,\(i\)只会将另一组的时间压缩,不会有任何好处,还不如和\(j\)同组。因此,如果\(i\)和\(j\)不同组,那么\(i\)一定单独为一组。
我们可以先将所有包含其他人的人先剔除掉,这样剩下的人,满足若\(l_i < l_j\),一定有\(r_i < r_j\),且一定有\(l_i \neq l_j\)。那么,按pair的排序方法排序后,放到同一组的人的下标一定是连续的一段。这个性质使得dp成为可能。
令\(f[i][j]\)为前\(i\)个人分为\(j\)组的最大时间,那么转移过程为 \(f[k][j-1]+l_{k+1}-r_i \to f[i][j]\),其中\(l_{k+1}>r_i\)。可以考虑单调队列,使得dp的时间复杂度为\(O(NK)\)
现在考虑那些被剔除掉的人。若将这些人中的\(i\)个单独分组,那么一定是选择空闲时间最大的\(i\)个人,设他们时间之和为\(sum[i]\),那么总时间为\(f[][k-i]+sum[i]\)。这部分用前缀和即可\(O(N)\)处理。
这道题经过我近一个小时的思考,已经把dp的式子列了出来,实际上我忽略了题目中每组时间不为0的条件,因此进行了更细致的讨论。我已经发现了剔除掉那些人后剩下的人的性质,但是没想到一组的时间可以用\(l_i-r_j\)这么简单的式子表示,因此卡在了dp的优化上,最终没能做出来这道题。反思自己,应该是没有足够的经验,没有足够的自信,还有自己的急躁使得没有耐心想下去了。果然还是需要保持良好的精神,耐心地思考啊。
#include<bits/stdc++.h>
using namespace std;
#define fst first
#define sed second
typedef pair<int,int> PII;
typedef long long LL;
const int N=5010, K=5010, A=100010;
const LL INF=1e10;
int n, k;
PII a[N], c[N];
LL e[N], sum[N];
bool v[N];
LL ans;
LL f[N][K];
LL q[N], qf, qt;
bool cmp(const PII &a, const PII &b){
if(a.fst!=b.fst) return a.fst>b.fst;
return a.sed<b.sed;
}
int main(){
cin>>n>>k;
for(int i=1; i<=n; ++i) scanf("%d%d", &a[i].first, &a[i].second);
//全部组时间不为0
//只保留不包含其他区间的
int minn=A;
sort(a+1,a+n+1,cmp);
for(int i=1, x; i<=n; ++i){
if(a[i].sed>=minn) v[i]=true;
else minn=a[i].sed;
}
int cn=0, en=0;
for(int i=1; i<=n; ++i){
if(!v[i]) c[++cn]=a[i];
else e[++en]=a[i].sed-a[i].fst;
}
sort(e+1,e+en+1);
for(int i=1; i<=n || i<=k; ++i) sum[i]=-INF;
for(int i=1; i<k && en-i+1>0; ++i) sum[i]=sum[i-1]+e[en-i+1];
for(int i=1, j=cn; i<j; ++i, --j) swap(c[i],c[j]);
//dp
for(int i=1; i<=cn; ++i) for(int j=1; j<=k; ++j) f[i][j]=-INF;
for(int i=1; i<=cn; ++i) if(c[1].sed>c[i].fst) f[i][1]=c[1].sed-c[i].fst;
auto ff=[](int x, int j)->LL{return f[x][j]+c[x+1].sed;};
for(int j=2; j<=k && j<=cn; ++j){
qf=qt=1;
q[qt++]=j-1;
for(int i=j; i<=cn; ++i){
while(qf<qt && c[q[qf]+1].sed<=c[i].fst) qf++;
if(qf<qt) f[i][j]=max(f[i][j], ff(q[qf],j-1)-c[i].fst);
while(qf<qt && ff(q[qt-1],j-1)<=ff(i,j-1)) qt--;
q[qt++]=i;
}
}
//output
for(int i=1; i<=k&&i<=cn; ++i){
if(f[cn][i]>0) ans=max(ans, f[cn][i]+sum[k-i] );
}
printf("%lld\n", max(0ll,ans));
}
J Product of GCDs(https://ac.nowcoder.com/acm/contest/11253/J)
标签:数学,质因数分解,快速幂,组合数
题意:给定一个含有\(n\)个元素的可重复集合\(S\),对于所有包含\(k\)个元素的子集\(T\),求gcd(\(T\))之积,结果对\(p\)取模。gcd(\(T\))是指\(T\)中所有数的最大公因数。一共\(t\)组数据。
\(1 \le n \le 40000, 1 \le S_i \le 80000, 1 \le k \le 30, 10^6 \le p \le 10^{14} , 1\le t \le 60\)
解法:
我不是按照题解做的。
容易想到质因数分解,对每个质数分别求解。对于一个质数,处理出它在集合中每个数中出现了几次。用\(v[i][j]\)表示第\(i\)个质数在\(S_j\)里出现了几次,之后将\(V[i]\)排序,扫描一遍,运用组合数即可求出答案。
上述方法可行,但是运行太慢,用前缀和等优化后可达到要求。
写代码时遇到的问题:
质因数分解:一是\(O(\sqrt n)\)枚举因数,时间上会慢一点,但是代码短;二是借用pollard rho算法,代码很长,但是速度会快一些。这道题需要分解的数要不就是很小\((S_i<=80000)\),要不就是很少\((p<10^{16}, 但是T\le 60)\),因此用前者最合适。
求\(\varphi(p)\):找到\(p\)所有的质因数\(a\),全部都进行 \(p=p/a*(a-1)\)运算
快速幂:由于\(p\)不能保证是质数,因此要用到公式 \(a^b\%p=a^{b\%\varphi(p)+\varphi(p)}\%p\)
大整数乘法:对于\(c=a*b\)的运算,由于\(a\)和\(b\)范围是\(<10^{14}\),直接乘会爆long long,因此可以用__int128做中间处理。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef __int128 I128;
const int P=1e7+10, X=80010, N=40010, PRI=700000, K=35;
int read(){
int s=0, w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') w=-1; ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0', ch=getchar();
return s*w;
}
//prime
bool not_prime[P];
int prime[PRI], pn;
int mp[P];
LL phi;
void getPrime(int n){
not_prime[1]=true;
for(int i=2; i<=n; ++i){
if(!not_prime[i]) prime[pn++]=i;
for(int j=0; j<pn && i*1ll*prime[j]<=n; ++j){
not_prime[i*prime[j]]=true;
if(i%prime[j]==0) break;
}
}
}
//main
vector<pair<int,int> > V[X];
int v[80000][20];
int n, k; LL p;
int a[N];
bool big[N][K];
LL c[N][K];
LL sum[N];
void factorNum(){
pair<int,int> tmp;
for(int x=2; x<X; ++x){
int t=x;
for(int i=2; i*1ll*i<=t; ++i){
if(t%i==0){
tmp.first=i; tmp.second=0;
do{
tmp.second++;
t/=i;
}while(t%i==0);
V[x].push_back(tmp);
}
}
if(t>1) V[x].push_back(pair<int,int>(t,1));
}
}
void Factor(int x){
for(auto&i:V[x]){
v[mp[i.first]][i.second]++;
v[mp[i.first]][0]++;
}
}
I128 Pow(int x, I128 nn){
if(nn>=phi) nn=nn%phi+phi;
I128 res=1, tmp=x;
LL n=nn;
while(n){
if(n&1ll) res=res*tmp%p;
n>>=1ll;
tmp=tmp*tmp%p;
}
return res;
}
LL calP(LL x){
LL res=x;
for(int i=0; i<pn && prime[i]*1ll*prime[i]<=x; ++i){
if(x%prime[i]==0){
res=res/prime[i]*(prime[i]-1);
while(x%prime[i]==0) x/=prime[i];
}
}
if(x>1) res=res/x*(x-1);
return res;
}
int main(){
//init
getPrime(P);
factorNum();
for(int i=0; i<pn; ++i){
mp[prime[i]]=i;
}
int t; cin>>t;
while(t--){
scanf("%d%d%lld", &n, &k, &p);
phi=calP(p);
for(int i=0; i<80000; ++i) for(int j=0; j<19; ++j) v[i][j]=0;
LL ans=1;
for(int i=1; i<=n+1; ++i){
for(int j=1; j<=k+1; ++j){
c[i][j]=0;
big[i][j]=false;
}
}
//start
for(int i=1; i<=n; ++i){
a[i]=read();
Factor(a[i]);
}
for(int i=0; i<=n; ++i){
c[i][0]=1;
for(int j=1; j<=k && j<=i; ++j){
c[i][j]=c[i-1][j]+c[i-1][j-1];
big[i][j]=big[i-1][j]|big[i-1][j-1];
c[i][j]>=phi && (c[i][j]-=phi, big[i][j]=true);
}
sum[i]=c[i][k-1]+big[i][k-1]*phi;
}
for(int i=1; i<=n; ++i) sum[i]+=sum[i-1];
//solve
I128 e=1;
for(int i=0, n1, n2, j; i<8000; ++i){
int sz=v[i][0];
if(sz<k) continue;
n1=0; n2=0;
for(j=18; j>=1; --j){
n2=n1+v[i][j];
if(n1+v[i][j]>=k){
n1=k-1;
break;
}
n1=n2;
}
for(; j>=1; --j){
ans=ans* Pow(prime[i], e*j*(sum[n2-1]-sum[n1-1]))%p;
n1=n2;
n2=n1+v[i][j-1];
}
}
printf("%lld\n", ans);
}
}
K Stack(https://ac.nowcoder.com/acm/contest/11253/K)
标签:构造
题意:原本有一个数组\(a\),将\(a_1\)到\(a_n\)依次添加到单调栈中,记录每次添加后的单调栈大小为\(b\)数组。现在\(a\)数组不见了,\(b\)数组只剩下了\(k\)个位置和对应的值。问能否构造一个\(a\)数组,如果能,输出一个方案。
\(1 \le k \le n \le 10^6\)
解法:
首先应想到一个贪心的结论:\(b[i+1]\)比\(b[i]\)至多大\(1\),因此当\(b[i+1]\)未知时,最优方案是令\(b[i+1]=b[i]+1\)。这样我们就得到了完整的\(b\)数组。
构造的方法有很多,这里说两种:
方法一:拓扑。
老套路了。若\(a[i]\)比\(a[j]\)大,就连一条边\((i \to j)\)。最后求一下拓扑序就好了。
方法二:模拟?
整个\(b\)数组中的最后一个\(1\)的位置(记为\(w_1\))是当前\(a\)数组中最小的,也就是\(a[w_1]=1\);将\(b[w_1]\)去掉,将\(b[w_1+1]\)到\(b[n]\)都减去\(1\),再次找到最后一个\(1\)的位置(记为\(w_2\))是当前次小的,也就是\(a[w_2]=2\)......以此类推
可以用递归实现。
这里附上我用的第二种方法的代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n, k;
int a[N], b[N];
vector<int> v[N];
void solve(int l, int r, int val, int base){
if(l>r) return;
int p=lower_bound(v[val].begin(),v[val].end(),l)-v[val].begin();
int q=upper_bound(v[val].begin(),v[val].end(),r)-v[val].begin()-1;
int last=l, sum=r-l+1;
for(int i=p; i<q; ++i){
a[v[val][i]]=base+r-v[val][i+1]+1+1;
solve(v[val][i]+1, v[val][i+1]-1, val+1, a[v[val][i]]);
}
a[v[val][q]]=base+1;
solve(v[val][q]+1,r,val+1,a[v[val][q]]);
}
int main(){
cin>>n>>k;
for(int i=1, w, x; i<=k; ++i){
scanf("%d%d", &w, &x);
b[w]=x;
}
bool flag=true;
for(int i=1; i<=n&&flag; ++i){
if(!b[i]){
b[i]=b[i-1]+1;
}else{
if(b[i]-b[i-1]>1) flag=false;
}
v[b[i]].push_back(i);
}
if(!flag){
puts("-1");
return 0;
}
solve(1,n,1,0);
for(int i=1; i<=n; ++i){
printf("%d ", b[i]);
}puts("");
for(int i=1; i<=n; ++i){
printf("%d ", a[i]);
}
}