英単語の覚え方

7000語くらい知ってる人(大学受験でそんなに苦労しないレベル)が15000~20000語くらい知ってる人になるための方法です。

自分の体験にしか基づいていないので、これが多くの人に適用可能かとかそういうことは一切考えてないのであしからず。

文脈で覚えるべきなのか単語リストとして覚えるべきなのか?英英で覚えるべきなのか英和で覚えるべきなのか?etc

以下、僕がやったことは単語リストで覚えて、多読で実際に触れることで実際の使用例を後から文脈で知るという方法をとりました。最初から文脈で覚えようとすると異常なほどに労力が必要でペースが落ちるので、非効率だと思います。特に高校生レベルの単語ならまだしも、それ以上になってくるとものすごい多義語とかは減ってくるわけで、和訳を見ただけでもう文脈も限られてくるようなものばかりです。(たとえば、sonnet (イタリアの14行詩) のような単語が、文脈で覚えないと情報量が落ちるかといわれると、そんなことはないと思います)。ただし、具体的なものの名称で、単語の説明だけ見ても想像が困難なものがあり、これは Google 画像検索によってイメージで覚えることが得策なこともあります。(僕が想像に苦労したのは、西洋の城に関する単語です。例えば portcullis という単語は「落とし格子」という意味ですが、城といえばまず鶴ヶ城を思い浮かべるような人には想像が難しいでしょう?)
英英で覚えるべきか英和で覚えるべきかに関しては、どちらも一長一短あるので好きなほうを選べばいいと思います。個人的には英和が(多くの場合)ストレスがないのでおすすめです。英和の良い点は、「パッと見で分かるので、単語を回すスピードが上がる」「訳語の意味を勘違いしていたせいで間違って覚える心配が少なめ(無いという事ではありません)」、英英の良い点は「説明が豊富でより的確」「GREレベルですら英和辞典に載ってない単語が意外とあるが、英英には必ず載ってる」ということでしょうか。
もう一つは媒体ですが、僕は紙で覚えようと思ったことはありません。格安で数千枚の単語カードが買えるなら考えなくもないのですが、本だと順番が固定されていたりする点、自分でカードを準備するには大変過ぎる点がネックでした。また、音声も付加して覚えるというのはすごく良いアイデアだと思います。しかし、これは自力で辞書と単語リストだけから環境を構築する場合はほぼ無理です。

覚えて思ったこと等

語幹を使って覚えるというのは、ある程度助けになります。例えば、mal-は悪いことですし、dys-は何かが壊れていたり失っていたりするものですし、arbo-は樹木に関しています。しかし、in-が特に厄介です。
単語と訳語の対応だけを覚えていると、実際に使えないというのは広く言われています。これは正しいです。例えば、infiltrate にはどのようなときにin/throughが付くのかとかは、infiltrateの意味が分かっていても全く知りません。
ただし、そのような難しい単語で、専門的な名詞や形容詞というわけでもないものを、作家でもないノンネイティブが使うときが来るのでしょうか?特にノンネイティブが使う複雑な動詞というのは、科学的な論文に登場しがちな単語だけな気がします。

厄介な外来語

mikanのGRE/TOEFLを終えて新しい単語リストを始めたとき、フランス語の単語を勉強しているような錯覚に囚われるほど、外来語が大量に登場しました。外来語は発音が意味不明で、複数形が意味不明で、語幹が全く通用しないものが多く、さらに意味がやけにマイナーなことが多いので、単語リストのため、試験のためだけに勉強しているような気分になります。特に感情が消えるフランス語由来の単語を並べてみます。発音してみましょう。

  • malapropos
  • maladroit
  • verdigris
  • cliche
  • raconteur
  • patois

実際、何をやったか

で、実際僕がやったことをまとめていきます。

  1. mikan に出会う。mikan は本当に良いアプリです。ただし、ある程度覚えてからが本当に不便です。あと、四択クイズはいらないですね。やったことは、「とりあえず出来る限り早く全レベルを解禁し、学習済みにする」→「『全ての単語』モードからランダムを選び、150個カードをめくる」→「分からなかった単語が未学習に戻るが、それを全部学習済みに埋めなおす」を毎日やることです。3ヶ月くらいでGREを全部やり、次の3ヵ月くらいでTOEFLを全部やりました。これだけでもかなり十分だと思います。具体的には、Eragonシリーズはそこまで辞書に頼る必要がなくなります。
  1. mikan の GRE/TOEFL 単語を9割以上覚えてしまったことと、覚えた単語が多いときの効率の悪さに不満を感じたので、自分でプログラムを作って今までmikanで毎日やってきたことがより効率的に出来るものを作りました。これが #1日150Shokubutsuランダム でおなじみのShokubutsuです。単語リストはネット上にあった難しめ(なかなか難しい!)のものを使いました。これが割りとできるようになるとEragonシリーズは10ページに1回くらいしか辞書を引かなくてよくなります。
  1. やめたことは、Ankiに12000語レベルのものを大量にぶち込んでやったことです。これは単語リストの質が悪かった。(本当にどうでも良い単語ばかりな上和訳が変なのばっかりでした)
  1. これ以降やるべきことは、小説や新聞で実際に出会った単語を覚えることと、気が向いた単語をthesaurusで引いて眺めることだと思いました。しかし、小説を読んで知らない単語に線を引くと、高確率でスラングなのが気がかりです。

