Good Bye 2018 题解

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";
    }
}

https://codeforces.com/blog/entry/64196

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值