1. 程式人生 > 其它 >AtCoder Grand Contest 044

AtCoder Grand Contest 044

AtCoder Grand Contest 044

A - Pay to Win

不妨將操作倒過來考慮,問題就變成了每次除以 \(2,3,5\) 或者 \(+1,-1\),令 \(f_n\) 表示將 \(n\) 變成 \(0\) 的最小花費,然後記憶化搜尋即可,可以證明覆雜度是對的。


程式碼:

#include<iostream>
#include<cstdio>
#include<map>
using namespace std;
int T;
long long n;
int a,b,c,d;
map<long long,long long>f;
long long dfs(long long n)
{
	if(n==0) return 0;
	if(n==1) return d;
	if(f[n]) return f[n];
	long long x=n/2*2,y=(n+1)/2*2;
	f[n]=min(dfs(x/2)+1LL*(n-x)*d+a,dfs(y/2)+1LL*(y-n)*d+a);
	x=n/3*3,y=(n+2)/3*3;
	f[n]=min(f[n],min(dfs(x/3)+1LL*(n-x)*d+b,dfs(y/3)+1LL*(y-n)*d+b));
	x=n/5*5,y=(n+4)/5*5;
	f[n]=min(f[n],min(dfs(x/5)+1LL*(n-x)*d+c,dfs(y/5)+1LL*(y-n)*d+c));
	f[n]=min((__int128)f[n],(__int128)n*d);
	return f[n];
}
void solve()
{
	scanf("%lld%d%d%d%d",&n,&a,&b,&c,&d);
	f.clear();
	printf("%lld\n",dfs(n));
	return;
}
int main()
{
	scanf("%d",&T);
	while(T--)
		solve();
	return 0;
}

B - Joker

當位於 \((x,y)\) 離開時我們將這個點的最短路加進答案裡,將這個點標記為離開,然後用這個點去暴力更新每個點的最短路。可以發現最短路的權值之和為 \(O(n^3)\) 的級別,每次至少使得最短路 \(-1\),時間複雜度為 \(O(n^3)\)


程式碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=505;
const int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
int n;
int p[N*N];
pair<int,int> pos[N*N];
int dis[N][N];
bool vis[N][N];
void dfs(int x,int y)
{
	for(int i=0;i<4;i++)
	{
		int tx=x+dir[i][0],ty=y+dir[i][1];
		if(tx<0||tx>n||ty<0||ty>n) continue;
		int d=dis[x][y];
		if(!vis[tx][ty]) d++;
		if(dis[tx][ty]>d)
		{
			dis[tx][ty]=d;
			dfs(tx,ty);
		}
	}
	return;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n*n;i++)
	{
		scanf("%d",&p[i]);
		pos[i]=make_pair((p[i]-1)/n+1,p[i]%n==0?n:p[i]%n);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			dis[i][j]=min({j-1,n-j,i-1,n-i});
	long long ans=0;
	for(int i=1;i<=n*n;i++)
	{
		int x=pos[i].first,y=pos[i].second;
		vis[x][y]=true;
		ans+=dis[x][y];
		dis[x][y]--;
		for(int i=0;i<4;i++)
		{
			int tx=x+dir[i][0],ty=y+dir[i][1];
			dis[x][y]=min(dis[x][y],dis[tx][ty]);
		}
		dfs(x,y);
	}
	printf("%lld",ans);
	return 0;
}

C - Strange Dance

從高位到低位插入 Trie 很難處理,考慮從低位到高位插入 Trie,每個葉子節點記錄一下當前的數為多少。S 操作就可以在節點上打上翻轉標記。R 操作可以將子樹 \(0\to 1,1\to 2,2\to 0\) 其中 \(2\) 中可能會對下一位進位,遞迴下去就好了,最後將每個點的位置在 Trie 上找出來就是了。


