B. New Year and the Treasure Geolocation
我们知道,存在一些排列p,因此对于所有ii,以下都成立:
(tx,ty)=(xpi+ai,ypi+bi)
总结一下,我们得到:
n⋅(tx,ty)=∑(xpi+ai,ypi+bi)=(∑(xpi+ai),∑(ypi+bi))=(∑xi+∑ai,∑yi+∑bi)
因此,我们可以分别求出所有的x,y,方尖碑和线索的坐标,并除以nn。这需要O(N)时间。
另一种解决方案:采用词典学上最小的方尖碑坐标。很明显,这需要与词典学上最大的线索配对。我们简单地求出O(n)和和中的最小值和最大值。
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
typedef pair<int,int> pii;
#define x first
#define y second
int main() {
int N; cin >> N;
vector<pii> O(N), T(N);
for (int i = 0; i < N; i++) cin >> O[i].x >> O[i].y;
for (int i = 0; i < N; i++) cin >> T[i].x >> T[i].y;
sort(O.begin(),O.end());
sort(T.begin(),T.end());
reverse(T.begin(),T.end());
vector<pii> Ans(N);
for (int i = 0; i < N; i++) Ans[i] = {O[i].x+T[i].x, O[i].y+T[i].y};
sort(Ans.begin(),Ans.end());
cout << Ans[0].x << ' ' << Ans[0].y << endl;
}
C New Year and the Sphere Transmission
为了方便起见,从所有值中减去11。固定kk的值。我们得到了所有A的 a kmodn值,从0到再次达到0。该值也可以写成ak-bn。根据贝佐特定理,当且仅当c 可被gcd(k,n)gcd(k,n)整除时,方程式ak-b*n=c具有 a和 b的整数解。
此外,C的所有可能值将在返回到00之前被访问。这是因为元素k/gcd(k,n)k/gcd(k,n)生成组zn/gcd(k,n)zn/gcd(k,n)。
因此,我们只能考虑kk的值除以nn。我们可以在O(N−√)O(N)中通过试验区找到它们。对于每一个问题,我们都可以通过求算术级数求出一个闭式解。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
int main() {
ll N; cin >> N;
vector<ll> ans;
for (ll i = 1; i*i <= N; ++i) {
if (N%i==0) {
ans.push_back(N*(i-1)/2 + i);
if (i*i!=N) {
ans.push_back(N*(N/i-1)/2 + N/i);
}
}
}
sort(ans.begin(),ans.end());
for (int i = 0; i < ans.size(); ++i) {
cout << ans[i] << " \n"[i==ans.size()-1];
}
}
D New Year and the Permutation Concatenation
有两种长度为n的子阵列:
*它们完全由一个排列组成。
*它们是一个排列的k k长后缀和下一个排列的n-k长前缀的串联。
有n!这样的子数组是第一类的,它们都有正确的和。
让我们来研究第二种类型。回想一下按词典编纂顺序查找下一个排列的算法。我们找到最长的后缀,按降序排列,其长度为k。我们将前面的元素x与小于xx的递减序列中最小的元素交换,并按递增顺序对后缀进行排序。长度n−k−1的前缀保持不变,但所有较长的正确前缀都不同,并且还会更改它们的和。
回到我们的问题,如果长度k k的后缀是递减的,那么下一个排列的长度 n−k的前缀与当前排列的相同长度的前缀的和不同,因此子数组的和不正确。相反,如果长度k k的后缀不是递减的,那么下一个排列的长度n−k 的前缀等于当前排列的前缀,其和为n*(n+1)/2
为了找到答案,我们必须减去所有正在减少的排列的后缀数。固定K有多少台?这很简单——我们可以自由选择第一个n-k元素,然后排列它们。其余的必须以特定的方式进行分类。因此,来自长度kk的后缀的坏子数组的数量等于n!/K!.
让自己相信,即使对于最后一个排列,这种方法也能正常工作,因为没有下一个排列可以连接其后缀。
答案是:
这可以用o(n)来计算,而不需要进行模块划分。
还有一个简单的重复计算同样的答案,由Arsijo发现:
d(n)=(d(n−1)+(n−1)!−1)⋅n
#include <iostream>
#include <vector>
using namespace std;
template <unsigned int N> class Field {
typedef unsigned int ui;
typedef unsigned long long ull;
inline ui pow(ui a, ui p){ui r=1,e=a;while(p){if(p&1){r=((ull)r*e)%N;}e=((ull)e*e)%N;p>>=1;}return r;}
inline ui inv(ui a){return pow(a,N-2);}
public:
inline Field(int x = 0) : v(x) {}
inline Field<N> pow(int p){return (*this)^p; }
inline Field<N> operator^(int p){return {(int)pow(v,(ui)p)};}
inline Field<N>&operator+=(const Field<N>&o) {if (v+o.v >= N) v += o.v - N; else v += o.v; return *this; }
inline Field<N>&operator-=(const Field<N>&o) {if (v<o.v) v -= o.v-N; else v-=o.v; return *this; }
inline Field<N>&operator*=(const Field<N>&o) {v=(ull)v*o.v % N; return *this; }
inline Field<N>&operator/=(const Field<N>&o) { return *this*=inv(o.v); }
inline Field<N> operator+(const Field<N>&o) const {Field<N>r{*this};return r+=o;}
inline Field<N> operator-(const Field<N>&o) const {Field<N>r{*this};return r-=o;}
inline Field<N> operator*(const Field<N>&o) const {Field<N>r{*this};return r*=o;}
inline Field<N> operator/(const Field<N>&o) const {Field<N>r{*this};return r/=o;}
inline Field<N> operator-() {if(v) return {(int)(N-v)}; else return {0};};
inline Field<N>& operator++() { ++v; if (v==N) v=0; return *this; }
inline Field<N> operator++(int) { Field<N>r{*this}; ++*this; return r; }
inline Field<N>& operator--() { --v; if (v==-1) v=N-1; return *this; }
inline Field<N> operator--(int) { Field<N>r{*this}; --*this; return r; }
inline bool operator==(const Field<N>&o) const { return o.v==v; }
inline bool operator!=(const Field<N>&o) const { return o.v!=v; }
inline explicit operator ui() const { return v; }
inline static vector<Field<N>>fact(int t){vector<Field<N>>F(t+1,1);for(int i=2;i<=t;++i){F[i]=F[i-1]*i;}return F;}
inline static vector<Field<N>>invfact(int t){vector<Field<N>>F(t+1,1);Field<N> X{1};for(int i=2;i<=t;++i){X=X*i;}F[t]=1/X;for(int i=t-1;i>=2;--i){F[i]=F[i+1]*(i+1);}return F;}
private: ui v;
};
template<unsigned int N>istream &operator>>(std::istream&is,Field<N>&f){unsigned int v;is>>v;f=v;return is;}
template<unsigned int N>ostream &operator<<(std::ostream&os,const Field<N>&f){return os<<(unsigned int)f;}
template<unsigned int N>Field<N> operator+(int i,const Field<N>&f){return Field<N>(i)+f;}
template<unsigned int N>Field<N> operator-(int i,const Field<N>&f){return Field<N>(i)-f;}
template<unsigned int N>Field<N> operator*(int i,const Field<N>&f){return Field<N>(i)*f;}
template<unsigned int N>Field<N> operator/(int i,const Field<N>&f){return Field<N>(i)/f;}
typedef Field<998244353> FF;
int main(int argc, char* argv[]) {
int n; cin >> n;
auto F = FF::fact(n);
auto I = FF::invfact(n);
FF ans = n * F[n];
for (int i = 1; i < n; ++i) ans -= F[n]*I[i];
cout << ans << endl;
}
E New Year and the Acquaintance Estimation
第一个观察是,使用握手引理,我们知道了an+1的奇偶性。
第二,在同一个奇偶校验的整数上,如果一个 an+1=x是一个可能的答案,而一个 an+1=y是另一个x<y x<y,那么每个x<z<y =满足xmod2=zmod2也是一个答案。因此,我们应该研究一些二进制搜索方法。
我们使用在语句中链接的ErdosGallai定理来确定序列是否是图形。如果不是这样,我们必须确定答案是太大还是太小。这取决于当a(n+1)不满足时,它是在不等式的左边还是右边。如果它在左边,这意味着它太大了——很明显,让它变大永远不会改变不平等的迹象。相反,如果一个a(n+1)在右边,它显然太小了。
也可能发生的情况是,对于一些kk,如果一个a(n+1)在左边,而对于另一些k,它在右边,那么这个不等式是错误的。显然没有解决办法。
简单地检查不等式需要O(n^2)时间,但我们也可以在O(n)中进行:对于左侧,我们需要前缀和;对于右侧,我们维护和,以及特定值发生的次数。最多kk的值之和可以保持在o(1)中。这就产生了O(nlog n)O(nlog_n)中的算法。
或者,我们可以使用Havel-Hakimi算法执行类似的二进制搜索,使用段树或多集,跟踪是否已经处理了an+1an+1,以找出答案是太小还是太大。这就产生了O(nlog2 n)O(nlog2 n)算法
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
#define MAXN 500000
int N;
int A[MAXN];
long long sum;
#define TOO_SMALL -1
#define OK 0
#define TOO_BIG 1
int is_score(int value) {
vector<int> C(N+1,0);
for (int i = 0; i < N; ++i) ++C[A[i]];
++C[value];
int less = 0;
long long left = 0, right = 0;
for (int k = 0, i = 0; k <= N; k++) {
int val = (i == k && (i == N || A[i] < value)) ? value : A[i++];
left += val;
--C[val];
right -= min(val, k);
less += C[k];
right += N-k-less;
if (left > right + (long long)(k+1)*k) {
return (i == k) ? TOO_BIG : TOO_SMALL;
}
}
return OK;
}
int main(int,char**) {
ios_base::sync_with_stdio(false);
scanf("%d", &N);
sum = 0;
for (int i = 0; i < N; i++) {
scanf("%d", A + i);
sum += A[i];
}
sort(A,A+N,greater<int>());
int parity = sum & 1;
int lo = 0, hi = (N - parity) / 2, lores = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (is_score(2*mid + parity) == TOO_SMALL) {
lo = mid + 1;
} else {
lores = mid;
hi = mid - 1;
}
}
lo = lores;
hi = (N - parity) / 2;
int hires = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (is_score(2*mid + parity) == TOO_BIG) {
hi = mid - 1;
} else {
hires = mid;
lo = mid + 1;
}
}
if (lores == -1 || hires == -1) printf("-1\n");
else {
for (int i = lores; i <= hires; ++i) printf("%d ", 2*i+parity);
printf("\n");
}
}
F New Year and the Mallard Expedition
我们从贪婪的解决方案开始。这意味着我们可以飞越熔岩,在水上游泳,在草地上行走,追踪时间和耐力。可能会发生两种类型的问题——要么我们没有足够的精力飞越熔岩,要么我们最后还有一些剩余的精力。
如果我们缺乏耐力,我们可以步行或游泳“在适当的地方”一段时间来获得它。这种“原地”运动可以通过简单地向后走或游泳半米,然后向前走半米来完成。这会增加1点耐力。在水上做更有效,因为我们在那里移动得更快,所以我们也可以在旅途中的第一次水上做。如果在我们飞过的熔岩之前没有水,我们就使用稍微贵一点的地面。
另一方面,如果我们最后有一些未使用的耐力,我们应该把以前的一些动作转换成飞行来节省时间。请注意,转换1米的移动需要2个耐力,而不是一个,因为我们消耗了1个耐力,并且获得的1个耐力更少。因为在草地上运动比较慢,我们更喜欢转换这种运动。然而,我们必须注意不要转换一个太早的步行段——我们不应该以这样一种方式修改旅程,我们在某个时刻耗尽了精力。因此,我们跟踪我们在旅途中穿过的草地的长度。考虑一些地形的末端。让我们走过的草地的总长度是g,让现在的耐力是s。我们可能永远不会转换超过g草,也不会超过s/2,因为如果不是,我们将在当前位置耗尽体力。我们只需将可转换的草g量降低到s/2。最后,我们将G步行转换为飞行,将S/2-G游泳转换为飞行以节省一些时间。
它在O(N)中工作。
#include <iostream>
#include <vector>
#include <string>
typedef long long ll;
using namespace std;
int main() {
int N; cin >> N;
vector<ll> L(N);
for (ll &l: L) cin >> l;
string T; cin >> T;
bool hadWater = false;
ll time = 0, stamina = 0, twiceGrass = 0;
for (int i = 0; i < N; ++i) {
if (T[i] == 'L') {
time += L[i];
stamina -= L[i];
if (stamina < 0) {
/* not enough stamina, walk or swim "in place" to gain it */
time -= stamina * (hadWater ? 3 : 5);
stamina = 0;
}
} else if (T[i] == 'W') {
hadWater = true;
stamina += L[i];
time += 3 * L[i];
} else {
stamina += L[i];
time += 5 * L[i];
twiceGrass += 2*L[i];
}
/* no more than stamina/2 of walking can be converted to flying to save time,
* otherwise there would not be enough stamina at this point */
twiceGrass = min(twiceGrass, stamina);
}
if (stamina > 0) {
// convert walking to flying
time -= (5-1) * twiceGrass/2;
// convert swimming to flying
time -= (3-1) * (stamina - twiceGrass)/2;
}
cout << time << endl;
}
G New Year and the Factorisation Collaboration
我们得到的大多数操作都是无用的,因为我们可以自己执行它们。但是,我们不能自己执行平方根,因此我们应该能够使用平方根Oracle提供的信息来找到分解。
首先,我们来解决两个素数积的任务。
随机均匀选择x,并指定z=x^2。让YY作为查询sqrt z的答案。
对于固定的xx,由于中国的余数定理,这个方程有四个解,其中两个解是 x和−x。如果不是这样,那么当p和q是互质的,那么p将其中一个因子除掉,而qq将另一个因子除掉。因此,一个因素可以计算为gcd(x-y,n)。因为我们随机选择了 x,所以交互程序返回x或−x的概率正好是12,并且预期需要 2个查询。
让我们来看看当有两个以上的主要因素时会发生什么。其中一部分将累积在 x+y中,另一部分将累积在 x-y中。通过多个查询收集所有这些值(同样,在使用n获取这些值的gcd之后)。当且仅当对于 i≠j,我们已经看到一个值t,使得gcd(t,pi* pj)=pj时,我们才能找到主因子pj 。这是因为我们可以将包含pipi的所有值的gcd作为一个因子,得到pi。
在知道素数因子之前,我们不知道哪些值包含素数因子,但我们只是计算检索值的所有子集的gcd。由于GCD是一个交换和关联运算,因此我们得到的所有值都必须是nn的因子,我们可以在o(f* 4^f)时间中执行此步骤,其中 f是n的素数因子。然后我们可以用初等性检查算法来检查哪些是素数。或者,我们可以贪婪地选择所有值中的最小值(除了1),这是迄今为止选择的所有值的互质性-这将始终是一个素数。
它仍然显示了什么是成功的可能性。我们需要每对素数至少分开一次。在一个查询中发生这种情况的概率是1/2。在Q查询过程中,我们有可能2-q仍然无法分离这两个素数。在所有素数对上取并集,得到误差概率4 10 14的上界。
实现说明:为了求平方根,我们使用中国余数定理,在各自的有限域中求平方根。素数的形式为4x+3,原因很简单——我们可以通过简单计算xp+12找到x模p的平方根。对于4x+1形式的素数,我们需要tonelli-shanks,它在经验上使用了大约5个模指数。由于查询的数量和素数因子以及数字的大小,交互人员突然无法适应时间限制。
import random
def isPrime(n):
"""
Miller-Rabin primality test.
A return value of False means n is certainly not prime. A return value of
True means n is very likely a prime.
"""
if n!=int(n):
return False
n=int(n)
#Miller-Rabin test for prime
if n==0 or n==1 or n==4 or n==6 or n==8 or n==9:
return False
if n==2 or n==3 or n==5 or n==7:
return True
s = 0
d = n-1
while d%2==0:
d>>=1
s+=1
assert(2**s * d == n-1)
def trial_composite(a):
if pow(a, d, n) == 1:
return False
for i in range(s):
if pow(a, 2**i * d, n) == n-1:
return False
return True
for i in range(20):#number of trials
a = random.randrange(2, n)
if trial_composite(a):
return False
return True
def gcd(x, y):
return x if y == 0 else gcd(y, x % y)
n = int(input())
divs = [n]
def split(parts):
global divs
divs = [gcd(d, p) for d in divs for p in parts if gcd(d, p) != 1]
while not all([isPrime(x) for x in divs]):
x = random.randint(0, n - 1)
g = gcd(n, x)
if gcd(n, x) != 1:
split([g, n // g])
continue
y = int(input('sqrt {}\n'.format(x * x % n)))
if x == y:
continue
a, b = abs(x - y), x + y
g = gcd(x, y)
split([a // g, b // g, g])
print('!', len(divs), ' '.join(str(d) for d in sorted(divs)))
H New Year and the Tricolore Recreation
如果您将行状态表示为一对(x,y),其中x是蓝色和白色令牌之间的距离,y是白色和红色之间的距离,我们可以看到每个玩家都有一个将 x减少kk的操作,以及第二个将y减少k的操作。换句话说,每个X和Y定义一个对称的游戏,并且所有的游戏都是独立的。这毕竟是一场公正的比赛!
一旦我们得到了每一堆的格兰迪数,我们就可以用斯普拉格-格兰迪定理来找到答案。如何找到Grundy数字?我们通过筛选找到素数和半素数的集合。为了找到桩号n的值,我们可以从n(即n−p,其中p是素数或半素数)中收集每个桩号的Grundy数,然后找到mex。这需要O(m^2 ),其中mm是两个标记(2*10^5)之间的最大距离,并且速度太慢。
为了改进这一点,我们可以使用位集,但这还不够。我们实际上需要更多的这些!当JJ状态转换到Grundy编号为I的状态时,在J th位置保持k位集g0,…,gmgmm,其中ii th位集包含true。
你是怎么做到的?有一个位集pp存储所有素数和半素数。对于每个II,对mex执行线性搜索,让grundy编号为jj。然后是带P«IP«I的JGJ。
这在O(n2/w)中工作。给定输入约束的最大grundy数小于100,因此我们可以安全地选择k=100。
#include <vector>
#include <stack>
#include <iostream>
#include <algorithm>
#include <bitset>
using namespace std;
typedef unsigned int ui;
typedef long long ll;
struct Sieve : public std::vector<bool> {
// ~10ns * n
explicit Sieve(ui n) : vector<bool>(n+1, true), n(n) {
at(0) = false;
if (n!=0) at(1) = false;
for (ui i = 2; i*i <= n; ++i) {
if (at(i)) for (int j = i*i; j <= n; j+=i) (*this)[j] = false;
}
}
vector<int> primes() const {
vector<int> ans;
for (int i=2; i<=n; ++i) if (at(i)) ans.push_back(i);
return ans;
}
private:
int n;
};
constexpr int M = 2e5;
auto P = Sieve{M}.primes();
int main() {
ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
vector<int> G(M, 0);
int Q = P.size();
for (int i = 0; i < Q; ++i) {
for (int j = i; j < Q; ++j) {
if (ll(P[i])*P[j] >= M) break;
P.push_back(P[i]*P[j]);
}
}
bitset<M> PB;
for (int p : P) PB[p] = true;
int N, F; cin >> N >> F;
PB[F] = false;
vector<bitset<M>> W(100);
W[0] = PB;
for (int i = 1; i < M; ++i) {
while (W[G[i]][i]) G[i]++;
W[G[i]] |= PB << i;
}
cerr << *max_element(G.begin(),G.end()) << endl;
int g = 0;
for (int i = 0; i < N; i++) {
int r,w,b;
cin >> r >> w >> b;
g ^= G[w-r-1];
g ^= G[b-w-1];
}
if (g == 0) {
cout << "Bob\nAlice\n";
} else {
cout << "Alice\nBob\n";
}
}