おまけ

almost_sorted

区間をK伸ばしてソートするだけです。

#include<stdio.h>
#include<algorithm>
#include<message.h>
#include"almost_sorted.h"
using namespace std;

long long p[3100000];
const int mod=1<<20;
int main(){
	int T=NumberOfNodes();
	int I=MyNodeId();
	int N=NumberOfFiles();
	int K=MaxDistance();
	int L=(long long)N*I/T;
	int R=(long long)N*(I+1)/T;
	int ans=0; 
	if(L==R){
		ans=0;
	}else{
		int left=max(0,L-K);
		int right=min(N,R+K);
		for(int i=left;i<right;i++){
			p[i-left]=Identifier(i);
		}
		std::sort(p,p+right-left);
		
		for(int i=L;i<R;i++){
			long long t=p[i-left]%mod*i%mod;
			ans=(ans+t)%mod;
		}
	}
	if(I==0){
		for(int i=1;i<T;i++){
			Receive(i);
			ans=(ans+GetInt(i))%mod;
		}
		printf("%d\n",ans);
	}else{
		PutInt(0,ans);
		Send(0);
	}
}

shhhh

中継点をランダムに決めることが本質です。ハッシュの計算を間違えました。

#include<stdio.h>
#include<algorithm>
#include<message.h>
#include<set>
#include<cassert>
#include"shhhh.h"
using namespace std;

int key[120];
int ha[110000];
int p[120];
int q[120];
int main(){
	int T=NumberOfNodes();
	int I=MyNodeId();
	int N=GetN();
	if(N<1000){
		if(I!=0)return 0;
		int ret=0;
		int at=0;
		while(1){
			int to=GetRightNeighbour(at);
			ret++;
			if(to==1)break;
			at=to;
		}
		int R=ret;
		int L=N-ret;
		if(L<R)printf("LEFT %d\n",L);
		else if(L>R)printf("RIGHT %d\n",R);
		else printf("WHATEVER %d\n",L);
		return 0;
	}
	srand(114514810);
	set<int>S;
	S.insert(0);
	S.insert(1);
	for(int i=0;i<100000;i++)ha[i]=-1;
	ha[0]=0;
	ha[1]=T;
	for(int i=1;i<T;i++){
		while(1){
			key[i]=rand()%N;
			if(!S.count(key[i]%100000)){
				ha[key[i]%100000]=i;
				S.insert(key[i]%100000);break;
			}
		}
	}
	//printf("%d: %d\n",I,key[I]);
	//fflush(stdout);
	int at=key[I];
	int dist=0;
	int nx=0;
	while(1){
		int to=GetRightNeighbour(at);
		//printf("%d: %d %d\n",I,at,to);
		//fflush(stdout);
		dist++;
		if(~ha[to%100000]&&(to==1||to==key[ha[to%100000]])){
			nx=ha[to%100000];
			break;
		}
		at=to;
	}
//	printf("%d: %d %d %d\n",I,key[I],nx,dist);
	//fflush(stdout);
	if(I==0){
		p[0]=nx;
		q[0]=dist;
		for(int i=1;i<T;i++){
			Receive(i);
			p[i]=GetInt(i);
			q[i]=GetInt(i);
		}
		int R=0;
		int L=0;
		int now=0;
		int cnt=0;
		while(1){
			int to=p[now];
			R+=q[now];
			cnt++;
			if(to==T){
				break;
			}
			now=to;
			if(cnt>T){
				assert(false);
			}
		}
		L=N-R;
		if(L<R)printf("LEFT %d\n",L);
		else if(L>R)printf("RIGHT %d\n",R);
		else printf("WHATEVER %d\n",L);
	}else{
		PutInt(0,nx);
		PutInt(0,dist);
		Send(0);
	}
}

load_balance

半分全列挙をどのスレッドでもやるのですが、条件を満たした結果(modとかで振り分ける)のみを各スレッドで考慮することにすると、log分もなんとかなります。
ただしこの解法はあんまり良くないようで、TLEぎりぎりです。(26個1があると26C13個の13を持つことになってそこがネック)

#include<stdio.h>
#include<algorithm>
#include<message.h>
#include"load_balance.h"
using namespace std;

long long p[60];
int m[110];
const int mod=53461;
int bt[60000];
int L,R;
int I;
int ls;
int rs;
long long left[11000000];
long long right[11000000];