程式碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int M=15,N=600005;
int n,m,Q;
char s[N];
int Pw[M];
struct Trie
{
	struct Node
	{
		int ch[3];
		int val;
		int tag;
		Node()
		{
			ch[0]=ch[1]=ch[2]=0;
			val=-1;
			tag=0;
			return;
		}
	}trie[N*3];
	int tot=1;
	void insert(int x)
	{
		vector<int>v;
		int t=x;
		while(t)
			v.push_back(t%3),t/=3;
		while(v.size()<m)
			v.push_back(0);
		int u=1;
		for(int c:v)
		{
			if(!trie[u].ch[c]) trie[u].ch[c]=++tot;
			u=trie[u].ch[c];
		}
		trie[u].val=x;
		return;
	}
	void push_down(int u)
	{
		if(!trie[u].tag) return;
		swap(trie[u].ch[1],trie[u].ch[2]);
		for(int i=0;i<3;i++)
			trie[trie[u].ch[i]].tag^=1;
		trie[u].tag=0;
		return;
	}
	void reverse(int u)
	{
		trie[u].tag^=1;
		return;
	}
	void add(int u)
	{
		if(!u) return;
		push_down(u);
		int t=trie[u].ch[2];
		trie[u].ch[2]=trie[u].ch[1];
		trie[u].ch[1]=trie[u].ch[0];
		trie[u].ch[0]=t;
		add(trie[u].ch[0]);
		return;
	}
	void query(int u,int sum,int dep,vector<int>&res)
	{
		if(!u) return;
		push_down(u);
		if(trie[u].val!=-1) res[trie[u].val]=sum;
		for(int i=0;i<3;i++)
			query(trie[u].ch[i],sum+i*Pw[dep],dep+1,res);
		return;
	}
}T;
int main()
{
	scanf("%d",&m);
	Pw[0]=1;
	for(int i=1;i<=m;i++)
		Pw[i]=Pw[i-1]*3;
	n=Pw[m];
	scanf("%s",s+1);
	Q=strlen(s+1);
	for(int i=0;i<n;i++)
		T.insert(i);
	for(int i=1;i<=Q;i++)
		if(s[i]=='S') T.reverse(1);
		else if(s[i]=='R') T.add(1);
	vector<int>res;
	res.resize(n);
	T.query(1,0,0,res);
	for(int u:res)
		printf("%d ",u);
	return 0;
}

D - Guess the Password

我們可以用構造出一個長度為 \(L\),字元為 \(c\) 的字串,詢問之後就可以得出每個字元在串中的出現次數 \(cnt_c\),將他們的和相加即可得到字串的長度 \(len\)

可以發現,我們可以用一次操作判斷一個串是否是原串的子序列

考慮如果字符集只有 \(0,1\) 的時候怎麼做。考慮從左到右確定每個位置的字元。考慮判斷當前位是否為 \(0\),我們可以構造一個 \(011\ldots 1\) 將目前可以填的所有 \(1\) 填成一個串,在前面加入一個 \(0\)。如果這是原串的一個子序列,則當前位為 \(0\),否則當前位為 \(1\)

仔細思考上面的過程,可以發現同樣適合於兩個無交的字元集合的情況。那麼就可以得出了,用歸併的方法,每次將兩個字元集合併成原串的一個子序列,詢問次數為 \(O(L\log 62)\)


程式碼:

#include<iostream>
#include<cstdio>
using namespace std;
const int L=128,N=65;
const char ch[]={"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"};
int len;
int cnt[N];
int query(const string &s)
{
	cout<<"? "<<s<<endl; 
	int ans;
	cin>>ans;
	return ans;
}
bool check(const string &s)
{
	return query(s)==len-s.size();
}
string merge(int l,int r)
{
	if(l==r) return string(cnt[l],ch[l]);
	int mid=(l+r)/2;
	string a=merge(l,mid),b=merge(mid+1,r);
	string t;
	int i=0,j=0;
	while(i<a.size()&&j<b.size())
	{
		if(check(t+a[i]+b.substr(j))) t+=a[i],i++;
		else t+=b[j],j++;
	}
	while(i<a.size())
		t+=a[i],i++;
	while(j<b.size())
		t+=b[j],j++;
	return t;
}
int main()
{
	for(int i=0;i<62;i++)
		cnt[i]=L-query(string(L,ch[i])),len+=cnt[i];
	string ans=merge(0,62);
	cout<<"! "<<ans<<endl;
	return 0;
}

