CCF-NOIP-2018 提高組(複賽) 模擬試題(四)
T1 貪吃蛇
【問題描述】
貪吃蛇是一個好玩的遊戲。在本題中,你需要對這個遊戲進行模擬。
這個遊戲在一個 \(n\) 行 \(m\) 列的二維棋盤上進行。 我們用 \((x, y)\) 來表示第 \(x\) 行第 \(y\) 列的格子,那麼左上角為 \((1, 1)\),右下角為 \((n, m)\)。
我們用一個長度為 \(k\) 的不重複的座標的序列(形如 \((x_1,y_1 )\), \((x_2 , y_2 )\), ... , \((x_k , y_k )\))來表示一條長度為 \(k\) 的蛇,其中 \((x_1 , y_1 )\) 稱為蛇的頭部。在遊戲的任何時刻,都滿足 \(k > 1\)
遊戲開始時,蛇的長度為 \(1\),座標為 \((x_s , y_s )\)。接下來會進行個操作,每個操作是以下兩種型別之
\(l\) \(1\) \(d:\) 蛇的頭部往 \(d\) 方向伸長一格, 其中 \(d\) 為 \(U D L R\) 之一, 分別表示 “上” 、“下” 、 “左” 、 “右” 。
\(l\) \(2:\) 蛇的尾部縮短一格,保證該操作前蛇的長度大於 \(1\)。
棋盤上還有 \(t (0 ≤ t < nm)\) 個障礙物, 分別位於 \((u_i , v_i )\) \((1 ≤ i ≤ t)\), 保證沒有兩個障礙物佔據同一個格子。
在任何時候,如果蛇的頭部碰到蛇的身體(即蛇的其他格子) ,或碰到棋盤上的障礙物,或移動到棋盤的邊界之外,那蛇會立即死亡。
你的任務是,輸入遊戲配置以及 \(q\) 個操作,判斷蛇是否會死亡。
【輸入格式】
輸入的第一行包含四個非負整數 \(n, m, t, q\),具體含義見問題描述。
接下來 \(t\) 行,每行兩個整數,表示一個障礙物的座標。保證每個座標都在棋盤上,即在 \((1, 1)\) 到 \((n, m)\) 之間,且不存在重複的座標。
接下來一行兩個整數,表示遊戲開始時蛇的座標 \((x_s , y_s)\)。保證該座標在棋盤上,且不是任何一個障礙物的座標。
接下來 \(q\) 行,每行給出一個操作,具體格式和含義見問題描述。
【輸出格式】
如果在 \(q\) 個操作後蛇沒有死亡,輸出 \(-1\)
\(k\) 個操作之後死亡。
【樣例1】
樣例輸入 |
---|
3 4 2 10 |
1 3 |
3 3 |
1 1 |
1 D |
1 R |
2 |
1 R |
2 |
1 R |
1 U |
1 L |
1 L |
1 L |
樣例輸出 |
---|
8 |
資料規模與約定
在所有測試點中,有20%的測試點\(n = 1\)。
在所有測試點中,有40%的測試點\(t = 0\)。
在所有測試點中,有20%的測試點滿足任何時候蛇的長度不超過\(2\)。
以上三類特殊的測試點可能存在交叉。
對於全部測試點,\(1 ≤ n, m ≤ 100,0 ≤ t < nm,1 ≤ q ≤ 10000\)。
題解
簡單模擬題,甚至不需要思考。用一個集合儲存所有訪問後會導致蛇死掉的點,並使用一個佇列儲存整隻蛇的位置。每當貪吃蛇伸長身子時在佇列尾端push入伸長後到達的新位置,並將新位置加入集合。當貪吃蛇縮短時則彈出隊首元素,並刪除集合中的該點。
ps:需要注意的是,\(n\)代表的是地圖的行數,而\(m\)代表的是地圖的列數。因此,事實上題面裡的\(x_1\)代表的實際上是縱向座標,\(y_1\)則代表橫向座標。因此,我們需要先讀入變數\(y\)再讀入變數\(x\)。
#include<bits/stdc++.h>
using namespace std;
inline int read(){
register char c=getchar();register int f=1,_=0;
while(c>'9' || c<'0')f=(c=='-')?-1:1,c=getchar();
while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=getchar();
return _*f;
}
int n,m,t,q;
set<pair<int,int> > stone;
queue<pair<int,int> > snake;
int nowy,nowx;
int cmd;
bool init(){
if(nowx<=0 || nowx>m)return false;
if(nowy<=0 || nowy>n)return false;
if(stone.count(make_pair(nowy,nowx)))return false;
return true;
}
int step=0;
bool sc=1;
int main(){
freopen("snake.in","r",stdin);
freopen("snake.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
n=read();m=read();t=read();q=read();
//cout<<"max y:"<<n<<" max x:"<<m<<endl;
int y,x;
for(register int i=0;i<t;i++){
y=read();x=read();
stone.insert(make_pair(y,x));
//cout<<"stone:"<<x<<" "<<y<<endl;
}
y=read();x=read();
//cout<<"snakenow:"<<x<<" "<<y<<endl;
snake.push(make_pair(y,x));
stone.insert(make_pair(y,x));
nowx=x,nowy=y;
//cout<<x<<" "<<y;
while(q--){
step++;
cmd=read();
if(cmd==1){
char cas=' ';
while(cas==' ')cas=getchar();
//cout<<cmd<<" "<<cas<<endl;
if(cas=='U'){
nowy-=1;
if(!init()){
sc=0;
break;
}
snake.push(make_pair(nowy,nowx));
stone.insert(make_pair(nowy,nowx));
}
if(cas=='D'){
nowy+=1;
if(!init()){
sc=0;
break;
}
snake.push(make_pair(nowy,nowx));
stone.insert(make_pair(nowy,nowx));
}
if(cas=='L'){
nowx-=1;
if(!init()){
sc=0;
break;
}
snake.push(make_pair(nowy,nowx));
stone.insert(make_pair(nowy,nowx));
}
if(cas=='R'){
nowx+=1;
if(!init()){
sc=0;
break;
}
snake.push(make_pair(nowy,nowx));
stone.insert(make_pair(nowy,nowx));
}
//cout<<nowx<<" "<<nowy<<endl;
}
else{
stone.erase(snake.front());
snake.pop();
}
//cout<<"snakenow:"<<nowx<<" "<<nowy<<endl;
}
if(sc)cout<<-1<<endl;
else cout<<step<<endl;
return 0;
}
T2 分糖果
【問題描述】
到了學期末,在幼兒園工作的劉老師要為自己所帶班級的小朋友分發糖果。
劉老師的班上共有\(n\)名小朋友,第 i 位小朋友對糖果的喜愛程度為\(a_i\),他在本學期的表現評分為\(b_i\)。劉老師分配糖果的方法如下:
1. 以某個順序安排這\(n\)位小朋友排成一排,劉老師從頭到尾逐一分配糖果。
2. 隊伍中的 第\(i\)位小朋友至少獲得的糖果數量為前\(i\)位小朋友對糖果的喜愛程度之和。
3. 由於第\(i\)位小朋友可以看見第\(i-1\)位小朋友獲得的糖果數量,為了不讓第\(i\)位小朋友覺得不公平,劉老師保證第\(i\)位小朋友獲得的糖果不少於第\(i-1\)位小朋友。
4. 在為第 i 位小朋友分配完糖果後, 劉老師將額外再獎勵第 i 位小朋友數量為\(b_i\)的糖果。
我們設第\(i\)位小朋友獲得的糖果數量為\(c_i\),形式化地講:
\[c_i = \begin{cases} a_1+b_1 & i=1 \\ max(c_{i-1},\sum\limits_{j=1}^{i}a_j)+b_i & i + j \leq n \end{cases}\]
由於預算有限,劉老師希望你能幫她安排這\(n\)位小朋友的順序,使得獲得糖果最多的小朋友,所獲得的糖果數量儘可能少。
【輸入格式】
第一行包含一個正整數\(T\),表示測試資料的組數。
接下來描述這\(T\)組測試資料,每組陣列的第一行包含一個正整數\(n\),表示劉老師班上小朋友的數量。
每組資料接下來\(n\)行中,每行兩個正整數,分別為\(a_i和b_i\),含義如問題描述中所述。
【輸出格式】
共\(T\)行,每行包含一個整數,表示被分配到最多糖果的那位小朋友最少獲得的糖果數量。
【樣例1】
樣例輸入 |
---|
1 |
3 |
4 1 |
2 2 |
1 2 |
樣例輸出 |
---|
8 |
【樣例2】
樣例輸入 |
---|
1 |
12 |
9 68 |
18 45 |
52 61 |
39 83 |
63 67 |
45 99 |
52 54 |
82 100 |
23 54 |
99 94 |
63 100 |
52 68 |
樣例輸出 |
---|
902 |
資料規模與約定
\(n \le 50000,1 \le a_i,b_i \le 10^9\)。
題解
簡單題,很明顯的貪心。有兩種貪心的方法,先講部分分的半錯誤貪心。
對於整個序列來說,我們考慮每個節點的\(a_i\)和其\(b_i\)對答案的影響。因為任取一個節點\(i\),其答案的值都是\(b_i+x\)的形式,因此我們排除\(b_i\)對答案的影響。(這種常見思路只能得部分分的原因就在這裡,後文說明)則因此,對答案有影響的只剩下\(a_i\)。因為第\(i\)個人一定可以得到至少\(i-1\)個人得到的糖的數量,因此我們可以確定一個序列的最大值一定在序列末尾。此時要求\(\sum\limits_{j=1}^ia_j\)的值最小,因此我們將整個序列按\(a_i\)的值從小到大排序即可。最後得到的答案總會滿足其\(\sum\limits_{j=1}^ia_j\)最小。
貼出程式碼。
#include<bits/stdc++.h>
#define maxn 50005
using namespace std;
inline char get(){
static char buf[30],*p1=buf,*p2=buf;
return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline long long read(){
register char c=get();register long long f=1,_=0;
while(c>'9' || c<'0')f=(c=='-')?-1:1,c=get();
while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=get();
return _*f;
}
struct edge{
long long a,b;
}E[maxn];
bool cmp(edge a,edge b){
return a.a<b.a;
}
long long n;
long long tot[maxn];
long long pre[maxn];
int main(){
//freopen("candy.in","r",stdin);
//freopen("candy.out","w",stdout);
long long t;
t=read();
while(t--){
n=read();
for(register long long i=1;i<=n;i++)E[i].a=read(),E[i].b=read();
sort(E+1,E+1+n,cmp);
pre[1]=E[1].a;
for(register int i=2;i<=n;i++)pre[i]=pre[i-1]+E[i].a;
tot[1]=E[1].a+E[1].b;
for(register long long i=2;i<=n;i++){
tot[i]=max(tot[i-1],pre[i])+E[i].b;
}
printf("%lld\n",tot[n]);
}
return 0;
}
部分分貪心錯誤性的證明
事實上,在考慮之前的貪心時,我們完全忽略了\(b_i\)對答案的影響。事實上,當\(b_i\)足夠大導致\(c_i\)足夠大時,其將對往後的答案造成影響,因為整個序列的值應該單調上升。我們考慮讓兩個數\(i和j\)中的\(b\)均對答案產生影響,則應該考慮在排序時將\(a\)和\(b\)同時納入一個不等式中。我們通過構造資料可以發現,\(a_i和b_j\)以及\(a_j和b_i\)可以互相影響,再造幾組樣例自測,因此我們將上述程式碼中的排序稍作修改即可。
bool cmp(edge a,edge b){
return min(a.a,b.b)<min(b.a,a.b);
}
T3 序列
【問題描述】
已知一個正整數陣列中包含 n 個正整數,依次為 \(a_1\) , \(a_2\) , \(…\) , \(a_n\) 。
我們將進行 \(m\) 次操作。對於第 \(j\) 次操作,會指定一個位置 \(p_j\) ,將所有位置 \(k\) 滿足 \(p_j ≤ k ≤ n\) 且大小滿足 \(a_k ≤ a\), \(p_j\) 的正整數 \(a_k\) 從陣列中拿出,並將這些正整數按照從小到大 的順序進行排序,之後重新放回陣列中。
舉一個例子,對於正整數陣列 \(1\) \(4\) \(2\) \(5\) $ 3$ 而言,若選擇位置 \(p_j = 2\),則拿出的正整數為 \(4\) \(2\) \(3\),分別對應 \(k = 2\), \(k = 3\), \(k = 5\),拿出的正整數排序以後變為 \(2\) \(3\) \(4\),再將它們放回到陣列中,陣列變成 \(1\) \(2\) \(3\) \(5\) \(4\)。
在每次操作以後,你需要回答整個陣列中逆序對的總數。正整數 \(a_i\) 與 \(a_j\) 構成一個逆序對,當且僅當 \(a_i > a_j\) 且 \(1 ≤ i < j ≤ n\)。
【輸入格式】
輸入第一行包含兩個正整數 \(n\) 和 \(m\),其中 \(n\) 表示陣列的長度,\(m\) 表示操作的次數。
接下來一行包括\(n\)個正整數,依次表示正整數陣列中的元素 \(a_1 , a_ 2 ,…, a_ n\) 。
接下來一行包括\(m\)個正整數,依次表示詢問的位置 \(p _1 , p _2 ,…, p_ m\) 。
【輸出格式】
輸出檔案共包括 \(m\) 行,其中第 $ j $ 行表示第 \(j\) 次操作以後整個陣列的逆序對總數。
【樣例輸入1】
5 3 |
---|
1 4 2 5 3 |
5 2 4 |
【樣例輸出1】
樣例輸出 |
---|
3 |
1 |
0 |
【樣例輸入2】
7 4 |
---|
7 7 1 4 2 5 3 |
6 4 2 1 |
【樣例輸出2】
樣例輸出 |
---|
12 |
10 |
5 |
0 |
【資料規模與約定】
\(1\le n \le 500000,1\le m \le 500000,1\le a_i \le 10^9,1\le p_j \le n\)。
【題解】
每次把拿出來的數暴力排序,然後塞回去,
每次操作時候都重新求一次逆序對。使用\(O(n^2)\)複雜度演算法進行排序和求逆序對,期望得分 20 分。使用 \(O(n logn)\)複雜度的演算法進行排序和求逆序對則可以得到45~50分。這裡給出45分的做法。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500010;
int tmp[maxn], t[maxn];
int n, m, a[maxn], b[maxn];
long long ans = 0;
inline bool my_cmp(int a, int b) {
return a > b;
}
inline void merge_sort(int l, int r) {
int p1, p2, p, mid;
if (l == r) return ;
mid = (l + r) >> 1;
merge_sort(l, mid);
merge_sort(mid + 1, r);
p1 = l, p2 = mid + 1, p = 0;
while (p1 <= mid || p2 <= r) {
if (p1 <= mid && (p2 > r || a[p1] <= a[p2])) {
b[++p] = a[p1];
p1++;
} else {
b[++p] = a[p2];
p2++;
ans += mid - p1 + 1;
}
}
for (register int i = 1; i <= p; i++) a[l + i - 1] = b[i];
}
int main() {
//freopen("sort.in", "r", stdin);
//freopen("sort.out", "w", stdout);
scanf("%d%d", &n, &m);
for (register int i = 1; i <= n; i++) cin >> a[i];
while (m--) {
int p;
ans = 0;
int c = 0;
scanf("%d", &p);
vector<int> vec;
memset(t, 0, sizeof t);
for (register int i = p; i <= n; i++) {
if (a[i] <= a[p]) {
vec.push_back(i);
t[c] = a[i];
c += 1;
}
}
sort(t, t + n + 1, my_cmp);
int cnt = 0;
for (register int i = vec.size() - 1; i >= 0; i--) {
a[vec[i]] = t[cnt];
cnt += 1;
}
for (register int i = 1; i <= n; i++) tmp[i] = a[i];
ans = 0;
merge_sort(1, n);
printf("%d\n", ans);
for (register int i = 1; i <= n; i++) a[i] = tmp[i];
}
return 0;
}
AC的思路也很容易想到
使用線段樹或平衡樹維護對答案仍有貢獻的\(b[i]\)即可,每次刪掉一個數,就把這個數線上段樹中改成無窮大。線段樹的每次操作相當於在區間\([p_j,n]\)中求最小值的下標。每個數均攤被訪問\(1\)次,總複雜度\(O((n + m)logn)\)
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const long long maxn = 500010;
long long a[maxn];
long long minv[4 * maxn];
bool vis[maxn];
long long num[maxn];
long long ans[maxn];
long long to[maxn];
long long back[maxn];
inline void pushup(long long id) {
minv[id] = minv[id << 1] + minv[id << 1 | 1];
}
inline void build(long long id, long long l, long long r) {
if (l == r) {
minv[id] = a[l];
return;
}
long long mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
pushup(id);
}
inline void update(long long id, long long l, long long r, long long x, long long v) {
if (l == r) {
minv[id] += v;
return;
}
long long mid = (l + r) >> 1;
if (x <= mid) {
update(id << 1, l, mid, x, v);
} else {
update(id << 1 | 1, mid + 1, r, x, v);
}
pushup(id);
}
inline long long query(long long id, long long l,long long r,long long x,long long y){
if(x <= l && r <= y){
return minv[id];
}
long long mid = (l + r) >> 1;
long long ans = 0;
if(x <= mid){
ans = ans + query(id << 1, l, mid, x, y);
}
if(y > mid){
ans = ans + query(id << 1 | 1, mid + 1, r, x, y);
}
return ans;
}
long long aim1[maxn];
long long aim2[maxn];
int main() {
/*freopen("sort.in","r",stdin);
freopen("sort.out","w",stdout);*/
long long n,m;
scanf("%lld%lld",&n,&m);
//cin >> n >> m;
for(register int i = 1;i <= n;i++){
scanf("%lld",&num[i]);
//cin >> num[i];
aim1[i] = num[i];
}
sort(aim1+1,aim1+1+n);
int mm = unique(aim1+1,aim1+1+n) - aim1 - 1;
for(register int i=1;i<=n;i++){
num[i] = lower_bound(aim1+1,aim1+1+mm,num[i]) - aim1;
}
//cout << "haha";
/*for(register int i = 1;i <= n;i++){
scanf("%lld",&aim1[i]);
//cin >> aim1[i];
aim2[i] = aim1[i];
}
sort(aim1+1,aim1+1+n);
map<long long,long long> MM;
for(register long long i = 1;i <= n;i++){
if(MM[aim1[i]] != 0){
continue;
}
MM[aim1[i]] = i;
}
for(register long long i = 1;i <= n;i++){
num[i] = MM[aim2[i]];
//cout << num[i] << " ";
}
//cout << endl;*/
memset(a,0,sizeof(a));
build(1, 1, n);
for (register long long i = n; i >= 1; i--) {
if(num[i] == 1){
ans[i] = 0;
update(1,1,n,num[i],1);
continue;
}
ans[i] = query(1,1,n,1,num[i]-1);
update(1,1,n,num[i],1);
}
long long now = 0;
for(register long long i = 1;i <= n;i++){
now += ans[i];
to[i] = i+1;
back[i] = i-1;
}
back[n+1] = n;
to[0] = 1;
memset(vis,false,sizeof(vis));
while(m--){
long long p;
scanf("%lld",&p);
//cin >> p;
if(vis[p] == true){
cout << now << endl;
continue;
}
for(register long long i = p;i <= n;i = to[i]){
//cout << i << " ";
if(num[i] <= num[p]){
now -= ans[i];
vis[i] = true;
ans[i] = 0;
long long ff = back[i];
to[ff] = to[i];
back[to[i]] = ff;
//back[to[i]] = back[i];
//to[back[i]] = to[i];
/*for(int i = 1;i <= n;i++){
cout << to[i] << " ";
}
cout << endl;*/
}
}
//cout << endl;
printf("%lld\n",now);
//cout << now << endl;
}
return 0;
}