void dfs(int a,int b,int c,long long d){
	if(a==b){
		if(bt[c%mod]==I){
			if(b==L)left[ls++]=d;
			else right[rs++]=d;
		}
		return;
	}
	dfs(a+1,b,(c+p[a])%mod,d+p[a]);
	dfs(a+1,b,(c+mod-p[a]%mod)%mod,d-p[a]);
}
int main(){
	int T=NumberOfNodes();
	I=MyNodeId();
	int N=GetN();
	for(int i=0;i<N;i++)p[i]=GetWeight(i);
	srand(114514810);
	for(int i=0;i<mod;i++){
		bt[i]=rand()%T;
	}
	L=N/2;
	R=N-L;
	dfs(0,L,0,0LL);
	dfs(L,N,0,0LL);
//	std::sort(left,left+ls);
	std::sort(right,right+rs);
	int ok=0;
	for(int i=0;i<ls;i++){
		if(binary_search(right,right+rs,left[i]))ok=1;
	}
	
	if(I==0){
		bool ret=false;
		if(ok==1)ret=true;
		for(int i=1;i<T;i++){
			Receive(i);
			int tmp=GetInt(i);
			if(tmp==1)ret=true;
		}
		if(ret)printf("POSSIBLE\n");
		else printf("IMPOSSIBLE\n");
	}else{
		PutInt(0,ok);
		Send(0);
	}
}

kolakoski

最初に配列の種みたいなのを割り振っておき、順々に種から次の世代を発生させていきます。
発生のさせ方ですが、最初に長さと1/2が切り替わる回数がわかるので、それらを求め親ノードにこれを転送します。親ノードは子ノードの左から順にこれらを使うことで、その子ノードで1/2どちらから始まるか、オフセットがどこかというのを渡すことができるようになります。

デバッグがしにくいです。スレッド数を下げる(3とか)のがおすすめです。

#include<stdio.h>
#include<algorithm>
#include<message.h>
#include"kolakoski.h"
using namespace std;

char a[2000];
long long N;
int Get(long long i){
	if(i>=N)return 0;
	return GetMultiplier(i);
}
char p[2][41000000];
long long X[110];
long long Y[110];
int main(){
	int T=NumberOfNodes();
	int I=MyNodeId();
	N=GetIndex();
	
	a[0]=1;
	a[1]=a[2]=2;
	int left=2;
	int right=3;
	while(right-left<T){
		if(a[left]==1){
			a[right++]=left%2+1;
			
		}else{
			a[right++]=left%2+1;a[right++]=left%2+1;
		}
		left++;
	}
	//if(I==0){for(int i=0;i<right;i++)printf("%d",a[i]);printf("\n");}
	if(I==T-1){
		long long ret=0;
		for(int i=0;i<right;i++){
			ret+=Get(i)*a[i];
	//		printf("%d",a[i]);
		}
	//	printf("\n");fflush(stdout);
		long long now=left;
		long long at=right;
		while(1){
			for(int i=0;i<T-1;i++){
				Receive(i);
				X[i]=GetLL(i);
				Y[i]=GetLL(i);
			}
			for(int i=0;i<T-1;i++){
				PutLL(i,now%2);
				PutLL(i,at);
				Send(i);
				now+=X[i];
				at+=Y[i];
			}
			bool end=false;
			for(int i=0;i<T-1;i++){
				Receive(i);
				long long tmp=GetLL(i);
				long long cnt=GetLL(i);
			
				ret+=tmp;
				if(cnt){
					end=true;
				}
			}
			if(end)break;
		}
		for(int i=0;i<T-1;i++){
				Receive(i);
				X[i]=GetLL(i);
				Y[i]=GetLL(i);
			}
			for(int i=0;i<T-1;i++){
				PutLL(i,-1);
				PutLL(i,-1);
				Send(i);
			}
		
		printf("%lld\n",ret);
	}else{
		int m=right-left;
		int L=m*I/(T-1)+left;
		int R=m*(I+1)/(T-1)+left;
		for(int i=L;i<R;i++){
			p[0][i-L]=a[i];
		}
		int ptr=0;
		int n=R-L;
		while(1){
			long long r1=0;
			for(int i=0;i<n;i++)r1+=p[ptr][i];
			PutLL(T-1,n);
			PutLL(T-1,r1);
			Send(T-1);
			Receive(T-1);
			int sc=GetLL(T-1);
			long long off=GetLL(T-1);
			if(sc<0)break;
			int nn=0;
			
			for(int i=0;i<n;i++){
				for(int j=0;j<p[ptr][i];j++){
					p[!ptr][nn++]=(sc+i)%2+1;
				}
			}
			long long res=0;
			long long end=0;
		//	printf("%d: ",off);
			for(int i=0;i<nn;i++){
				if(off+i>=N){end=1;break;}
				res+=Get(off+i)*p[!ptr][i];
		//		printf("%d",p[!ptr][i]);
			}
		//	printf("\n");fflush(stdout);
			PutLL(T-1,res);
			PutLL(T-1,end);
			Send(T-1);
			ptr=!ptr;
			n=nn;
		}
	}
}

DCJは、残り解法が分かってないのが4問あります。解法だけは読んでおこうかと思っています。