E - Random Pawn

可以發現,可以在最大值那裡肯定會停止,我們可以將環斷成兩段,問題變成了序列上的問題。

考慮如果沒有 \(b_i\) 的代價的時候怎麼做。可以發現一個結論:

在長度為 \(L\) 的數軸上的位置 \(x\) 處,每次可以等概率的向左或者向右走一步,只有當到達 \(0,L\) 的時候才停止,則到達 \(L\) 停止的概率為\(\frac {x}{L}\),到達 \(0\) 停止的概率為 \(\frac {L-x}{L}\)

證明:

\(f_i\) 表示當前位於 \(i\) 到達 \(L\) 的概率,則 \(f_0=0,f_L=1\),可以得出 \(f_i=\frac{f_{i-1}+f_{i+1}}{2}\),那麼解出的 \(f_i=\frac{i}{L}\),原命題得證。

我們稱一個點為停止點當且僅當這個點移動的收益比當前點停止的收益低。

可以發現,如果當前點為 \(i\),移動的期望收益一定是由 \(i\) 前面第一個停止點 \(a\) 和後面第一個停止點 \(b\) 貢獻的。到達 \(a\) 停止的概率為\(\frac {b-i}{b-a}\),到達 \(b\) 停止的概率為 \(\frac {i-a}{b-a}\),那麼 \(i\) 移動的期望收益為 \(A_a\cdot \frac {b-i}{b-a}+A_b\cdot \frac {i-a}{b-a}\)

考慮如何求出所有的停止點,令第 \(i\) 個點的座標為 \((i,A_i)\),可以發現,第 \(i\) 個點移動的期望即為 \(x=i\)\(a-b\) 這條線段的 \(y\) 值,停止點的條件即為 \(A_i\) 需要比這條線段高。那麼停止點顯然在一個凸包內,維護一個上凸殼即可。

考慮如果 \(b_i\) 的代價的時候,式子就變成了 \(f_i=\max(A_i, \frac{f_{i-1}+f_{i+1}}{2}-b_i)\),我們需要將它變換成 \(g_i=\max(A_i',\frac{g_{i-1}+g_{i+1}}{2})\) 的形式,令 \(c_i=f_i-g_i\),那麼

\[\begin{aligned}g_i&=f_i-c_i \\ &=\max(A_i-c_i, \frac{f_{i-1}+f_{i+1}}{2}-b_i-c_i) \\ &=\max(A_i-c_i, \frac{g_{i-1}+c_{i-1}+g_{i+1}+c_{i+1}}{2}-b_i-c_i) \\ &=\max(A_i-c_i,\frac{g_{i-1}+g_{i+1}}{2}+\frac{c_{i-1}+c_{i+1}}{2}-b_i-c_i)\end{aligned} \]

我們可以構造出一種 \(c_i\),使得 \(\frac{c_{i-1}+c_{i+1}}{2}-b_i-c_i=0\),令 \(A_i'=A_i-c_i\),則式子變成了 \(g_i=\max(A_i',\frac{g_{i-1}+g_{i+1}}{2})\) 的形式,維護一個上凸殼即可。


程式碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=200005;
int n;
long long A[N];
int b[N];
long long c[N];
long long a[N];
struct Point
{
	int x;
	long long y;
};
double slope(Point a,Point b)
{
	return (double)(b.y-a.y)/(b.x-a.x);
}
Point p[N];
Point s[N];
int top;
int pos[N];
bool book[N];
long long ans[N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&A[i]);
	for(int i=1;i<=n;i++)
		scanf("%d",&b[i]);
	int Max=max_element(A+1,A+n+1)-A;
	rotate(A+1,A+Max,A+n+1);
	rotate(b+1,b+Max,b+n+1);
	n++;
	A[n]=A[1],b[n]=b[1];
	c[1]=c[2]=0;
	for(int i=2;i<=n;i++)
		c[i+1]=2*b[i]+2*c[i]-c[i-1];
	for(int i=1;i<=n;i++)
		a[i]=A[i]-c[i];
	for(int i=1;i<=n;i++)
	{
		p[i]=(Point){i,a[i]};
		while(top>=2&&slope(s[top-1],s[top])<=slope(s[top-1],p[i])) top--;
		s[++top]=p[i];
	}
	for(int i=1;i<=top;i++)
		pos[i]=s[i].x,book[pos[i]]=true;
	long double ans=0;
	for(int i=2;i<=n;i++)
		ans+=c[i];
	for(int i=2;i<=n;i++)
		if(book[i]) ans+=p[i].y;
		else
		{
			int a=lower_bound(pos+1,pos+top+1,i)-pos-1,b=upper_bound(pos+1,pos+top+1,i)-pos;
			a=pos[a],b=pos[b];
			ans+=(long double)(p[a].y*(b-i)+p[b].y*(i-a))/(b-a);
		}
	printf("%.12Lf",ans/(n-1));
	return 0;
}

F - Name-Preserving Clubs

考慮建一個 \(k\times n\)\(01\) 矩陣,每個位置表示這個集合中是否包含這個元素。

考慮任意兩個集合都不同的情況。

稱一個矩陣是好的,當且僅當任意打亂它的列,不存在一種方式使得打亂行和最初的矩陣相同。可以發現,一個好的矩陣的條件任意兩列都不同。

假設一個矩陣 \(A\) 是好的,可以注意到下面兩個性質:

  • \(A\) 的轉置 \(A^T\) 是好的。
  • 考慮 \(2^k\) 種不同的列,由其中所有不存在於 \(A\) 的列構成的矩陣 \(A^C\) 也是好的。

注意到行列操作是獨立的,因此先打亂行,再打亂列是等價的。

根據上面的討論,我們可以得出以下結論:

\(c(k,n)\) 表示本質不同的 \(k\times n\) 的好的矩陣的數量,那麼有 \(c(k,n)=c(n,k),c(k,n)=c(k,2^k-n)\)

\(g(n)\) 表示最小的 \(k\) 使得 \(c(k,n)> 0\),那麼有:

\(2^{g(n)}-n\ge g(g(n))\)

證明:

\(c(g(n),n)=c(g(n),2^{g(n)}−n)=c(2^{g(n)}−n,g(n))> 0\)。則根據 \(g\) 的定義有 \(2^{g(n)}-n\ge g(g(n))\)

\(G(n)\) 滿足 \(G(1)=0\)\(G(n)\) 是最小的 \(k\) 滿足 \(2^k-n\ge G(k)\)

引理 1:\(G(i)< i\)

證明:

考慮證明 \(2^{i-1}-i\ge G(i-1)\)

  • 對於 \(i=1\) 顯然;
  • 對於 \(i> 1\) 的情況,\(G(i-1)\leq i-2\leq 2^{i-1}-i\),得證。

引理 2:對於 \(n> 1\),有 \(G(i)\ge G(i−1)\)

證明:

因為滿足 \(2^{G(i)}-G(i)\ge i\)\(G(i)\) 必然滿足 \(2^{G(i)}-G(i)\ge i-1\),故 \(G(i)\ge G(i−1)\)

引理 3:對於 \(n> 1\)\(0\leq G(i)-G(i-1)\leq 1\)

證明:

  • \(n=2,3\) 時顯然;
  • 對於 \(i> 3\) 的情況,\(2^{G(i−1)}-(i-1)\ge G(G(i−1))\)\(2^{G(i-1)}\ge 2\),所以有 \(2^{G(i - 1) + 1} - i \ge 2^{G(i - 1)} + 1 - (i - 1) \ge G(G(i - 1)) + 1 \ge G(G(i - 1) + 1)\)

引理 4:對於 \(k\ge G(n)\),有 \(2^k-n\ge G(k)\)

證明:

  • \(k=G(n)\) 顯然;
  • 對於 \(k> G(n)\) 的情況,有 \(2^k-n\ge 2^{k−1}+1−n\ge G(k−1)+1\ge G(k)\)

引理 5:當且僅當 \(G(n)\leq k\leq 2^n−G(n)\) 時,\(c(k,n)> 0\)

證明:

考慮證明充分性。

\(k=1\) 顯然;

考慮 \(k> 1\) 的情況。

  • \(G(n)-k< n\),不妨令 \(k> n\)

\(k\ge G(n)\) 可得 \(2^k−n\ge G(k)\),所以有 \(n\leq 2^n-G(k)\)

\(k\le 2^n- G(n)\) 可得 \(2^n−k\ge G(n)\),故 \(n\geq G(k)\),得證。

  • \(k> 2^{n−1}\)

考慮證明 \(G(n)-2n−k\)。又 \(2^n−G(n)\ge k\) 得證。

  • \(k< 2^{n−1}\) 時同理。

  • \(n \leq k \leq 2^{n - 1}\)

考慮構造一個集合 \(\{(1),(1,2),(2,3),\cdots ,(n−1,n)\}\)。剩下的填 \(k−n\) 個大小 \(\ge 3\) 的集合。

可以得出 \(n=2,3\) 合法;當 \(n\ge 4\) 的時候,大小 \(\ge 3\) 的集合的個數大於一半,得證。

引理 4:當 \(6 \leq k \leq n \leq 2^{k - 1}\) 時,$ c(k,n)> 1000$

考慮先構造一個大小為 \(k−2\) 的集合,包含 \(\{(1,2),(2,3),\cdots ,(n−1,n)\}\),然後將其中一個或者其中 \(k−3\) 個取補集。剩下的 \(n−k+2\) 個集合任意放大小不為 \(2\) 或者 \(k−2\) 的集合。

結論是 \(n=4\) 的時候答案加上 \(1\)\(n=7\) 的時候答案加上 \(2\).

剩下的情況就是 \(k\leq 5,n\leq 2^{k−1}\),暴搜後打表即可。


#include<iostream>
#include<cstdio>
using namespace std;
const int res[6][20]={{},
{1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,4,6,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,36,108,220,334,384,0,0,0,0,0,0,0,0},
{0,0,0,0,0,976,1001,1001,1001,1001,1001,1001,1001,1001,1001,1001,1001}};
const int N=1005;
int G[N];
void init()
{
	G[1]=0;
	for(int n=2;n<=1000;n++)
	{
		for(int k=1;k<n;k++)
			if((1LL<<k)>=n&&((1LL<<k)-n)>=G[k])
			{
				G[n]=k;
				break;
			}
		if(G[n]==0) G[n]=-1;
	}
	return;
}
int g(long long n)
{
	if(n<=1000) return G[n];
	for(int k=1;;k++)
		if((1LL<<k)>=n&&((1LL<<k)-n)>=g(k)) return k;
	return -1;
}
int solve(long long n,long long k)
{
	if(n<k) swap(n,k);
	if(k<63&&(1LL<<k)-n<n) return solve((1LL<<k)-n,k);
	if(k>5) return 1001;
	if(k==0) return 1;
	return res[k][n];
}
long long n;
int main()
{
	init();
	scanf("%lld",&n);
	int ans=solve(n,g(n));
	if(n==4) ans++;
	if(n==7) ans+=2;
	if(ans>1000) printf("-1");
	else printf("%d",ans);
	return 0;
}